Utilizando SQFlite para criar e manipular um banco de dados com Flutter
Detalhamentos sobre a implementação de banco de dado relacional utilizando o plugin SQFlite para persistência dos dados na aplicação.
Dependências e projeto da aplicação
Pra facilitar o entendimento disponibilizei o código ao qual vou me referenciar através deste link do Github Gist.
Abaixo temos as seguintes depêndencias de plugins utilizadas para o nosso objetivo
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
Definindo as colunas que vão compor o db com variáveis
final String contactTable = "contactTable";
final String idCol = "idCol";
final String nameCol = "nameCol";
final String emailCol = "emailCol";
final String phoneCol = "phoneCol";
final String imgCol = "imgCol";
Aqui é onde começa a primeira definição sobre as colunas que vão compor o banco, cada variável tem o mesmo nome e valor, respectivos a coluna (o nome e valor das variáveis podem ser alterados). Lembre-se neste caso, cada variável representa uma String com o seu mesmo nome como valor, por quê o que o banco vai utilizar para compor o nome das colunas é seu respectivo valor.
Aqui temos uma variável para armazenar apenas o nome da tabela do banco.
Iniciando a classe ContactHelper {}
ContactHelper {}
Utilizando o Sigleton Pattern
ContactHelper.internal();
static final ContactHelper _instance = ContactHelper.internal();
factory ContactHelper() => _instance;
Para um melhor entendimento, mudei a ordem das linhas quanto ao arquivo original.
Aqui temos algo mais interessante, se você já leu algum livro sobre Design Patterns deve ter visto algo sobre o Singleton Pattern, onde implementa um padrão bastante conhecido, onde só haverá uma única instância da classe, mesmo se você tentar isso:
db_one = new ContactHelper();
db_two = new Contact Helper();
As duas linhas de comando acima estarão referenciando a mesma classe, o mesmo banco de dados existirá, e nenhuma outra instância será criada.
Na primeira linha um construtor privado é criado, então uma váriavel chamada _instance
do tipo da classe ContactHelper
recebe o construtor privado, e então um factory
da classe ContactHelper executa o bloco de código no qual chama _instance
que representa o mesmo conteúdo da classe antes instanciada a primeira vez, por isso sempre é referenciada a mesma instância do objeto.
Outras formas de implementar o padrão singleton em Dart.
Criando o banco de dados
Para criar o banco de dados precisamos vamos guardar um tipo Database
que pertence ao plugin sqflite em uma variável privada, chamada _db
.
A partir daqui devemos saber que maior parte senão todas as funções do plugin SQFlite executam de forma assíncrona, não vou entrar a fundo em funções assíncronas, isso é assunto para outra parte dessa wiki. Enfim vamos prosseguir com o código.
Future<Database> get db async {
// Now the method to know if the database isn't null
if (_db != null) {
return _db; // If isn't null returns a _db registered on Database type
} else {
_db = await initDb(); // If null (not created), initiate a new database
/* The initDb() is a asynchronous function, so have to be a Future object
* to return */
return _db;
}
}
Acima declaramos uma condicional para testar se o banco de dados já foi criado anteriormente, como consequência, se houver sido criado então irá retornar o _db
(Database)`já existente, caso contrário irá executar a função initDb
função no qual inicializará um novo _db
(Database), nos dois finais o retorno será o _db
, sendo o já existente ou o novo.
Inicializando um novo banco de dados
Aqui vai mais uma função assíncrona que irá retornar um tipo Database
futuro.
Future<Database> initDb() async {
final databasesPath = await getDatabasesPath();
final path = join("databasesPath", "contacts.db");
}
Aqui temos a função raiz do nosso banco de dados, será aqui que o banco vai ser criado a primeira vez, primeiro declaramos o caminho do diretório no qual o banco de dados vai ser armazenado, pra isso é utilizado uma funcionalidade da biblioteca path do Dart, e atribuímos esse caminho a uma variável:
final databasesPath = await getDatabasesPath();
Vale lembrar que esses processos que demandam um carregamento para obter o caminho de diretórios do aparelho, então essas funções são assíncronas, o await
então espera que o getDatabasesPath()
finalize para que atríbua o caminho a variável.
final path = join("databasesPath", "contacts.db");
Aqui juntamos o endereço do diretório com o arquivo contacts.db
e atribuímos a essa variável.
return await openDatabase(path, version: 1,
onCreate: (Database db, int newerVersion) async {
// The SQL command to initiate
await db.execute(
"CREATE TABLE $contactTable($idCol INTEGER PRIMARY KEY, $nameCol TEXT, $emailCol TEXT, $phoneCol TEXT, $imgCol TEXT)");
});
Agora que já temos o diretório com o arquivo, podemos manipula-lo com os comandos SQL, e como retorno da função, vamos abrir o banco de dados, utilizamos a função openDatabase
(sqflite) passando o arquivo do banco juntamente com o caminho(path
) e a versão do banco(parâmetro requerido por openDatabase) e então mais um parâmetro especificando qual comando será executado no onCreate
.
Para executar a inicialização do novo banco de dados, referenciamos o db.execute
e como parâmetro o comando SQL:
"CREATE TABLE $contactTable($idCol INTEGER PRIMARY KEY, $nameCol TEXT, $emailCol TEXT, $phoneCol TEXT, $imgCol TEXT)");
E isso tudo retorna como para dentro da variável _db
declarada no escopo if
e else
para verificação da existência do banco.
O que vai ser armazenado?
Agora que já temos as colunas do banco devidamente prontas, devemos ter as funcionalidades de manipulação do banco.
A classe Contact
Você deve estar se perguntando o que exatamente iremos salvar no banco de dados? Pra deixar mais claro no final do arquivo temos a declaração da classe contato, com todas as informações que um contato tem. É importante saber que é aqui onde vai ser criado o objeto que será armazenado no banco de dados.
class Contact {
int id;
String name;
String email;
String phone;
String img;
// Recovering the data from map database to a normal object
// The name of method "fromMap" is created by user
// So this is a simple function that's inside contact and receive a Map as parameter
// Empty constructor
Contact();
Contact.fromMap(Map map) {
// Putting the columns variables inside class variables
id = map[idCol];
name = map[nameCol];
email = map[emailCol];
phone = map[phoneCol];
img = map[imgCol];
}
// Putting the object values inside a map
Map toMap() {
Map<String, dynamic> map = {
nameCol: name,
emailCol: email,
phoneCol: phone,
imgCol: img,
};
// When the object is created, it's don't have an id
// So the database have to attribute the id
// If the object isn't null, the id variable is set to idCol
if (id != null) {
map[idCol] = id;
}
// In the end of all, this function returns a map
return map;
}
// Overriding toString method to show better the information
@override
String toString() {
return "Contact (id: $id, name: $name, email: $email, phone: $phone, img: $img)";
}
}
Declaração dos dados do contato
int id;
String name;
String email;
String phone;
String img;
Nesta declaração de atributos da classe contato, devemos declarar as variáveis que vão compor as colunas criadas anteriormente no banco de dados.
Transformando o objeto em um map
Map toMap() {
Map<String, dynamic> map = {
nameCol: name,
emailCol: email,
phoneCol: phone,
imgCol: img,
};
// When the object is created, it's don't have an id
// So the database have to attribute the id
// If the object isn't null, the id variable is set to idCol
if (id != null) {
map[idCol] = id;
}
// In the end of all, this function returns a map
return map;
}
Para o funcionamento do plugin SQFlite qualquer objeto que for adentrar o banco de dados deve ser transformado em um mapa primeiro, faz sentido, pois em um mapa em Dart cada valor tem uma chave, que neste caso irá representar a coluna do banco de dados.
Na linha 2 especificamos os tipos que vão compor o map e declaramos uma variável chamada map
e dentro dela declaramos as chaves com o nome das colunas que declaramos no banco de dados e os valores das chaves serão os valores das variáveis do contato.
Na linha 13 criaremos um id para o objeto que está sendo transformado em map.
No final é retornado o map em questão, com suas respectivas chaves.
Recuperando o map para objeto
Contact.fromMap(Map map) {
// Putting the columns variables inside class variables
id = map[idCol];
name = map[nameCol];
email = map[emailCol];
phone = map[phoneCol];
img = map[imgCol];
}
Para exibirmos o contato na aplicação, devemos transformar o map de volta em objeto, então os respectivos valores do map são atribuídos novamente ás variáveis.
Fazendo funcionar!
Agora que já temos o banco de dados e os dados que vai o compor, já podemos criar as funcionalidades que irão manipular essas duas coisas. Devemos lembrar que todas as funcionalidades aqui são assíncronas, devido a requisição em diretórios do aparelho não serem instantâneas as funcionalidades e retornos necesitam de um carregamento para se efetivarem.
Salvando o contato no banco
Future<Contact> saveContact(Contact contact) async {
Database dbContact = await db;
// Here the method toMap catch the attributes of table and pass toMap
contact.id = await dbContact.insert(contactTable, contact.toMap());
return contact;
}
O primeiro método que implementamos é o de salvar um contato no banco de dados, recuperamos o banco de dados para uma variável local dbContact
e então atríbuimos a ela o seguinte:
dbContact.insert(contactTable, contact.toMap());
O método insert vem da API do SQFlite, o que obedece a seguinte regra:
Future<int> insert(String table, Map<String, dynamic> values,
{String nullColumnHack, ConflictAlgorithm conflictAlgorithm});
Como podem ver essa função retorna um inteiro que é atribuído como id ao contato em questão. E então retorna o contato.
Deletando o contato
Future<int> deleteContact(int id) async {
Database dbContact = await db;
return await dbContact
.delete(contactTable, where: "$idCol = ?", whereArgs: [id]);
}
Para deletar o código não muda muito, só o fato do uso da função delete
do SQFlite que recebe a tabela em questão e a condição dizendo que a coluna tem de ser igual ao id do db.
Update do contato
Future<int> updateContact(Contact contact) async {
Database dbContact = await db;
return await dbContact.update(contactTable, contact.toMap(),
where: "$idCol = ?", whereArgs: [contact.id]);
}
Mais um comando do SQFlite update
passando a tabela, o map e impondo a condição de que os id's devem ser iguais.
Future<List> getAllContacts() async {
Database dbContact = await db;
List listMap = await dbContact.rawQuery("SELECT * FROM $contactTable");
List<Contact> listContact = List();
for (Map m in listMap) {
listContact.add(Contact.fromMap(m));
}
return listContact;
}
Future<int> getNumber() async {
Database dbContact = await db;
return Sqflite.firstIntValue(
await dbContact.rawQuery("SELECT COUNT(*) FROM $contactTable"));
}
Acima mais algumas funções explicítando como pegar todos os contatos do banco e como pegar uma informação específica.
Lembre-se que toda vez que instânciamos o db, ele faz todo aquele processo de verificação se o banco já existe e o retorna.
Fechando o banco de dados
Future close() async {
Database dbContact = await db;
dbContact.close();
}
Esta funcionalidade não foi utilizada no projeto.
Utilizando o banco de dados
Agora com toda a nossa classe do banco de dados podemos utiliza-lo em nossa aplicação para inserção e remoção dos dados, para exemplificar o uso, deixei minha interface do aplicativo feita em outro arquivo, toda a aplicação pode ser encontrada aqui.
ContactHelper helper = ContactHelper();
// Declaring the list of contacts, it's empty
List<Contact> contacts = List();
@override
void initState() {
super.initState();
_getAllContacts();
}
A primeira coisa a se fazer é adicionar uma instância do banco no seu StatefulWidget
, então poderá usar os códigos em qualquer lugar, em seguida vamos sobrescrever o método initState()
que por padrão inicializa a classe, então utilizamos o _getAllContacts
para conseguir todos os contatos do banco.
Refenciando os itens do banco
Text(
contacts[index].name ?? "",
style: TextStyle(
fontSize: 22.0, fontWeight: FontWeight.bold),
),
Text(
contacts[index].email ?? "",
style: TextStyle(fontSize: 18.0),
),
Text(
contacts[index].phone ?? "",
style: TextStyle(fontSize: 18.0),
),
Acimas temos uma estrutura utilizada em um widget avulso chamado _contactCard, no qual pega um index
do contacts referindo seu atributo após o .
e apontando se há uma condição de ""
(vazio) nesses atributos.
Lista dos contatos
ListView.builder(
padding: EdgeInsets.all(10.0),
itemCount: contacts.length,
itemBuilder: (context, index) {
return _contactCard(context, index);
},
),
Na nossa lista de itens, o widget no qual pega as informações é referenciado no retorno do widget de lista passando o context
e o index
.
Last updated
Was this helpful?