Escreva um aplicativo TODO para aprender o Moutter, uma ferramenta de banco de dados do Flutter

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.yamlMé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_ffilugar 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 é Todosque a classe de entidade específica gerada removerá s, ou seja, Todose 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.dartarquivo.

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 Valueagrupar 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.dartarquivo 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 Categoryclasse 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 .moorarquivos.

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

Finalmente, preste atenção ao número público do WeChat: Paladin Wind
Conta pública WeChat

Acho que você gosta

Origin www.cnblogs.com/mengdd/p/flutter-todo-app-database-using-moor.html
Recomendado
Clasificación