Mudanças entre as edições de "Flutter - Persistência"

De Aulas
Linha 2: Linha 2:
  
 
= Persistência com Flutter =
 
= Persistência com Flutter =
 +
 +
{{tip|A aplicação com sqflite só vai funcionar no dispositivo Android. Para Linux, windows e web dá pra usar o sqflite_common_ffi}}
  
 
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.
 
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.

Edição das 17h15min de 10 de outubro de 2022

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

Persistência com Flutter

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

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

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' 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