.NET MAUI - Usando o banco de dados SQLite - I


  Neste artigo vamos criar uma aplicação .NET MAUI para gerenciar um pequena biblioteca de livros pessoal realizando a persistência dos dados no SQLite.

As aplicações .NET MAUI podem acessar uma variedade de bancos de dados e isso inclui bancos de dados relacionais (MySQL, Oracle, PostgreSQL) e bancos de dados NoSQL (MongoDB). A escolha do banco de dados a ser usado vai depender das necessidades específicas do seu aplicativo e da plataforma de destino.

Em se tratando de banco de dados locais O SQLite é uma escolha popular para aplicativos móveis multiplataforma, incluindo aplicativos .NET MAUI.  Ele é incorporado diretamente na biblioteca .NET MAUI e permite que você crie bancos de dados locais leves para armazenar dados offline ou dados que não precisam ser compartilhados com um servidor.

O SQLite é um sistema de banco de dados local leve que se tornou um padrão da indústria para aplicativos móveis.
Ele é compacto, eficiente, altamente confiável, de código aberto e muito popular para aplicativos que precisam de um banco de dados leve e autônomo.

O SQLite é uma biblioteca em processo  que implementa um mecanismo de banco de dados SQL transacional independente, sem servidor e sem configuração e usa o sistema de arquivos local para armazenamento. (Ao eliminar o servidor, o SQLite elimina a complexidade.)

Assim, não há necessidade de suporte multitarefa ou comunicação entre processos do sistema operacional e o SQLite requer apenas leitura/gravação em algum armazenamento.

A biblioteca SQLite foi escrita originalmente em C e C++ e pode ser acessada através de um invólucro C#
onde podemos usar todos o conhecimento do C# para criar aplicações baseadas no SQLite.  Este invólucro C# refere-se à biblioteca nuget SQLite-net e suas dependências  que fornecem um mecanismo para mapear classes para tabelas e assim podemos trabalhar com objetos na memória fazendo a correspondência com as tabelas ou seja podemos fazer o mapeamento ORM - Object Relational Mapping.

Neste artigo vamos criar uma aplicação  .NET MAUI para gerenciar informações de uma pequena biblioteca pessoal e realizar o CRUD básico em informações sobre livros representando pela entidade Livro onde vamos usar o banco de dados SQLite.

recursos usados:

Criando o projeto

No VS 2022 Community vamos criar um projeto usando o template .NET MAUI e nomear a solução como MeuLivrosApp e o projeto como MeuLivros.

Após criar o projeto vamos remover as referências que não vamos usar e a seguir incluir os seguintes pacotes nugets no projeto:

<ItemGroup>
  <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
  <PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.5" />
  <PackageReference Include="SQLitePCLRaw.core" Version="2.1.5" />
  <PackageReference Include="SQLitePCLRaw.provider.dynamic_cdecl" Version="2.1.5" />
  <PackageReference Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.5" />
</ItemGroup>

1- sqlite-net-pcl  versão 1.8.116
2- SQLitePCLRaw.bundle_green
versão 2.1.5
3- SQLitePCL.Raw.core 
versão 2.1.5
4- SQLitePCLRaw.provider.dynamic_cdecl
versão 2.1.5
5- SQLitePCLRaw.provider.sqlite3
versão 2.1.5

Ao usar o SQLite em uma aplicação .NET MAUI, você precisa incluir este cinco  pacotes no seu projeto. Cada pacote desempenha um papel específico no acesso e funcionamento do SQLite no .NET.

  1. sqlite-net-pcl: É uma biblioteca simples e leve que fornece uma API fácil de usar para trabalhar com o SQLite no .NET. Ela oferece recursos para criar, consultar, atualizar e excluir registros em um banco de dados SQLite. É um pacote popularmente usado com o SQLite no ecossistema do .NET.
  2. SQLitePCLRaw.bundle_green: Esse pacote contém implementações de baixo nível para o SQLite, que são compartilhadas entre diferentes plataformas. Ele fornece suporte básico para o SQLite e é usado em conjunto com os pacotes SQLitePCLRaw.core e SQLitePCLRaw.provider.sqlite3 para obter acesso ao SQLite em diferentes plataformas.
  3. SQLitePCLRaw.core: Esse pacote contém a implementação principal do SQLitePCLRaw, que é uma camada de abstração de plataforma para o SQLite. Ele define interfaces e classes básicas para trabalhar com o SQLite e é usado para interagir com a implementação específica da plataforma fornecida pelo pacote SQLitePCLRaw.provider.sqlite3.
  4. SQLitePCLRaw.provider.dynamic_cdecl: Esse pacote contém uma implementação do provedor SQLitePCLRaw que usa a convenção de chamada "cdecl" para interagir com a biblioteca SQLite subjacente. É usado como um provedor para o SQLitePCLRaw e permite a interoperabilidade com a biblioteca SQLite nativa.
  5. SQLitePCLRaw.provider.sqlite3: Esse pacote contém a implementação específica da plataforma do provedor SQLitePCLRaw para a biblioteca SQLite nativa. Ele fornece acesso à biblioteca SQLite subjacente e lida com as operações de baixo nível necessárias para interagir com o SQLite em cada plataforma suportada.
  6. CommunityToolkit.Mvvm versão 8.2.1: - Este pacote Inclui uma biblioteca MVVM que facilita a implementação do padrão MVVM
  7. CommunityToolkit.Maui versão 5.3.0:  Este pacote é uma coleção de animações, comportamentos, conversores e visualizações personalizadas para desenvolvimento com .NET MAUI e simplifica as tarefas comuns na criação de aplicativos .NET MAUI.

Esses pacotes são necessários para permitir o acesso ao SQLite em uma aplicação .NET MAUI, fornecendo as funcionalidades e as abstrações necessárias para trabalhar com o banco de dados SQLite em diferentes plataformas suportadas.

Criando as pastas no projeto

Para organizar o código no nosso projeto vamos criar uma pasta MVVM e dentro desta pasta criar as pastas : Models, Views e ViewModels.

A seguir vamos criar uma pasta Services onde iremos criar o serviço para acessar o SQLite.  Abaixo temos a estrutura da solução criada:

Criando o domínio e os serviços

Vamos criar na pasta Models a classe Livro que define o nosso domínio:
 
using SQLite;
namespace
MeusLivros.MVVM.Models;

[Table(
"Livros")]
public
class Livro
{
   [PrimaryKey, AutoIncrement]
  
public int Id { get; set; }
   [MaxLength(200)]
  
public string Titulo { get; set; }
   [MaxLength(250)]
  
public string Autor { get; set; }
   [MaxLength(200)]
  
public string ImagemUrl { get; set; }
   [MaxLength(200)]
  
public string EmprestadoPara { get; set; }
  
public int Paginas { get; set; }
  
public DateTime DataLancamento { get; set; }
  
public bool LeituraConcluida { get; set; }
}

Nesta classe estamos usando os atributos da biblioteca SQLite.NET para realizar o mapeamento da classe para a tabela no SQLite

Os atributos mais comuns usados no .NET MAUI para mapear uma classe para tabelas no SQLite são:

- [Table]: Este atributo especifica o nome da tabela no banco de dados.
- [Column]: Este atributo especifica o nome da coluna na tabela.
- [PrimaryKey]: Este atributo especifica que a coluna é a chave primária da tabela.
- [NotNull]: Este atributo especifica que a coluna não pode ser nula.
- [ForeignKey]: Este atributo especifica que a coluna é uma chave estrangeira para outra tabela.
- [Unique]: Este atributo especifica que a coluna deve ter valores únicos.
- [AutoIncrement]: Este atributo especifica que a coluna deve ter um valor incrementado automaticamente.

A seguir vamos criar na pasta Services a interface ILivroService e a classe LivroService onde vamos implementar as operações de acesso aos dados usando o SQLite:

1- ILivroService

using MeusLivros.MVVM.Models;

namespace MeusLivros.Services;

public interface ILivroService
{
  Task InitializeAsync();
  Task<IEnumerable<Livro>> GetLivrosAsync();
  Task<IEnumerable<Livro>> GetLivroTituloAsync(
string titulo);
  Task<
int> CriaLivroAsync(Livro livro);
  Task<
int> DeletaLivroAsync(int id);
  Task<
int> AtualizaLivroAsync(Livro livro);
}

2- LivroService

using MeusLivros.MVVM.Models;
using
SQLite;

namespace MeusLivros.Services;

public class LivroService : ILivroService
{
  
private SQLiteAsyncConnection _dbConnection;
  
public async Task InitializeAsync()
   {
     
await SetUpDb();
   }

   private async Task SetUpDb()
   {
     
if (_dbConnection == null)
      {
        
string dbPath = Path.Combine(Environment.
                GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
               
"LivrosDB.db3");

         _dbConnection =
new SQLiteAsyncConnection(dbPath);
        
await _dbConnection.CreateTableAsync<Livro>();
       }
   }

   public async Task<int> CriaLivroAsync(Livro livro)
   {
      
return await _dbConnection.InsertAsync(livro);
   }

   public async Task<int> DeletaLivroAsync(int id)
   {
      
return await _dbConnection.DeleteAsync(id);
   }

   public async Task<int> AtualizaLivroAsync(Livro livro)
   {
      
return await _dbConnection.UpdateAsync(livro);
   }

   public async Task<IEnumerable<Livro>> GetLivrosAsync()
   {
     
var livros = await _dbConnection.Table<Livro>().ToListAsync();
     
return livros;
    }

    public async Task<IEnumerable<Livro>> GetLivroTituloAsync(string titulo)
    {

       var livros = await _dbConnection.Table<Livro>()
                          .Where(x => x.Titulo.Contains(titulo)).ToListAsync();
     
 return livros;
    }
}

Nesse código , o caminho do banco de dados é definido usando Environment.GetFolderPath para obter o diretório especial LocalApplicationData, que é usado para armazenar dados específicos do aplicativo. Em seguida, é definido o nome do arquivo do banco de dados ("LivrosDB.db3") e o caminho completo do banco de dados é obtido usando Path.Combine.

Temos o método InitializeAsync() que será responsável por configurar o banco de dados. O método SetUpDb() foi alterado para ser assíncrono e retornar uma Task, permitindo o uso de await para aguardar a criação da tabela.

Dessa forma, ao utilizar o serviço, você poderá chamar explicitamente o método InitializeAsync() para realizar a inicialização do banco de dados de forma assíncrona.

Criando as ViewModels

Vamos criar na pasta MVVM/ViewModels as view models usadas no projeto, iniciando com a view model LivrosViewModel que encapsula a lógica de apresentação e interações relacionadas aos livros e que implementa os comandos usando o atributo [RelayCommand] que é uma implementação da interface ICommand que expõe o método para a view.

1-LivrosViewModel

using CommunityToolkit.Mvvm.ComponentModel;
using
CommunityToolkit.Mvvm.Input;
using
MeusLivros.MVVM.Models;
using
MeusLivros.Services;
using
System.Collections.ObjectModel;

namespace MeusLivros.MVVM.ViewModels;

public partial class LivrosViewModel : ObservableObject
{

   private readonly ILivroService _livroService;
  
public ObservableCollection<Livro> MeusLivros { get; set; } = new();
  
public LivrosViewModel(ILivroService livroService)
   {
      _livroService = livroService;
   }

   [RelayCommand]
   public async Task GetLivros()
   {
      MeusLivros.Clear();
     
try
      {
       
await _livroService.InitializeAsync();
       
var livros = await _livroService.GetLivrosAsync();
       
if (livros.Any())
        {
          
foreach (var livro in livros)
           {
             MeusLivros.Add(livro);
            }
        }
     }
    
catch (Exception ex)
     {
       
await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
     }
}

[RelayCommand]
private async Task AddLivro() => await Shell.Current.GoToAsync("AddLivroPage");

[RelayCommand]
private async Task DeleteLivro(Livro livro)
{
  
var result = await Shell.Current.DisplayAlert("Deletar",
                $"Confirma exclusão do livro : \n\n \"
{livro.Titulo}\"?", "Sim", "Não");

   if (result is true)
   {
    
try
     {
       
await _livroService.InitializeAsync();
       
await _livroService.DeletaLivroAsync(livro.Id);
       
await GetLivros();
     }
    
catch (Exception ex)
     {
       
await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
     }
   }
}

[RelayCommand]
private async Task UpdateLivro(Livro livro) =>
   
await Shell.Current.GoToAsync("UpdateLivroPage", new Dictionary<string, object>
 {
    {
"LivroObject", livro }
  });
}

O construtor recebe uma instância de ILivroService como parâmetro e o serviço é injetado por meio de injeção de dependência no construtor.

A propriedade MeusLivros é uma propriedade pública que representa uma coleção de objetos Livro. Ela é do tipo ObservableCollection, e assim vai notificar automaticamente a interface do usuário (View) sobre alterações em seus elementos, garantindo que a interface do usuário seja atualizada quando a coleção for modificada.

A seguir temos os seguintes métodos decorados com o atributo [RelayCommand] que implementa a interface ICommand expondo o método para a view:

Método GetLivros:

  • Método AddLivro:
    • Método invocado quando você deseja adicionar um novo livro. O método navega para a página "AddLivroPage".
       
  • Método DeleteLivro:
    • Método acionado quando você deseja excluir um livro. O método exibe um alerta de confirmação e, se o usuário confirmar, chama o serviço para excluir o livro e, em seguida, chama GetLivros para atualizar a lista de livros.
       
  • Método UpdateLivro:
    • Método invocado quando você deseja atualizar um livro. O método navega para a página "UpdateLivroPage" e passa o objeto Livro selecionado como parâmetro.
  • 2- AddLivroViewModel

    Na página Index.razor vamos exibir uma imagem  alunos.png que esta contida na pasta wwwroot/images:

    using CommunityToolkit.Mvvm.ComponentModel;
    using
    CommunityToolkit.Mvvm.Input;
    using
    MeusLivros.MVVM.Models;
    using
    MeusLivros.Services;

    namespace MeusLivros.MVVM.ViewModels;

    public partial class AddLivroViewModel : ObservableObject
    {
     
    private readonly ILivroService _livroService;
      [ObservableProperty]
     
    private string _livroTitulo;
      [ObservableProperty]
     
    private string _livroAutor;
      [ObservableProperty]
     
    private string _livroImagemUrl;
      [ObservableProperty]
     
    private string _livroEmprestadoPara;
      [ObservableProperty]
     
    private int _livroPaginas;
      [ObservableProperty]
     
    private bool _livroLeituraConcluida;

      public AddLivroViewModel(ILivroService livroService)
      {
        _livroService = livroService;
      }

      [RelayCommand]
     
    private async Task AddLivro()
      {
       
    try
        {
         
    if (!string.IsNullOrEmpty(LivroTitulo))
          {
             Livro livro =
    new()
             {
               Titulo = LivroTitulo,
               Autor = LivroAutor,
               ImagemUrl = LivroImagemUrl,
               EmprestadoPara = LivroEmprestadoPara,
               Paginas = LivroPaginas,
               LeituraConcluida = LivroLeituraConcluida
             };
            
    await _livroService.InitializeAsync();
            
    await _livroService.CriaLivroAsync(livro);
            
    await Shell.Current.GoToAsync("..");
          }
         
    else
          {
            
    await Shell.Current.DisplayAlert("Error", "Livro sem Título", "OK");
          }
        }

        catch
    (Exception ex)
        {
         
    await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
        }
      }
    }

    A possui vários campos privados decorados com [ObservableProperty] que são usados para armazenar dados relacionados a um novo livro que o usuário deseja adicionar. Essas propriedades são observáveis, o que significa que qualquer alteração nelas notificará automaticamente a interface do usuário (View) para atualizar.

    A ViewModel também é responsável por controlar a lógica de adição de um novo livro à lista de livros do aplicativo. Ela interage com o serviço de livro para realizar as operações de adição e lida com possíveis erros durante o processo. As propriedades observáveis são usadas para vincular os dados da interface do usuário aos campos de entrada do novo livro, permitindo que a interface do usuário seja atualizada conforme o usuário insere informações. O uso de [RelayCommand] permite que um comando seja associado a uma ação de botão na interface do usuário, que, quando acionado, chama o método AddLivro.

    Temos nesta view model a definição do método decorado com o atributo [RelayCommand]:

  • Método AddLivro:
    • Método invocado quando o usuário deseja adicionar um novo livro.
    • O método faz o seguinte:
      • Verifica se o título do livro (LivroTitulo) não está vazio ou nulo. Se estiver, exibe um alerta de erro.
      • Se o título do livro não estiver vazio, cria um objeto Livro com os detalhes fornecidos pelo usuário (título, autor, URL da imagem, etc.).
      • Chama o serviço de livro para inicialização e, em seguida, chama o serviço para criar o livro assincronamente, usando o objeto Livro criado.
      • Após a adição bem-sucedida do livro, navega de volta para a página anterior (Shell.Current.GoToAsync("..")).
  • 3- UpdateLivroViewModel

    using CommunityToolkit.Mvvm.ComponentModel;
    using
    CommunityToolkit.Mvvm.Input;
    using
    MeusLivros.MVVM.Models;
    using
    MeusLivros.Services;

    namespace MeusLivros.MVVM.ViewModels;

    [QueryProperty(nameof(Livro), "LivroObject")]
    public
    partial class UpdateLivroViewModel : ObservableObject
    {
     
    private readonly ILivroService _livroService;
      [ObservableProperty]
     
    private Livro _livro;
     
    public UpdateLivroViewModel(ILivroService livroService)
      {
        _livroService = livroService;
      }

      [RelayCommand]
     
    private async Task UpdateLivro()
      {
       
    if (!string.IsNullOrEmpty(Livro.Titulo))
        {
          
    await _livroService.InitializeAsync();
          
    await _livroService.AtualizaLivroAsync(Livro);
          
    await Shell.Current.GoToAsync("..");
        }
       
    else
        {
           
    await Shell.Current.DisplayAlert("Error", "Livro sem Título", "OK");
        }
       }
    }

    Esta ViewModel é responsável por controlar a lógica de atualização dos detalhes de um livro no aplicativo. Ela recebe um parâmetro de consulta (o objeto do livro a ser atualizado) da página anterior e fornece a funcionalidade para atualizar os detalhes desse livro.

    As propriedades observáveis são usadas para vincular os dados da interface do usuário ao livro a ser atualizado, permitindo que a interface do usuário seja atualizada conforme o usuário faz alterações. O uso de [RelayCommand] permite que um comando seja associado a uma ação de botão na interface do usuário, que, quando acionado, chama o método UpdateLivro.

    Nesta view model temos a definição do método decorado com o atributo [RelayCommand]:

  • Método UpdateLivro:
    • Este é um método assíncrono decorado com [RelayCommand]. Ele é acionado quando o usuário deseja atualizar um livro.
    • O método faz o seguinte:
      • Verifica se o título do livro (Livro.Titulo) não está vazio ou nulo. Se estiver, exibe um alerta de erro.
      • Se o título do livro não estiver vazio, chama o serviço de livro para inicialização e, em seguida, chama o serviço para atualizar o livro assincronamente, usando o objeto Livro armazenado no campo _livro.
      • Após a atualização bem-sucedida do livro, navega de volta para a página anterior (Shell.Current.GoToAsync("..")).
  • Com isso podemos iniciar a criação das Views e fazer a vinculação com as respectivas view models usando o BindingContext.

    Faremos isso na próxima parte do artigo.

     "Se confessarmos os nossos pecados, ele é fiel e justo para nos perdoar os pecados, e nos purificar de toda a injustiça."
    1 João 1:9
     

    Referências:


    José Carlos Macoratti