Escriba una aplicación TODO para aprender Moutter, una herramienta de base de datos Flutter

Escriba una aplicación TODO para aprender Moutter, una herramienta de base de datos Flutter

El almacenamiento oficial de la base de datos de Flutter, el documento oficial: https://flutter.dev/docs/cookbook/persistence/sqlite
está escrito directamente para manipular la base de datos SQLite.

¿Existe algún paquete que pueda ayudar a los desarrolladores a almacenar la base de datos de manera más conveniente como Android Room?

Moor es para este propósito: https://pub.dev/packages/moor .
Su nombre es revertir Room. Es un paquete de terceros.

Para aprender cómo usarlo, hice una pequeña aplicación de tareas: https://github.com/mengdd/more_todo .

Este artículo es un registro de trabajo.

TL; DR

Haga la aplicación TODO con Moor:

  • Uso básico: adición dependiente, creación de bases de datos y tablas, operaciones básicas en tablas.
  • Resolución de problemas: inserte el tipo de atención de datos; organización de archivos de varias tablas.
  • Funciones de uso común: claves externas y combinaciones, actualizaciones de bases de datos, consultas condicionales.

Código: aplicación Todo: https://github.com/mengdd/more_todo

Moor uso básico

Aquí hay un documento oficial:
Moor Getting Started

Paso 1: agregar dependencias

pubspec.yamlMedio:

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

Aquí estoy usando la última versión actual (2020.4), después de eso, actualice el número de versión de cada paquete a la última versión.

Explicación de cada paquete:

* 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

Ahora se recomienda usar moor_ffien su lugar moor_flutter.

Se utilizan algunos ejemplos en Internet moor_flutter, por lo que al mirar esos ejemplos, algunos lugares pueden no ser correctos.

Paso 2: definir la base de datos y la tabla

Cree un nuevo archivo, por ejemplo 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);
  });
}

Varios puntos de conocimiento:

  • Para agregar part 'todo_database.g.dart';, espere un minuto para generar este archivo.
  • La clase definida aquí es Todosque la clase de entidad específica generada eliminará s, es decir Todo. Si desea especificar el nombre de la clase generada, puede agregar un comentario @DataClassName("Category")a la clase , por ejemplo :, la clase generada se llamará "Categoría".
  • Convención: $ es el prefijo del nombre de clase de la clase generada, .g.dartes el archivo generado.

Paso 3: generar código

Ejecutar:

flutter packages pub run build_runner build

o:

flutter packages pub run build_runner watch

Para construir una sola vez (construir) o continuo (mirar).

Si no sale bien, puede ser necesario agregar --delete-conflicting-outputs:

flutter packages pub run build_runner watch --delete-conflicting-outputs

Después de ejecutar con éxito, genere el todo_database.g.dartarchivo.

Todos los errores reportados en el código deberían desaparecer.

Paso 4: agregue el método de agregar, eliminar, modificar y verificar

Para un ejemplo simple, escriba el método directamente en la clase de base de datos:

@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);
}

Las consultas a la base de datos no solo pueden devolver Future sino también Stream, manteniendo una observación continua de los datos.

Tenga en cuenta que el método insertado utiliza el objeto Companion. Más adelante hablaremos de por qué.

El método anterior escribe todos los métodos de operación de la base de datos, y obviamente no es bueno después de más códigos. El
método mejorado es escribir DAO:
https://moor.simonbinder.eu/docs/advanced-features/daos/

Será cambiado más tarde.

Paso 5: proporcionar datos a la interfaz de usuario

Proporcionar métodos de acceso a datos implica la gestión del estado del programa. Hay
muchos métodos, se ha escrito un artículo antes: https://www.cnblogs.com/mengdd/p/flutter-state-management.html

Aquí primero elegimos un método simple para proporcionar objetos de base de datos directamente con el Proveedor, envuelto en la capa externa del 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(),
      ),
    );
  }
}

Cuando sea necesario:

TodoDatabase database = Provider.of<TodoDatabase>(context, listen: false);

Tome el objeto de la base de datos y luego puede llamar a sus métodos.

Luego está la cuestión de cómo usar la interfaz de usuario, y no diré más aquí.

Etiqueta: v0.1.1es el método más simple en mi código .
Puedes ver el pasado para ver la implementación de esta versión más simple.

Paso 6: Mejora: método de extracción a DAO, refactorizar

El método de agregar, eliminar, modificar y verificar se extrae de la base de datos y se escribe en 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);
}

Ejecute la línea de comando para regenerarlo (no es necesario si es un reloj).

De hecho, esto se genera:

part of 'todos_dao.dart';

mixin _$TodosDaoMixin on DatabaseAccessor<TodoDatabase> {
  $TodosTable get todos => db.todos;
}

Los todos aquí son los objetos de la tabla.

Entonces, si no es para cambiar la tabla y solo cambiar la implementación del método en DAO, no hay necesidad de regenerar.

En este momento, la parte que proporcionamos a la interfaz de usuario debe cambiarse.

Anteriormente, el proveedor proporcionaba directamente el objeto de la base de datos. Aunque puede reemplazarse directamente con el objeto DAO, habrá muchos DAO. Si lo proporciona de esta manera, el código pronto se estropeará.

Hay muchas maneras de resolver este problema, este es un problema de diseño arquitectónico.

Permítanme resumir brevemente aquí:

class DatabaseProvider {
  TodosDao _todosDao;

  TodosDao get todosDao => _todosDao;

  DatabaseProvider() {
    TodoDatabase database = TodoDatabase();
    _todosDao = TodosDao(database);
  }
}

La capa más externa se cambia para proporcionar esto:

    return Provider(
      create: (_) => DatabaseProvider(),
//...
    );

Cuando lo usa, puede sacar DAO y usarlo.

Si hay otros DAO, puede agregarlos.

Solución de problemas

Se debe usar un objeto complementario al insertar

Método de inserción de datos:
si escribe así:

Future insertTodo(Todo todo) => into(todos).insert(todo);

Solo enfrenté.

Porque por definición, nuestra identificación se genera e incrementa automáticamente:

IntColumn get id => integer().autoIncrement()();

Pero la clase Todo generada contiene todos los campos no vacíos @required:

Todo(
  {@required this.id,
  @required this.title,
  this.content,
  this.category,
  @required this.completed});

Para crear una nueva instancia e insertarla, no puedo especificar esta identificación incremental yo mismo (¿Es demasiado complicado consultar y luego incrementar manualmente? Por lo general, las prácticas extrañas que no son intuitivas son incorrectas).

Puede ver en estos dos problemas, la explicación del autor también utiliza el objeto Companion:

Entonces el método de inserción finalmente se escribió así:

Future insertTodo(TodosCompanion todo) => into(todos).insert(todo);

Otra forma de escribir es esta:

 Future insertTodo(Insertable<Todo> todo) => into(todos).insert(todo);

Agregar datos:

final todo = TodosCompanion(
  title: Value(input),
  completed: Value(false),
);
todosDao.insertTodo(todo);

Al construir objetos aquí, solo necesita Valueajustar los valores requeridos, lo que no se proporcionará será Value.absent().

La definición de la tabla debe escribirse junto con la clase de la base de datos. ¿Qué pasa con varias tablas?

Debe haber varias tablas en el proyecto real, creo que una tabla y un archivo son mejores.

Entonces, cuando ingenuamente creé un nuevo categories.dartarchivo para mi nueva tabla de datos, como Categoría , heredó la clase Tabla y también especificó el nombre del archivo generado.

part 'categories.g.dart';

@DataClassName('Category')
class Categories extends Table {
//...
}

Esta línea es roja en el código después de ejecutar la compilación:

part 'categories.g.dart';

Este archivo no fue generado.

Después de verificar, se descubre que la Categoryclase todavía se genera en el archivo databse.g.dart.

Discusión sobre este tema: https://github.com/simolus3/moor/issues/480

Hay dos ideas para la solución:

  • Solución simple: el código fuente todavía se escribe por separado, pero todo el código generado se junta.

Eliminar la declaración de parte.

@DataClassName('Category')
class Categories extends Table {
//...
}

El código generado todavía está en el archivo generado de la base de datos del método, pero nuestros archivos fuente parecen estar separados.
Cuando el tipo de datos específico se utiliza más tarde, el archivo importado sigue siendo la clase correspondiente del archivo de base de datos.

  • Usa .moorarchivos.

Requisitos avanzados

Claves foráneas y unirse

El requisito de asociar dos tablas es bastante común.

Por ejemplo, nuestra instancia de tarea pendiente, después de agregar la clase Categoría, desea colocar la tarea en una categoría diferente, si no hay categoría, se coloca en la bandeja de entrada como no categorizada.

Moor no admite directamente claves externas, pero customStatementse implementa a través de ellas.

Aquí, esta columna en la clase Todos, más las restricciones personalizadas, está asociada con la tabla de categorías:

IntColumn get category => integer()
  .nullable()
  .customConstraint('NULL REFERENCES categories(id) ON DELETE CASCADE')();

Use la identificación de la clave primaria .

Esto especifica que puede ser nulo dos veces: una vez nullable()y una vez en la declaración.

De hecho customConstraint, el primero estará cubierto, pero aún necesitamos el primero, que se utiliza para indicar que el campo puede ser nulo en la clase generada.

Además, cuando se elimina la categoría, también se elimina la tarea correspondiente.

Las claves externas no están habilitadas de forma predeterminada y deben ejecutarse:

customStatement('PRAGMA foreign_keys = ON');

Para la parte de consulta de unión, primero ajuste las dos clases en la tercera clase.

class TodoWithCategory {
  final Todo todo;
  final Category category;

  TodoWithCategory({@required this.todo, @required this.category});
}

Después de eso, cambie el DAO de TODO. Tenga en cuenta que aquí se agrega una tabla, por lo que debe regenerarse.

El método de consulta anterior se cambió a esto:

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();
});
}

El resultado devuelto por join es List<TypedResult>, aquí se convierte con el operador de mapa.

Actualización de base de datos

Actualización de la base de datos, agregue nuevas tablas y columnas cuando se actualice la base de datos.

Como las claves foráneas no están habilitadas de manera predeterminada, deben estar habilitadas.

PD: la categoría en Todo se ha creado antes.
Las columnas existentes no se pueden modificar durante la migración. Por lo tanto, la tabla solo se puede descartar y reconstruir.

@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');
        },
      );
}

No esperaba ser dado: Unhandled Exception: SqliteException: near "null": syntax error,
los errores son tabla de la gota de la frase:

Moor: Sent DROP TABLE IF EXISTS null; with args []

Digamos que todos.tableName es nulo.

El propósito del diseño de este get se usó originalmente para especificar un nombre personalizado:
https://pub.dev/documentation/moor/latest/moor_web/Table/tableName.html

Como no configuré un nombre personalizado, aquí se devuelve nulo.

Aquí me cambié a:

migrator.deleteTable(todos.actualTableName);

Consulta condicional

Marque una determinada categoría:

  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();
    });
  }

La combinación de múltiples condiciones &, como la combinación de consulta anterior, no está completa:

query.where(
        categories.id.equals(category.id) & todos.completed.equals(false));

Resumen

Moor es un paquete de terceros que se utiliza para ayudar al almacenamiento local de los programas Flutter. Dado que la consulta de sentencias SQL está abierta, cualquier personalización está bien. El autor está muy entusiasmado y puede ver sus respuestas detalladas en muchos temas.

Este artículo es para hacer una aplicación TODO para practicar el uso del páramo.
Incluye adiciones, eliminaciones y cambios básicos, claves externas, actualizaciones de bases de datos, etc.

Código: https://github.com/mengdd/more_todo

Referencia

Finalmente, bienvenido a prestar atención al número público de WeChat: Paladin Wind
Cuenta pública WeChat

Supongo que te gusta

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