Trabalhando com dados
Quando a aplicação começa a crescer, não fica legal trabalhar com dados soltos o tempo todo. Em vez de passar nome, ip e status separados para todo lado, faz mais sentido representar esses dados com uma classe.
Por exemplo, um servidor pode ser representado assim:
class Servidor {
final int? id;
final String nome;
final String ip;
final String status;
Servidor({
this.id,
required this.nome,
required this.ip,
required this.status,
});
}
Essa classe representa uma linha da tabela servidores. O id está como int?, ou seja, pode ser int ou null, porque antes de salvar o servidor no banco ele ainda não tem id. Quem gera esse id é o SQLite, por causa do AUTOINCREMENT.
A tabela no banco seria algo assim:
CREATE TABLE servidores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
ip TEXT NOT NULL,
status TEXT NOT NULL
);
A classe Servidor representa essa estrutura no Dart:
| Banco SQLite | Dart |
|---|---|
id INTEGER | int? id |
nome TEXT | String nome |
ip TEXT | String ip |
status TEXT | String status |
O problema é que o banco não salva diretamente um objeto Dart. O banco entende valores organizados por colunas. Por isso é comum transformar o objeto em um Map, onde cada chave representa o nome de uma coluna e cada valor representa o dado que será salvo, por exemplo:
Map<String, Object?> toMap() {
return {
'id': id,
'nome': nome,
'ip': ip,
'status': status,
};
}
Esse método transforma o objeto em um Map, ou seja, se existir este objeto:
final servidor = Servidor(
nome: 'web01',
ip: '192.168.0.10',
status: 'ativo',
);
O método toMap() gera algo parecido com isto:
{
'id': null,
'nome': 'web01',
'ip': '192.168.0.10',
'status': 'ativo',
}
Isso é útil porque o Map fica muito próximo do formato da tabela. A chave 'nome' aponta para a coluna nome, a chave 'ip' aponta para a coluna ip e a chave 'status' aponta para a coluna status. A classe completa ficaria assim:
class Servidor {
final int? id;
final String nome;
final String ip;
final String status;
Servidor({
this.id,
required this.nome,
required this.ip,
required this.status,
});
Map<String, Object?> toMap() {
return {
'id': id,
'nome': nome,
'ip': ip,
'status': status,
};
}
}
Usando o Map para salvar no banco
Com o pacote sqlite3, o Map não é enviado direto para o INSERT, como acontece em alguns pacotes de Flutter, mas ele ainda ajuda a organizar os dados.
void inserirServidor(Database db, Servidor servidor) {
final dados = servidor.toMap();
final insert = db.prepare('''
INSERT INTO servidores (nome, ip, status)
VALUES (?, ?, ?);
''');
insert.execute([
dados['nome'],
dados['ip'],
dados['status'],
]);
insert.dispose();
}
Aqui o objeto Servidor é convertido para Map antes de ser salvo. A linha abaixo pega os dados do objeto e transforma em Map.
final dados = servidor.toMap();
Pega o valor que será salvo na coluna nome.
dados['nome']
Pega o valor que será salvo na coluna ip.
dados['ip']
Pega o valor que será salvo na coluna status.
dados['status']
O id não foi usado no INSERT, porque ele será criado automaticamente pelo SQLite. No Flutter com sqflite, esse Map fica ainda mais direto, porque o pacote aceita algo assim:
await db.insert('servidores', servidor.toMap());
Nesse caso, o toMap() encaixa muito bem, porque o sqflite trabalha naturalmente com Map<String, Object?> para inserir registros.
Convertendo Map para objeto
Quando o banco retorna uma consulta, ele não devolve diretamente um objeto Servidor. Ele devolve uma linha com valores, algo parecido com um Map, onde cada coluna tem um valor.
Uma linha retornada pelo banco pode ser entendida assim:
{
'id': 1,
'nome': 'web01',
'ip': '192.168.0.10',
'status': 'ativo',
}
Para trabalhar melhor dentro da aplicação, é comum transformar esse Map de volta em um objeto Dart. Para isso, podemos criar um construtor chamado fromMap.
factory Servidor.fromMap(Map<String, Object?> map) {
return Servidor(
id: map['id'] as int,
nome: map['nome'] as String,
ip: map['ip'] as String,
status: map['status'] as String,
);
}
A classe completa agora fica assim:
class Servidor {
final int? id;
final String nome;
final String ip;
final String status;
Servidor({
this.id,
required this.nome,
required this.ip,
required this.status,
});
Map<String, Object?> toMap() {
return {
'id': id,
'nome': nome,
'ip': ip,
'status': status,
};
}
factory Servidor.fromMap(Map<String, Object?> map) {
return Servidor(
id: map['id'] as int,
nome: map['nome'] as String,
ip: map['ip'] as String,
status: map['status'] as String,
);
}
}
O toMap() faz parte da classe Servidor, porque ele é responsável por transformar aquele objeto específico em um formato que o banco entende melhor.
Explicando a parte nova:
factory Servidor.fromMap(Map<String, Object?> map) {
A linha acima cria um construtor nomeado chamado fromMap. Ele é usado para criar um objeto Servidor a partir de um Map. O factory é usado quando o construtor não precisa criar o objeto diretamente do jeito padrão. Nesse caso, ele recebe um Map, pega os valores e retorna uma instância de Servidor.
Retorna um novo objeto da classe Servidor.
return Servidor(
Pega o valor da chave id e converte para int.
id: map['id'] as int,
Pega o valor da chave nome e converte para String.
nome: map['nome'] as String,
Pega o valor da chave ip e converte para String.
ip: map['ip'] as String,
Pega o valor da chave status e converte para String.
status: map['status'] as String,
Esse as String e as int é necessário porque o Map<String, Object?> pode guardar vários tipos de valores. Como o Dart precisa saber o tipo correto, fazemos a conversão.
Exemplo completo com insert e select usando objeto
import 'package:sqlite3/sqlite3.dart';
class Servidor {
final int? id;
final String nome;
final String ip;
final String status;
Servidor({
this.id,
required this.nome,
required this.ip,
required this.status,
});
Map<String, Object?> toMap() {
return {
'id': id,
'nome': nome,
'ip': ip,
'status': status,
};
}
factory Servidor.fromMap(Map<String, Object?> map) {
return Servidor(
id: map['id'] as int,
nome: map['nome'] as String,
ip: map['ip'] as String,
status: map['status'] as String,
);
}
}
void main() {
final db = sqlite3.open('infra.db');
db.execute('''
CREATE TABLE IF NOT EXISTS servidores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
ip TEXT NOT NULL,
status TEXT NOT NULL
);
''');
db.execute('DELETE FROM servidores;');
db.execute("DELETE FROM sqlite_sequence WHERE name = 'servidores';");
final servidor = Servidor(
nome: 'web01',
ip: '192.168.0.10',
status: 'ativo',
);
inserirServidor(db, servidor);
final servidores = listarServidores(db);
for (final servidor in servidores) {
print(
'ID: ${servidor.id} | '
'Nome: ${servidor.nome} | '
'IP: ${servidor.ip} | '
'Status: ${servidor.status}',
);
}
db.dispose();
}
void inserirServidor(Database db, Servidor servidor) {
final dados = servidor.toMap();
final insert = db.prepare('''
INSERT INTO servidores (nome, ip, status)
VALUES (?, ?, ?);
''');
insert.execute([
dados['nome'],
dados['ip'],
dados['status'],
]);
insert.dispose();
}
List<Servidor> listarServidores(Database db) {
final resultado = db.select('''
SELECT id, nome, ip, status
FROM servidores
ORDER BY id;
''');
final servidores = <Servidor>[];
for (final linha in resultado) {
final map = {
'id': linha['id'],
'nome': linha['nome'],
'ip': linha['ip'],
'status': linha['status'],
};
final servidor = Servidor.fromMap(map);
servidores.add(servidor);
}
return servidores;
}