Flutter - Acessando o SQLite


 Hoje veremos como acessaro SQLite no Flutter usando o plugin sqflite.

O Flutter é um SDK de aplicativo móvel do Google que ajuda a criar aplicativos móveis modernos para iOS e Android usando uma única (quase) base de código.

Se você não conhece o Flutter veja o meu artigo :  Flutter - Primeiros contatos e impressões

Acessando o SQLite

Podemos acessar o banco de dados SQLite no Flutter usando o plugin sqlflite cuja documentação pode ser acessada neste link:  sqflite

Este plugin oferce os seguintes recursos:

Existe um procedimento padrão básico que geralmente consiste em:

  1. Adicionar as dependências do sqlflite no arquivo pubspec.yaml;
  2. Criar uma classe que representa o modelo de domínio;
  3. Criar uma classe com os métodos para acessar o banco de dados, ou seja, a classe database helper;
  4. Criar o layout da sua aplicação Flutter;

Criando o projeto Flutter e definindo as dependências

No Visual Studio Code tecle CTRL+ SHIFT+P para abrir a paleta de comandos e a seguir selecione a opção : Fluter:New Project



A seguir informe o nome do projeto : flutter_database1 e tecle ENTER

Na janela de diálogo a seguir selecione a pasta onde o projeto vai ser salvo e clique em :
Select a folder to create the project in

O Flutter vai criar um projeto padrão onde todo o código da aplicação vai estar no arquivo main.dart dentro da pasta lib do projeto.

Vamos iniciar definindo no arquivo pubspec.yaml as dependências ao sqflite ao path_provider :

A dependência path_provider é um plug-in Flutter para encontrar locais comumente usados no sistema de arquivos.

Criando a classe de acesso aos dados

Vamos agora criar a classe de acesso aos dados, ou seja, a classe database helper onde vamos definir os métodos para acessar o banco de dados.

Para simplificar vamos usar esta classe para também criar o banco de dados e a tabela que iremos acessar.

Vamos criar o banco de dados ExemploDB.db e uma tabela chamada contato com 3 campos : id, nome e idade

No projeto criado, dentro da pasta lib, crie o arquivo database_helper.dart e a seguir inclua o código abaixo neste artigo:

import 'dart:io';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
class DatabaseHelper {  
  static final _databaseName = "ExemploDB.db";
  static final _databaseVersion = 1;
  static final table = 'contato';  
  static final columnId = '_id';
  static final columnNome = 'nome';
  static final columnIdade = 'idade';
  // torna esta classe singleton
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
  // tem somente uma referência ao banco de dados
  static Database _database;

  Future<Database> get database async {
    if (_database != null) return _database;
    // instancia o db na primeira vez que for acessado
    _database = await _initDatabase();
    return _database;
  }  
  // abre o banco de dados e o cria se ele não existir
  _initDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);
    return await openDatabase(path,
        version: _databaseVersion,
        onCreate: _onCreate);
  }
  // Código SQL para criar o banco de dados e a tabela
  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE $table (
            $columnId INTEGER PRIMARY KEY,
            $columnNome TEXT NOT NULL,
            $columnIdade INTEGER NOT NULL
          )
          ''');
  }  
  // métodos Helper
  //----------------------------------------------------
  // Insere uma linha no banco de dados onde cada chave 
  // no Map é um nome de coluna e o valor é o valor da coluna. 
  // O valor de retorno é o id da linha inserida.
  Future<int> insert(Map<String, dynamic> row) async {
    Database db = await instance.database;
    return await db.insert(table, row);
  }
  // Todas as linhas são retornadas como uma lista de mapas, onde cada mapa é
  // uma lista de valores-chave de colunas.
  Future<List<Map<String, dynamic>>> queryAllRows() async {
    Database db = await instance.database;
    return await db.query(table);
  }
  // Todos os métodos : inserir, consultar, atualizar e excluir, 
  // também podem ser feitos usando  comandos SQL brutos. 
  // Esse método usa uma consulta bruta para fornecer a contagem de linhas.
  Future<int> queryRowCount() async {
    Database db = await instance.database;
    return Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM $table'));
  }
  // Assumimos aqui que a coluna id no mapa está definida. Os outros
  // valores das colunas serão usados para atualizar a linha.
  Future<int> update(Map<String, dynamic> row) async {
    Database db = await instance.database;
    int id = row[columnId];
    return await db.update(table, row, where: '$columnId = ?', whereArgs: [id]);
  }
  // Exclui a linha especificada pelo id. O número de linhas afetadas é
  // retornada. Isso deve ser igual a 1, contanto que a linha exista.
  Future<int> delete(int id) async {
    Database db = await instance.database;
    return await db.delete(table, where: '$columnId = ?', whereArgs: [id]);
  }
}

O código acima esta na linguagem Dart e se você nunca viu um código Dart pode estranhar a sintaxe.

Por exemplo, o que significa Future ?

Um código Dart é executado em uma única thread. Se o código for bloqueado por estar executando uma operação muito longa ou estiver esperando por uma operação de arquivo, o programa inteiro vai 'congelar'.

As operações assíncronas permitem que o seu programa conclua outro trabalho enquanto aguarda a conclusão de uma operação. O Dart usa objetos Future (futures) para representar os resultados de operações assíncronas. Para trabalhar com futures, você pode usar o async e o await ou a API Future.

Um future é um objeto Future<T>, que representa uma operação assíncrona que produz um resultado do tipo T. Se o resultado não for um valor utilizável, o tipo do future será Future<void>. Quando uma função que retorna um future é invocada, duas coisas acontecem:

  1. A função enfileira o trabalho a ser feito e retorna um objeto Future não concluído;
  2. Posteriormente, quando a operação for concluída, o objeto Future será concluído com um valor ou com um erro;

Ao escrever código que depende de um future, você tem duas opções:

  1. Use async e aguarde
  2. Use a API Future

O código acima usa Future e Future<T>. A classe Future<T> é um objeto que representa um cálculo atrasado.

Future é usado para representar um valor potencial, ou um erro, que estará disponível em algum momento no futuro. Receptores de um Futuro podem registrar callback (retornos de chamada) que manipulam o valor ou o erro assim que estiverem disponíveis.

Outro recurso usado é o Map.

O recurso Map no Dart é um objeto que associa chaves ao valor. Nos Maps DART, existe uma interface projetada para manipular uma coleção de chaves que apontam para um valor.

Assim, o objeto Map é um par chave/valor simples. As chaves e valores em um mapa podem ser de qualquer tipo, e, como um Map é uma coleção dinâmica, Maps podem crescer e diminuir em tempo de execução.

Podemos declarar um Map de duas maneiras:

  1. Usando Literais de Mapa
  2. Usando um construtor de mapa

Criando a interface UI

Vamos agora definir uma interface bem simples no arquivo main.dart.

Como esse é nosso primeiro contato com o Sqlite não vou criar uma interface onde o usuário vai poder informar os valores.

Para simplificar vamos criar apenas 4 botões usando o widget RaisedButton empilhados em um widget Column.

Em cada button definimos o callback onPressed que vão chamar os métodos para realizar as operações CRUD.

Abra o arquivo main.dart e substitua o código pelo código abaixo:

import 'package:flutter/material.dart';
import 'package:flutter_database1/database_helper.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SQFlite Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  // referencia nossa classe single para gerenciar o banco de dados
  final dbHelper = DatabaseHelper.instance;
  // layout da homepage
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Exemplo de CRUD básico'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text('Inserir dados', style: TextStyle(fontSize: 20),),
              onPressed: () {_inserir();},
            ),
            RaisedButton(
              child: Text('Consultar dados', style: TextStyle(fontSize: 20),),
              onPressed: () {_consultar();},
            ),
            RaisedButton(
              child: Text('Atualizar dados', style: TextStyle(fontSize: 20),),
              onPressed: () {_atualizar();},
            ),
            RaisedButton(
              child: Text('Deletar dados', style: TextStyle(fontSize: 20),),
              onPressed: () {_deletar();},
            ),
          ],
        ),
      ),
    );
  }
   // métodos dos Buttons 
  void _inserir() async {
    // linha para incluir
    Map<String, dynamic> row = {
      DatabaseHelper.columnNome : 'Macoratti',
      DatabaseHelper.columnIdade  : 53
    };
    final id = await dbHelper.insert(row);
    print('linha inserida id: $id');
  }
  void _consultar() async {
    final todasLinhas = await dbHelper.queryAllRows();
    print('Consulta todas as linhas:');
    todasLinhas.forEach((row) => print(row));
  }
  void _atualizar() async {
    // linha para atualizar
    Map<String, dynamic> row = {
      DatabaseHelper.columnId   : 1,
      DatabaseHelper.columnNome : 'Maria',
      DatabaseHelper.columnIdade  : 32
    };
    final linhasAfetadas = await dbHelper.update(row);
    print('atualizadas $linhasAfetadas linha(s)');
  }
  void _deletar() async {
    // Assumindo que o numero de linhas é o id para a última linha
    final id = await dbHelper.queryRowCount();
    final linhaDeletada = await dbHelper.delete(id);
    print('Deletada(s) $linhaDeletada linha(s): linha $id');
  }
}

Executando o projeto usando o emulador do Genymotion temos o resultado abaixo:



Vamos primeiro incluir um registro : Macoratti,53 ,e, a seguir vamos consultar, depois vamos atualizar os dados para Maria,32, e consultar novamente. Para concluir vamos deletar.

Após realizar as operações consultando a console vemos o resultado exibido conforme abaixo:

Pegue os arquivos do projeto aqui : Arquivos_Projeto_1.zip

"Não sejais vagarosos no cuidado; sede fervorosos no espírito, servindo ao Senhor;
Alegrai-vos na esperança, sede pacientes na tribulação, perseverai na oração;"
Romanos 12:11,12

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

Quer aprender os conceitos da Programação Orientada a objetos ?

Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ?

Referências:


José Carlos Macoratti