Flutter - Persistência

De Aulas
Revisão de 16h25min de 7 de outubro de 2022 por Admin (discussão | contribs) (→‎contactDao.dart)

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