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.
8class Contact {
9 static const String tableName = 'contact';
10 static const String columnNameName = 'name';
11 static const String columnNamePhone = 'phone';
12
13 final String name;
14 final String phone;
15
16 Contact({required this.name, required this.phone});
17
18 Map<String, dynamic> toMap() {
19 return {'name': name, 'phone': phone};
20 }
21}
contactDao.dart
Adicionar o sqflite no projeto:
$ flutter pub add sqflite
Atualização da versão do sqflite:
$ flutter pub upgrade sqflite
Adicionar o sqflite como dependência no pubspec.yaml
dependencies: ... sqflite:
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';
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 DATABASE_NAME = 'phonebook.db';
25 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 connect() async {
36 this.database = openDatabase(
37 join(await getDatabasesPath(), DATABASE_NAME),
38 onCreate: (db, version) {
39 return db.execute("CREATE TABLE IF NOT EXISTS " +
40 "${Contact.TABLE_NAME} ( " +
41 "${Contact.COLUMN_NAME_NAME} TEXT PRIMARY KEY, " +
42 "${Contact.COLUMN_NAME_PHONE} TEXT)");
43 },
44 version: 1,
45 );
46 }
47
48 /// **Método list**: recupera todos os registros da base de dados.
49 /// Para isso, usamos o método *query*. Não colocamos todos os
50 /// parâmetros, só passamos a tabela, então retorna tudo. Depois
51 /// retornamos em uma lista (Map) de contatos.
52 Future<List<Contact>> list() async {
53 final Database db = await database;
54 final List<Map<String, dynamic>> maps = await db.query(Contact.TABLE_NAME);
55 return List.generate(maps.length, (i) {
56 return Contact(
57 name: maps[i][Contact.COLUMN_NAME_NAME],
58 phone: maps[i][Contact.COLUMN_NAME_PHONE],
59 );
60 });
61 }
62
63 /// **Método insert**: insere um objeto contato como registro na nossa
64 /// base de dados. Quando usamos no *conflictAlgorithm* o valor
65 /// *ConflictAlgorithm.replace*, significa que não vai ter erro caso
66 /// encontre um objeto com a mesma chave, mas apenas substitui o
67 /// objeto existente pelo novo. É quase um update.
68 Future<void> insert(Contact contact) async {
69 final Database db = await database;
70 await db.insert(
71 Contact.TABLE_NAME,
72 contact.toMap(),
73 conflictAlgorithm: ConflictAlgorithm.replace,
74 );
75 }
76
77 /// **Método update**: faz o update na base de dados. Não uso nesse
78 /// exemplo, mas deixo aqui como forma instrutiva.
79 Future<void> update(Contact contact) async {
80 final db = await database;
81 await db.update(
82 Contact.TABLE_NAME,
83 contact.toMap(),
84 where: "${Contact.COLUMN_NAME_NAME} = ?",
85 whereArgs: [contact.name],
86 );
87 }
88
89 /// **Método delete**: exclui um registro da base de dados conforme a
90 /// String *name* passada como parâmetro. Lembrando que *name* na nossa
91 /// tabela de contatos é chave primária.
92 /// Veja que em *where* colocamos a condição indicando quando que a
93 /// exclusão vai ser feita e em *whereArgs* é o valor que será substituído
94 /// pelo curinga (?) do *where*. Dá para ter vários curingas e colocamos
95 /// todos eles no vetor onde está o *name*. Nesse caso, foi só um parâmetro.
96 Future<void> delete(String name) async {
97 final db = await database;
98 await db.delete(
99 Contact.TABLE_NAME,
100 where: "${Contact.COLUMN_NAME_NAME} = ?",
101 whereArgs: [name],
102 );
103 }
104}
main.dart
1import 'package:flutter/material.dart';
2import 'phonebook.dart';
3
4void main() => runApp(App());
5
6class App extends StatelessWidget {
7 @override
8 build(context) {
9 return MaterialApp(
10 title: 'Contatos',
11 home: PhoneBook(),
12 );
13 }
14}
phonebook.dart
1import 'package:flutter/material.dart';
2import 'contactDao.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 @override
11 _PhoneBookState createState() => _PhoneBookState();
12}
13
14class _PhoneBookState extends State<PhoneBook> {
15 /// Instancia o *Data Access Object* (DAO) dos contatos
16 final ContactDao dao = ContactDao();
17
18 /// lista de contatos
19 List<Contact> contacts = [];
20
21 /// Controladores dos TextFields
22 TextEditingController name = TextEditingController();
23 TextEditingController phone = TextEditingController();
24
25 /// **Constructor** - conecta o banco de dados. É uma função assincrona,
26 /// então segue o programa. Só quando o banco for conectado que
27 /// entra e executa o método load()
28 _PhoneBookState() {
29 dao.connect().then((value) {
30 load();
31 });
32 }
33
34 /// **Método load**: chama o método list do DAO. Quando terminada a
35 /// operação, atualiza o *state* da lista de contatos. Quando um *state*
36 /// é atualizado, é feita uma nova renderização da tela. No caso, a
37 /// nossa ListView será atualizada com os elementos lidos da base de
38 /// dados. Aqui, já aproveito para apagar os campos de texto.
39 load() {
40 dao.list().then((value) {
41 setState(() {
42 contacts = value;
43 name.text = "";
44 phone.text = "";
45 });
46 });
47 }
48
49 /// **Método build**: gera a nossa tela. Ela contém dois campos texto,
50 /// três botões (gravar, excluir e limpar campos) e uma ListView.
51 /// Por questão de conveniência e para o código ficar mais limpo,
52 /// coloquei os códigos de cada um dos elementos dentro de métodos, mas
53 /// não é obrigatório e vai da metodologia de desenvolvimento que você
54 /// melhor se adequar.
55 @override
56 Widget build(BuildContext context) {
57 return Scaffold(
58 appBar: AppBar(
59 title: Text("Agenda"),
60 ),
61 body: Container(
62 child: Column(
63 children: [
64 fieldName(),
65 fieldPhone(),
66 Row(
67 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
68 children: [
69 buttonSave(),
70 buttonDel(),
71 buttonClear(),
72 ],
73 ),
74 listView(),
75 ],
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: 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: 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: Text('Gravar'),
109 onPressed: () {
110 if (name.text.trim() != '') {
111 var c = new 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 primary: Colors.red, // background
132 onPrimary: Colors.white, // foreground
133 ),
134 child: 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 primary: Colors.green, // background
150 onPrimary: Colors.white, // foreground
151 ),
152 child: 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: 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