Escreva um aplicativo TODO para aprender o Moutter, uma ferramenta de banco de dados do Flutter
O armazenamento do banco de dados do Flutter, o documento oficial: https://flutter.dev/docs/cookbook/persistence/sqlite,
é gravado diretamente para manipular o banco de dados SQLite.
Existe algum pacote que possa ajudar os desenvolvedores a armazenar o banco de dados de maneira mais conveniente, como o Android Room?
Moor é esse propósito: https://pub.dev/packages/moor .
Seu nome é o quarto, por sua vez, é um pacote de terceiros ..
Para aprender a usá-lo, criei um pequeno aplicativo de tarefas: https://github.com/mengdd/more_todo .
Este artigo é um registro de trabalho.
TL; DR
Crie o aplicativo TODO com Moor:
- Uso básico: adição dependente, criação de banco de dados e tabela, operações básicas em tabelas.
- Solução de problemas: insira o tipo de atenção aos dados; organização de arquivos de várias tabelas.
- Funções comumente usadas: chaves e junções estrangeiras, atualizações de banco de dados, consultas condicionais.
Código: Todo app: https://github.com/mengdd/more_todo
Amarre o uso básico
Há um documento oficial aqui:
Moor Getting Started
Etapa 1: adicionar dependências
pubspec.yaml
Médio:
dependencies:
flutter:
sdk: flutter
moor: ^2.4.0
moor_ffi: ^0.4.0
path_provider: ^1.6.5
path: ^1.6.4
provider: ^4.0.4
dev_dependencies:
flutter_test:
sdk: flutter
moor_generator: ^2.4.0
build_runner: ^1.8.1
Aqui estou usando a versão mais recente (2020.4) atual; depois disso, atualize o número da versão de cada pacote para a versão mais recente.
Explicação de cada pacote:
* moor: This is the core package defining most apis
* moor_ffi: Contains code that will run the actual queries
* path_provider and path: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
* moor_generator: Generates query code based on your tables
* build_runner: Common tool for code-generation, maintained by the Dart team
Agora é recomendado usar em seu moor_ffi
lugar moor_flutter
.
Alguns exemplos na Internet são usados moor_flutter
; portanto, ao analisá-los, alguns lugares podem não estar certos.
Etapa 2: definir o banco de dados e a tabela
Crie um novo arquivo, por exemplo todo_database.dart
:
import 'dart:io';
import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
part 'todo_database.g.dart';
// this will generate a table called "todos" for us. The rows of that table will
// be represented by a class called "Todo".
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 50)();
TextColumn get content => text().nullable().named('description')();
IntColumn get category => integer().nullable()();
BoolColumn get completed => boolean().withDefault(Constant(false))();
}
@UseMoor(tables: [Todos])
class TodoDatabase extends _$TodoDatabase {
// we tell the database where to store the data with this constructor
TodoDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
// the LazyDatabase util lets us find the right location for the file async.
return LazyDatabase(() async {
// put the database file, called db.sqlite here, into the documents folder
// for your app.
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return VmDatabase(file, logStatements: true);
});
}
Vários pontos de conhecimento:
- Para adicionar
part 'todo_database.g.dart';
, aguarde um minuto para gerar este arquivo. - A classe definida aqui é
Todos
que a classe de entidade específica gerada removerá s, ou seja,Todo
se você desejar especificar o nome da classe gerada, poderá adicionar um comentário@DataClassName("Category")
à classe , por exemplo :, a classe gerada será chamada "Categoria". - Convenção: $ é o prefixo do nome da classe gerada e
.g.dart
é o arquivo gerado.
Etapa 3: gerar código
Execute:
flutter packages pub run build_runner build
ou:
flutter packages pub run build_runner watch
Para criar uma vez (compilação) ou contínua (assistir).
Se não der certo, pode ser necessário adicionar --delete-conflicting-outputs
:
flutter packages pub run build_runner watch --delete-conflicting-outputs
Depois de executar com sucesso, gere o todo_database.g.dart
arquivo.
Todos os erros relatados no código devem desaparecer.
Etapa 4: adicione o método de adicionar, excluir, modificar e verificar
Para um exemplo simples, escreva o método diretamente na classe de banco de dados:
@UseMoor(tables: [Todos])
class TodoDatabase extends _$TodoDatabase {
// we tell the database where to store the data with this constructor
TodoDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
Future<List<Todo>> getAllTodos() => select(todos).get();
Stream<List<Todo>> watchAllTodos() => select(todos).watch();
Future insertTodo(TodosCompanion todo) => into(todos).insert(todo);
Future updateTodo(Todo todo) => update(todos).replace(todo);
Future deleteTodo(Todo todo) => delete(todos).delete(todo);
}
As consultas ao banco de dados podem não apenas retornar Future, mas também Stream, mantendo a observação contínua dos dados.
Observe aqui que o método inserido usa o objeto Companion.Vamos falar sobre o porquê mais tarde.
O método acima grava todos os métodos de operação do banco de dados e, obviamente, não é bom após mais códigos.O
método aprimorado é gravar o DAO:
https://moor.simonbinder.eu/docs/advanced-features/daos/
Isso será alterado mais tarde.
Etapa 5: fornecer dados para a interface do usuário
O fornecimento de métodos de acesso a dados envolve o gerenciamento de estado do programa.Há
muitos métodos, um artigo foi escrito antes: https://www.cnblogs.com/mengdd/p/flutter-state-management.html
Aqui, primeiro escolhemos um método simples para usar diretamente o Provedor para fornecer objetos de banco de dados, envolvidos na camada externa do programa:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (_) => TodoDatabase(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
),
);
}
}
Quando necessário:
TodoDatabase database = Provider.of<TodoDatabase>(context, listen: false);
Pegue o objeto de banco de dados e, em seguida, você pode chamar seus métodos.
Depois, há a questão de como usar a interface do usuário, e não vou dizer mais aqui.
Tag: v0.1.1
é o método mais simples do meu código .
Você pode conferir o passado para ver a implementação desta versão mais simples.
Etapa 6: Melhoria: extrair o método para DAO, refatorar
O método de adição, exclusão, modificação e verificação é extraído do banco de dados e gravado no DAO:
part 'todos_dao.g.dart';
// the _TodosDaoMixin will be created by moor. It contains all the necessary
// fields for the tables. The <MyDatabase> type annotation is the database class
// that should use this dao.
@UseDao(tables: [Todos])
class TodosDao extends DatabaseAccessor<TodoDatabase> with _$TodosDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
TodosDao(TodoDatabase db) : super(db);
Future<List<Todo>> getAllTodos() => select(todos).get();
Stream<List<Todo>> watchAllTodos() => select(todos).watch();
Future insertTodo(TodosCompanion todo) => into(todos).insert(todo);
Future updateTodo(Todo todo) => update(todos).replace(todo);
Future deleteTodo(Todo todo) => delete(todos).delete(todo);
}
Execute a linha de comando para regenerá-la (não é necessário se for um relógio).
De fato, isso é gerado:
part of 'todos_dao.dart';
mixin _$TodosDaoMixin on DatabaseAccessor<TodoDatabase> {
$TodosTable get todos => db.todos;
}
Todos aqui são os objetos da tabela.
Portanto, se não for para alterar a tabela e alterar apenas a implementação do método no DAO, não será necessário gerar novamente.
No momento, a parte que fornecemos para a interface do usuário precisa ser alterada.
Anteriormente, o Provedor fornecia diretamente o objeto de banco de dados. Embora possa ser substituído diretamente pelo objeto DAO, haverá muitos DAOs. Se você fornecer dessa maneira, o código será danificado em breve.
Existem várias maneiras de resolver esse problema: este é um problema de design de arquitetura.
Deixe-me resumir aqui:
class DatabaseProvider {
TodosDao _todosDao;
TodosDao get todosDao => _todosDao;
DatabaseProvider() {
TodoDatabase database = TodoDatabase();
_todosDao = TodosDao(database);
}
}
A camada mais externa é alterada para fornecer isso:
return Provider(
create: (_) => DatabaseProvider(),
//...
);
Ao usá-lo, você pode obter o DAO e usá-lo.
Se houver outros DAOs, você poderá adicioná-los.
Solução de problemas
O objeto complementar deve ser usado ao inserir
Método de inserção de dados:
se você escrever assim:
Future insertTodo(Todo todo) => into(todos).insert(todo);
Apenas sem caroço.
Porque, por definição, nosso ID é gerado e incrementado automaticamente:
IntColumn get id => integer().autoIncrement()();
Mas a classe Todo gerada contém todos os campos não vazios @required
:
Todo(
{@required this.id,
@required this.title,
this.content,
this.category,
@required this.completed});
Para criar uma nova instância e inseri-la, não posso especificar esse ID incremental (é muito complicado consultar e depois incrementar manualmente por mim mesmo. Geralmente, práticas estranhas que não são intuitivas estão erradas).
Você pode ver nessas duas edições, a explicação do autor também usa o objeto Companion:
Portanto, o método insert foi finalmente escrito assim:
Future insertTodo(TodosCompanion todo) => into(todos).insert(todo);
Outra maneira de escrever é esta:
Future insertTodo(Insertable<Todo> todo) => into(todos).insert(todo);
Adicione dados:
final todo = TodosCompanion(
title: Value(input),
completed: Value(false),
);
todosDao.insertTodo(todo);
Ao construir objetos aqui, você só precisa Value
agrupar os valores necessários, o que não é fornecido Value.absent()
.
A definição da tabela deve ser escrita junto com a classe do banco de dados?
Deve haver várias tabelas no projeto real, acho que uma tabela e um arquivo são melhores.
Portanto, quando ingenuamente criei um novo categories.dart
arquivo para minha nova tabela de dados, como Category , ele herdou a classe Table e também especificou o nome do arquivo gerado.
part 'categories.g.dart';
@DataClassName('Category')
class Categories extends Table {
//...
}
Esta linha é vermelha no código após a execução da compilação:
part 'categories.g.dart';
Este arquivo não foi gerado.
Após a verificação, verifica-se que a Category
classe ainda é gerada no arquivo databse.g.dart.
Discussão sobre esse problema: https://github.com/simolus3/moor/issues/480
Existem duas idéias para a solução:
- Solução simples: o código fonte ainda é escrito separadamente, mas todo o código gerado é reunido.
Remova a declaração da peça.
@DataClassName('Category')
class Categories extends Table {
//...
}
O código gerado ainda está no arquivo gerado do banco de dados do método, mas nossos arquivos de origem parecem separados.Quando
o tipo de dados específico é usado posteriormente, o arquivo importado ainda é a classe correspondente do arquivo de banco de dados.
- Use
.moor
arquivos.
Requisitos avançados
Chaves estrangeiras e junção
O requisito de associar duas tabelas é bastante comum.
Por exemplo, nossa instância de todo, depois de adicionar a classe Category, deseja colocar o todo em uma categoria diferente; se não houver categoria, ele será colocado na caixa de entrada como não categorizado.
O Moor não suporta diretamente chaves estrangeiras, mas customStatement
é implementado através delas.
Aqui, esta coluna na classe Todos, além de restrições personalizadas, está associada à tabela de categorias:
IntColumn get category => integer()
.nullable()
.customConstraint('NULL REFERENCES categories(id) ON DELETE CASCADE')();
Use o ID da chave primária .
Isso especifica que ele pode ser nulo duas vezes: uma vez nullable()
e uma vez na instrução.
De fato customConstraint
, o primeiro será coberto, mas ainda precisamos do primeiro, que é usado para indicar que o campo pode ser nulo na classe gerada.
Além disso, quando a categoria é excluída, o todo correspondente também é excluído.
Chaves estrangeiras não estão ativadas por padrão e precisam ser executadas:
customStatement('PRAGMA foreign_keys = ON');
Para a parte da consulta de junção, primeiro agrupe as duas classes na terceira classe.
class TodoWithCategory {
final Todo todo;
final Category category;
TodoWithCategory({@required this.todo, @required this.category});
}
Depois disso, altere o DAO do TODO. Observe que uma tabela é adicionada aqui e, portanto, precisa ser regenerada.
O método de consulta anterior foi alterado para isso:
Stream<List<TodoWithCategory>> watchAllTodos() {
final query = select(todos).join([
leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
]);
return query.watch().map((rows) {
return rows.map((row) {
return TodoWithCategory(
todo: row.readTable(todos),
category: row.readTable(categories),
);
}).toList();
});
}
O resultado retornado pela união é List<TypedResult>
, aqui é convertido com o operador de mapa.
Atualização do banco de dados
Atualização do banco de dados, adicione novas tabelas e colunas quando o banco de dados for atualizado.
Como as chaves estrangeiras não estão ativadas por padrão, elas devem estar ativadas.
PS: A categoria no Todo foi criada antes.
As colunas existentes não podem ser modificadas durante a migração. Portanto, a tabela pode ser descartada e reconstruída apenas.
@UseMoor(tables: [Todos, Categories])
class TodoDatabase extends _$TodoDatabase {
// we tell the database where to store the data with this constructor
TodoDatabase() : super(_openConnection());
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (migrator, from, to) async {
if (from == 1) {
migrator.deleteTable(todos.tableName);
migrator.createTable(todos);
migrator.createTable(categories);
}
},
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
);
}
Eu não esperava ser dado: Unhandled Exception: SqliteException: near "null": syntax error
,
erros são tabela de gota de frase:
Moor: Sent DROP TABLE IF EXISTS null; with args []
Diga que todos.tableName é nulo.
O objetivo do design dessa obtenção foi originalmente usado para especificar um nome personalizado:
https://pub.dev/documentation/moor/latest/moor_web/Table/tableName.html
Como não defini um nome personalizado, null é retornado aqui.
Aqui mudei para:
migrator.deleteTable(todos.actualTableName);
Consulta condicional
Verifique uma determinada categoria:
Stream<List<TodoWithCategory>> watchTodosInCategory(Category category) {
final query = select(todos).join([
leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
]);
if (category != null) {
query.where(categories.id.equals(category.id));
} else {
query.where(isNull(categories.id));
}
return query.watch().map((rows) {
return rows.map((row) {
return TodoWithCategory(
todo: row.readTable(todos),
category: row.readTable(categories),
);
}).toList();
});
}
A combinação de várias condições &
, como a combinação de consulta acima, não está completa:
query.where(
categories.id.equals(category.id) & todos.completed.equals(false));
Sumário
O Moor é um pacote de terceiros usado para ajudar no armazenamento local de programas Flutter. Como a consulta da instrução SQL está aberta, qualquer personalização é boa. O autor está muito entusiasmado e pode ver suas respostas detalhadas em muitos problemas.
Este artigo é para criar um aplicativo TODO para praticar o uso do moor,
incluindo adições, exclusões e alterações básicas, chaves estrangeiras, atualizações de banco de dados etc.
Código: https://github.com/mengdd/more_todo
Referência
- Moor github
- Moor site
- pacote: moor_ffi
- Moor Introdução
- Moor (Room for Flutter) # 1 - Tabelas e consultas - Banco de dados SQLite fluente
- Moor (Room for Flutter) # 3 - Chaves estrangeiras, junções e migrações - Fluent SQLite Database
- Suporte para chave estrangeira SQLite
Finalmente, preste atenção ao número público do WeChat: Paladin Wind