Flutter - Persistência
Afluentes: Dispositivos Móveis; Usabilidade, desenvolvimento web, mobile e jogos
Persistência com Flutter
Estamos trabalhando com persistência no Flutter nesse exemplo com o sqflite. É tipo o sqlite, mas com o flutter. É uma forma de trabalhar como se estivéssemos usando banco de dados, mas é em um arquivo, e não em um SGBD (Sistema Gerenciador de Banco de Dados), que não seria conveniente 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.
Antes, vamos ver um pouco sobre Data Access Object:
A explicação dos códigos está como comentário nos códigos abaixo.
contact.dart
1/// # Classe Contact
2/// Colocamos o nome da tabela e das colunas da tabela
3/// como constantes, facilita na manutenção, pois caso
4/// em algum momento a gente decida mudar o nome, muda
5/// só aqui.
6/// Também tempos um constructor e um get, que retorna
7/// um Map com as informações do objeto.
8
9class Contact {
10 static const String tableName = 'contact';
11 static const String columnNameName = 'name';
12 static const String columnNamePhone = 'phone';
13
14 final String name;
15 final String phone;
16
17 Contact({required this.name, required this.phone});
18
19 Map<String, dynamic> toMap() {
20 return {'name': name, 'phone': phone};
21 }
22}
contactDao.dart
1/// # sqflite
2/// Como incluímos o sqflite, precisamos instalar ele no nosso
3/// projeto. Para isso, temos que digitar dentro da pasta raíz
4/// do projeto o seguinte comando:
5/// $ flutter pub add sqflite
6/// Caso queira atualizar a versão, então usamos o comando:
7/// $ flutter pub upgrade sqflite
8/// E isso serve para qualquer pacote que for necessário adicionar
9/// ao projeto.
10import 'package:sqflite/sqflite.dart';
11import 'package:path/path.dart' as p;
12import 'dart:async';
13import 'contact.dart';
14
15/// # Classe ContactDao
16/// Gerencia o CRUD da tabela de contatos. O objeto da
17/// base de dados é um *Future*.
18///
19/// Um **Future** é usado para representar um valor potencial,
20/// ou erro, que estará disponível em algum momento no futuro.
21/// Usando um **Future**, podemos tratar os erros mais ou menos
22/// no mesmo esquema que *Try* e *Catch*.
23class ContactDao {
24 static const String databaseName = 'phonebook.db';
25 late Future<Database> database;
26
27 /// **Método connect**: É *async*, ou seja, é executado de forma
28 /// assíncrona. Isso porque temos um **await** que vai conectar
29 /// à base de dados e, caso ela não exista, cria a tabela. No caso
30 /// aqui, só usamos o *onCreate*. Caso alguém tenha uma versão mais
31 /// antiga da base instalada e vai fazer uma atualização do aplicativo,
32 /// a versão em *verrsion* vai ser diferente, então usamos um método
33 /// *onUpdate* para fazer as operações de migração da antiga base de
34 /// dados para a nova.
35 Future connect() async {
36 var databasesPath = await getDatabasesPath();
37 String path = p.join(databasesPath, databaseName);
38 database = openDatabase(
39 path,
40 version: 1,
41 onCreate: (Database db, int version) {
42 return db.execute("CREATE TABLE IF NOT EXISTS ${Contact.tableName} ( "
43 "${Contact.columnNameName} TEXT PRIMARY KEY, "
44 "${Contact.columnNamePhone} TEXT)");
45 },
46 );
47 }
48
49 /// **Método list**: recupera todos os registros da base de dados.
50 /// Para isso, usamos o método *query*. Não colocamos todos os
51 /// parâmetros, só passamos a tabela, então retorna tudo. Depois
52 /// retornamos em uma lista (Map) de contatos.
53 Future<List<Contact>> list() async {
54 final Database db = await database;
55 final List<Map<String, dynamic>> maps = await db.query(Contact.tableName);
56 return List.generate(maps.length, (i) {
57 return Contact(
58 name: maps[i][Contact.columnNameName],
59 phone: maps[i][Contact.columnNamePhone],
60 );
61 });
62 }
63
64 /// **Método insert**: insere um objeto contato como registro na nossa
65 /// base de dados. Quando usamos no *conflictAlgorithm* o valor
66 /// *ConflictAlgorithm.replace*, significa que não vai ter erro caso
67 /// encontre um objeto com a mesma chave, mas apenas substitui o
68 /// objeto existente pelo novo. É quase um update.
69 Future<void> insert(Contact contact) async {
70 final Database db = await database;
71 await db.insert(
72 Contact.tableName,
73 contact.toMap(),
74 conflictAlgorithm: ConflictAlgorithm.replace,
75 );
76 }
77
78 /// **Método update**: faz o update na base de dados. Não uso nesse
79 /// exemplo, mas deixo aqui como forma instrutiva.
80 Future<void> update(Contact contact) async {
81 final db = await database;
82 await db.update(
83 Contact.tableName,
84 contact.toMap(),
85 where: "${Contact.columnNameName} = ?",
86 whereArgs: [contact.name],
87 );
88 }
89
90 /// **Método delete**: exclui um registro da base de dados conforme a
91 /// String *name* passada como parâmetro. Lembrando que *name* na nossa
92 /// tabela de contatos é chave primária.
93 /// Veja que em *where* colocamos a condição indicando quando que a
94 /// exclusão vai ser feita e em *whereArgs* é o valor que será substituído
95 /// pelo curinga (?) do *where*. Dá para ter vários curingas e colocamos
96 /// todos eles no vetor onde está o *name*. Nesse caso, foi só um parâmetro.
97 Future<void> delete(String name) async {
98 final db = await database;
99 await db.delete(
100 Contact.tableName,
101 where: "${Contact.columnNameName} = ?",
102 whereArgs: [name],
103 );
104 }
105}
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