Flutter - Persistência

De Aulas
Revisão de 10h35min de 17 de março de 2023 por Admin (discussão | contribs) (→‎contactDao.dart)

Afluentes: Dispositivos Móveis; Usabilidade, desenvolvimento web, mobile e jogos

Persistência com Flutter

Antes de vermos persistência, é importante ver (ou rever) um pouco sobre Data Access Object (DAO):

Para trabalhar com persistência no Flutter, vamos utilizar aqui o sqflite. É tipo o sqlite, mas para usar com o flutter.

Trabalhar com sqlite ou, no caso sqflite, é como se estivéssemos usando banco de dados, mas é em um arquivo, ou seja, não utilizamos um SGBD (Sistema Gerenciador de Banco de Dados) pois não seria conveniente para se trabalhar com um aplicativo instalado em um dispositivo móvel.

Outra forma de trabalhar com dados, é consumindo API (Serviço Web). A diferença é que nesse exemplo, os dados estão no dispositivo e não precisamos de conexão com a Internet para usá-lo. Quando consumimos uma API e os dados estão em um servidor, precisamos da internet para usar o aplicativo.

Vamos adicionar o pacote sqflite no projeto pra poder trabalhar com dados persistentes no dispositivo móvel:

$ flutter pub add sqflite

Também vamos adicionar o pacote path

$ flutter pub add path

Esses comandos adiciona o sqflite e o path como dependência no pubspec.yaml

dependencies:
 ...
 sqflite: ...
 path: ...

Caso queira atualizar um pacote, por exemplo o sqflite, digite o comando:

$ flutter pub upgrade sqflite


Tplnote Bulbgraph.png

A aplicação com sqflite só vai funcionar no dispositivo Android. Para Linux, windows e web dá pra usar o sqflite_common_ffi

contact.dart

/// # Classe Contact
/// Colocamos o nome da tabela e das colunas da tabela
/// como constantes, facilita na manutenção, pois caso
/// em algum momento a gente decida mudar o nome, muda
/// só aqui.
/// Também tempos um constructor e um get, que retorna
/// um Map com as informações do objeto.

class Contact {
  static const String tableName = 'contact';
  static const String columnNameName = 'name';
  static const String columnNamePhone = 'phone';

  final String name;
  final String phone;

  Contact({required this.name, required this.phone});

  Map<String, dynamic> toMap() {
    return {'name': name, 'phone': phone};
  }
}

contact_dao.dart

/// # sqflite
/// Como incluímos o sqflite, precisamos instalar ele no nosso
/// projeto. Para isso, temos que digitar dentro da pasta raíz
/// do projeto o seguinte comando:
/// $ flutter pub add sqflite
/// Caso queira atualizar a versão, então usamos o comando:
/// $ flutter pub upgrade sqflite
/// E isso serve para qualquer pacote que for necessário adicionar
/// ao projeto.
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'dart:async';
import 'contact.dart';

/// # Classe ContactDao
/// Gerencia o CRUD da tabela de contatos. O objeto da
/// base de dados é um *Future*.
///
/// Um **Future** é usado para representar um valor potencial,
/// ou erro, que estará disponível em algum momento no futuro.
/// Usando um **Future**, podemos tratar os erros mais ou menos
/// no mesmo esquema que *Try* e *Catch*.
class ContactDao {
  static const String databaseName = 'phonebook.db';
  late Future<Database> database;

  /// **Método connect**: É *async*, ou seja, é executado de forma
  /// assíncrona. Isso porque temos um **await** que vai conectar
  /// à base de dados e, caso ela não exista, cria a tabela. No caso
  /// aqui, só usamos o *onCreate*. Caso alguém tenha uma versão mais
  /// antiga da base instalada e vai fazer uma atualização do aplicativo,
  /// a versão em *verrsion* vai ser diferente, então usamos um método
  /// *onUpdate* para fazer as operações de migração da antiga base de
  ///  dados para a nova.
  Future connect() async {
    var databasesPath = await getDatabasesPath();
    String path = p.join(databasesPath, databaseName);
    database = openDatabase(
      path,
      version: 1,
      onCreate: (Database db, int version) {
        return db.execute("CREATE TABLE IF NOT EXISTS ${Contact.tableName} ( "
            "${Contact.columnNameName} TEXT PRIMARY KEY, "
            "${Contact.columnNamePhone} TEXT)");
      },
    );
  }

  /// **Método list**: recupera todos os registros da base de dados.
  /// Para isso, usamos o método *query*. Não colocamos todos os
  /// parâmetros, só passamos a tabela, então retorna tudo. Depois
  /// retornamos em uma lista (Map) de contatos.
  Future<List<Contact>> list() async {
    final Database db = await database;
    final List<Map<String, dynamic>> maps = await db.query(Contact.tableName);
    return List.generate(maps.length, (i) {
      return Contact(
        name: maps[i][Contact.columnNameName],
        phone: maps[i][Contact.columnNamePhone],
      );
    });
  }

  /// **Método insert**: insere um objeto contato como registro na nossa
  /// base de dados. Quando usamos no *conflictAlgorithm* o valor
  /// *ConflictAlgorithm.replace*, significa que não vai ter erro caso
  /// encontre um objeto com a mesma chave, mas apenas substitui o
  /// objeto existente pelo novo. É quase um update.
  Future<void> insert(Contact contact) async {
    final Database db = await database;
    await db.insert(
      Contact.tableName,
      contact.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  /// **Método update**: faz o update na base de dados. Não uso nesse
  /// exemplo, mas deixo aqui como forma instrutiva.
  Future<void> update(Contact contact) async {
    final db = await database;
    await db.update(
      Contact.tableName,
      contact.toMap(),
      where: "${Contact.columnNameName} = ?",
      whereArgs: [contact.name],
    );
  }

  /// **Método delete**: exclui um registro da base de dados conforme a
  /// String *name* passada como parâmetro. Lembrando que *name* na nossa
  /// tabela de contatos é chave primária.
  /// Veja que em *where* colocamos a condição indicando quando que a
  /// exclusão vai ser feita e em *whereArgs* é o valor que será substituído
  /// pelo curinga (?) do *where*. Dá para ter vários curingas e colocamos
  /// todos eles no vetor onde está o *name*. Nesse caso, foi só um parâmetro.
  Future<void> delete(String name) async {
    final db = await database;
    await db.delete(
      Contact.tableName,
      where: "${Contact.columnNameName} = ?",
      whereArgs: [name],
    );
  }
}

main.dart

 1import 'package:flutter/material.dart';
 2import 'phonebook.dart';
 3
 4void main() => runApp(const App());
 5
 6class App extends StatelessWidget {
 7  const App({super.key});
 8
 9  @override
10  build(context) {
11    return const MaterialApp(
12      title: 'Contatos',
13      home: PhoneBook(),
14    );
15  }
16}

phonebook.dart

  1import 'package:flutter/material.dart';
  2import 'contact_dao.dart';
  3import 'contact.dart';
  4
  5/// # Classe PhoneBook
  6/// Como vamos precisar de uma página que altera o *state*, Então
  7/// não dá para ser *StatelessWidget*. Daí usamos um *StatefulWidget*
  8/// que chama uma classe que herda de State do tipo *PhoneBook*.
  9class PhoneBook extends StatefulWidget {
 10  const PhoneBook({super.key});
 11
 12  @override
 13  State<PhoneBook> createState() => _PhoneBookState();
 14}
 15
 16class _PhoneBookState extends State<PhoneBook> {
 17  /// Instancia o *Data Access Object* (DAO) dos contatos
 18  final ContactDao dao = ContactDao();
 19
 20  /// lista de contatos
 21  List<Contact> contacts = [];
 22
 23  /// Controladores dos TextFields
 24  TextEditingController name = TextEditingController();
 25  TextEditingController phone = TextEditingController();
 26
 27  /// **Constructor** - conecta o banco de dados. É uma função assincrona,
 28  /// então segue o programa. Só quando o banco for conectado que
 29  /// entra e executa o método load()
 30  _PhoneBookState() {
 31    dao.connect().then((value) {
 32      load();
 33    });
 34  }
 35
 36  /// **Método load**: chama o método list do DAO. Quando terminada a
 37  /// operação, atualiza o *state* da lista de contatos. Quando um *state*
 38  /// é atualizado, é feita uma nova renderização da tela. No caso, a
 39  /// nossa ListView será atualizada com os elementos lidos da base de
 40  /// dados. Aqui, já aproveito para apagar os campos de texto.
 41  load() {
 42    dao.list().then((value) {
 43      setState(() {
 44        contacts = value;
 45        name.text = "";
 46        phone.text = "";
 47      });
 48    });
 49  }
 50
 51  /// **Método build**: gera a nossa tela. Ela contém dois campos texto,
 52  /// três botões (gravar, excluir e limpar campos) e uma ListView.
 53  /// Por questão de conveniência e para o código ficar mais limpo,
 54  /// coloquei os códigos de cada um dos elementos dentro de métodos, mas
 55  /// não é obrigatório e vai da metodologia de desenvolvimento que você
 56  /// melhor se adequar.
 57  @override
 58  Widget build(BuildContext context) {
 59    return Scaffold(
 60      appBar: AppBar(
 61        title: const Text("Agenda"),
 62      ),
 63      body: Column(
 64        children: [
 65          fieldName(),
 66          fieldPhone(),
 67          Row(
 68            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 69            children: [
 70              buttonSave(),
 71              buttonDel(),
 72              buttonClear(),
 73            ],
 74          ),
 75          listView(),
 76        ],
 77      ),
 78    );
 79  }
 80
 81  /// **Método fieldName**: Campo de texto do nome. Veja que usamos o
 82  /// atributo da classe chamado **name** no *controller*.
 83  fieldName() {
 84    return TextField(
 85      decoration: const InputDecoration(
 86        labelText: 'Nome',
 87      ),
 88      controller: name,
 89    );
 90  }
 91
 92  /// **Método fieldPhone**: Campo de texto para o telefone
 93  fieldPhone() {
 94    return TextField(
 95      decoration: const InputDecoration(
 96        labelText: 'Telefone',
 97      ),
 98      controller: phone,
 99    );
100  }
101
102  /// **Método buttonSave**: quando clicado, verifica se a entrada não
103  /// está vazia. Pega o conteúdo dos campos de texto, criando um objeto
104  /// do tipo *Contact* e pede para o DAO inserir no banco de dados.
105  /// Feito isso, faz um reload para atualizar a ListView.
106  buttonSave() {
107    return ElevatedButton(
108      child: const Text('Gravar'),
109      onPressed: () {
110        if (name.text.trim() != '') {
111          var c = Contact(
112            name: name.text,
113            phone: phone.text,
114          );
115          dao.insert(c).then((value) {
116            load();
117          });
118        }
119      },
120    );
121  }
122
123  /// **Método buttonDel**: Veja que colocamos um estilo nele para
124  /// diferenciar do botão Gravar. Colocamos ele com cor de fundo
125  /// vermelha e texto em branco. Quando clicado *onPressed* é
126  /// chamado e pede para o dao excluir o registro que possui o
127  /// nome que está no campo texto do nome.
128  buttonDel() {
129    return ElevatedButton(
130      style: ElevatedButton.styleFrom(
131        foregroundColor: Colors.white,
132        backgroundColor: Colors.red, // foreground
133      ),
134      child: const Text('Excluir'),
135      onPressed: () {
136        dao.delete(name.text.trim()).then((value) {
137          load();
138        });
139      },
140    );
141  }
142
143  /// **Método buttonClear**: Também mudamos a cor, verde de fundo e o
144  /// texto em branco. Quando clicado, apenas apaga os campos de texto.
145  /// Veja que usamos o setState pra fazer um novo render da página.
146  buttonClear() {
147    return ElevatedButton(
148      style: ElevatedButton.styleFrom(
149        foregroundColor: Colors.green, // background
150        backgroundColor: Colors.white, // foreground
151      ),
152      child: const Text('Limpar'),
153      onPressed: () {
154        setState(() {
155          name.text = "";
156          phone.text = "";
157        });
158      },
159    );
160  }
161
162  /// **Método listView**: aqui retornamos a listView que mostra o nome e o
163  /// telefone dos registros armazenados na base de dados. Colocamos ela
164  /// dentroo de um Expanded para poder rolar na tela. O Título contém o
165  /// nome e coloquei um estilo, e o subtítulo é o telefone. Quando clicado
166  /// um elemento, carrego os dados daquele elemento nos campos texto para
167  /// poder alterá-los ou apagá-lo.
168  listView() {
169    return Expanded(
170      child: ListView.builder(
171        shrinkWrap: true,
172        itemCount: contacts.length,
173        itemBuilder: (context, index) {
174          return ListTile(
175            title: Text(
176              contacts[index].name,
177              style: const TextStyle(
178                fontSize: 20.0,
179                color: Colors.black,
180              ),
181            ),
182            subtitle: Text(contacts[index].phone),
183            onTap: () {
184              setState(() {
185                name.text = contacts[index].name;
186                phone.text = contacts[index].phone;
187              });
188            },
189          );
190        },
191      ),
192    );
193  }
194}

Atividades

Desafio 1

Implemente no seu computador o exemplo passado na aula e faça-o funcionar. Faça alterações que achar conveniente (individual).

Arrumar a aplicação para poder alterar o nome também.

Desafio 2

Implemente um aplicativo para dispositivo móvel com as seguintes características (individual ou em grupo de até 4 integrantes):

  • Persistência de dados (CRUD)
  • Pelo menos duas janelas
  • Uso de Listview