Flutter - Persistência
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
|
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