在 Flutter 中使用数据库

在 Flutter 中使用数据库

在 Flutter 中使用数据库

介绍

以 sqlite 为例,flutter 在使用数据库时需要引入 sqflite 依赖

dependencies:
  flutter:
    sdk: flutter
  sqflite:

导入 package:sqflite/sqflite.dart 与 package:sqflite/sql.dart 后,可以在 openDatabase 中的 onCreate 参数中设置初始化

openDatabase(
  'file:///home/steiner/workspace/playground/todolist/todolist.db',
  onCreate: (database, version) async {
    await database.execute(
      'create table if not exists TaskList('
      'id integer primary key autoincrement,'
      'name text not null'
  version: 1
);

其余数据库操作参考 中文文档

组件中使用数据库

由于在 Dart 中数据库的操作是异步的,返回值是 Future 类型,对 Future 使用 await 需要在异步函数中进行, 各个组件的 build 方法又是同步的,无法使用 Future 不过 flutter 提供了 FutureBuilder 组件,为其提供 futurue 选项来构造组件

FutureBuilder({
  this.future,
  this.initialData,
  required this.builder,
})

这里我们只需要用 future 与 builder 就好了 其中

  • future : FutureBuilder 依赖的 Future ,通常是一个异步耗时任务。
  • initialData : 初始数据,用户设置默认数据。
  • builder : Widget 构建器; 该构建器会在 Future 执行的不同阶段被多次调用,构建器签名如下: Function (BuildContext context, AsyncSnapshot snapshot)

组件由 builder 返回,所有数据的获取通过 snapshot.data ,由于是异步操作,可能会有错误结果, 需要多次调用 future 此时可以通过 snapshot 的一些属性来判断状态

  • snapshot.hasError
  • snapshot.hasData

要查看错误信息,调用 snapshot.error

来看一个例子

  • 定义一个异步函数,返回数据库对象的 Future 类型
Future<Database> loadDataBase() async {
  WidgetsFlutterBinding.ensureInitialized();
  return openDatabase(
    'file:///home/steiner/workspace/playground/todolist/todolist.db',
    onCreate: (database, version) async {
      await database.execute(
    'create table if not exists TaskList('
        'id integer primary key autoincrement,'
        'name text not null'
      List<TaskList> listOfTaskList = [
    TaskList(name: 'Hello', id: 0),
    TaskList(name: 'World', id: 0),
    TaskList(name: 'Fuck', id: 0),
    TaskList(name: 'You', id: 0),
      listOfTaskList.forEach((tasklist) async {
    await database.rawInsert(
      'insert into ${TaskList.TABLE}'
          '(name)'
          'values(?);',
      [tasklist.name]
      await database.execute(
    'create table ${Task.TABLE} ('
        'id integer primary key autoincrement,'
        'name text,'
        'listid integer,'
        'isdone boolean,'
        'foreign key(listid) references ${TaskList.TABLE} (id)'
      List<Task> listOfTask = [
    Task(id: 0, name: "task1", isdone: false, listid: 1),
    Task(id: 0, name: "task2", isdone: false, listid: 1),
    Task(id: 0, name: "task3", isdone: false, listid: 1),
    Task(id: 0, name: "task4", isdone: false, listid: 2),
    Task(id: 0, name: "task5", isdone: false, listid: 2),
      listOfTask.forEach((task) async {
    await database.rawInsert(
      'insert into ${Task.TABLE}'
          '(name, isdone, listid)'
          'values(?, ?, ?);',
      [task.name, task.isdone, task.listid]
    version: 1
}
  • 在 HomePage 组件中定义异步函数 loadTaskList ,返回 List<TaskList> 类型
  • 使用 FutureBuilder ,传入 future
  • 在 builder 中返回组件
class HomePage extends StatelessWidget {
  Future<List<TaskList>> loadTaskList() async {
    final database = await loadDataBase();
    final maps =  await database.query(TaskList.TABLE);
    return List.generate(maps.length, (index) {
  Map<String, dynamic> record = maps[index];
  return TaskList(name: record['name'], id: record['id']);
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(title: Text('HomePage')),
      body: FutureBuilder(
  future: loadTaskList(),
  builder: (BuildContext context, AsyncSnapshot<List<TaskList>> snapshot) {
    if(snapshot.hasError) {
      return Text("fuck, error: ${snapshot.error}");
    } else if(snapshot.hasData) {
      List<TaskList> listOfTaskList = snapshot.data!;
      return Column(
        children: listOfTaskList.map((tasklist) => buildTaskList(context, tasklist)).toList(),
    } else {
      return Text("there is no data now");
  Widget buildTaskList(BuildContext context, TaskList tasklist) {
    return OutlineButton(
      onPressed: () {
  Navigator.push(context, MaterialPageRoute(
      builder: (context) => TaskPage(tasklist: tasklist)
      child: Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text(tasklist.name),
    Text(tasklist.id.toString()),
}

使用 ORM 框架

在一个测试的目录下,有以下文件

  • database.dart
  • database.g.dart
  • main.dart
  • task.dart
  • task_dao.dart

准备工作

在 pubspec.yaml 中需要导入几个依赖

  • floor
  • builder_runner
  • floor_generator

其中最重要的是 floor_generator ,没有他后面的代码生成不会成功

实体类的定义 task.dart

需要为实体类重载两个方法

  • operator ==
  • get hashCode

另外 toString() 可选

import 'package:floor/floor.dart';
@entity
class Task {
  @PrimaryKey(autoGenerate: true)
  int? id;
  final String message;
  Task({
      this.id,
      required this.message,
  @override
  bool operator ==(Object other) =>
  identical(this, other) ||
  other is Task &&
  runtimeType == other.runtimeType &&
  id == other.id &&
  message == other.message;
  @override
  int get hashCode => id.hashCode ^ message.hashCode;
  @override
  String toString() {
    // TODO: implement toString
    return 'Task{id: $id, message: $message}';
}

在代码中有

  • @entity 声明这个类是实体类
  • @PrimaryKey 声明主键
  • bool operator == 重载
  • int get hashCode 重载

其中 @PrimaryKey(autoGenerate = true) 表示这个主键是自增序列, 在构造函数中,主键 id 被定义为可以为空,这样不用传入 id , floor 会自动帮我们补上,按照自增顺序定义 id

DAO 的定义 task_dao.dart

task_dao 可以看作对表 Task 的操作接口

@dao
abstract class TaskDao {
  @Query('select * from task where id = :id')
  Future<Task?> findTaskById(int id) ;
  @Query('select * from task')
  Future<List<Task>> findAllTask();
  @Query('select * from task')
  Stream<List<Task>> findAllTasksAsStream();
  @insert
  Future<void> insertTask(Task task);
  @insert
  Future<void> insertTasks(List<Task> tasks);
  @update
  Future<void> updateTask(Task task);
  @update
  Future<void> updateTasks(List<Task> tasks);
  @delete
  Future<void> deleteTask(Task task);
  @delete
  Future<void> deleteTasks(List<Task> tasks);
}

在代码中,有

  • abstract class 抽象类
  • @dao 声明类是一个 Data Access Object
  • @Query 通过此函数来查询,传入查询语句表示函数的行为
  • @insert 通过此函数来插入数据
  • @update 通过此函数来更新数据
  • @delete 通过此函数来删除数据

其中,插入相同主键的数据,可能会产生冲突,从而程序崩溃 默认的冲突解决方法是 abort ,也可以自己定义方法为 relpace

@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insert_one(Task task);

数据库定义 database.dart

在文件中,

part 'database.g.dart';
@Database(version: 1, entities: [Task])
abstract class FlutterDataBase extends FloorDatabase {
  TaskDao get taskDao;
}
  • part 表示 database.g.dart 是该文件/模块的一部分?
  • FlutterDataBase 是抽象类,继承自 FloorDatabase
  • FlutterDataBase 中定义了一个 getter
  • @Database 这个类看作一个数据库
  • 其中 entities 表示访问的数据表,通过重载 get ,返回 DAO 对象来访问数据表

代码生成

在 database.dart 所在目录下,输入 flutter pub run build_runner build 会生成 database.g.dart 文件 接下来的数据库操作就会通过这个文件

注意 在 database.dart 中需要这样导入 sqflite

import 'package:sqflite/sqflite.dart' as sqflite;

因为 build_runner 生成的文件中有 sqflite.Database 等类声明

创建数据库 main.dart

在异步的主函数中,首先确认初始化 WidgetsFlutterBinding.ensureInitialized() 再通过 database.g.dart 中的 $FloorFlutterDatabase 来创建数据库,再获取 DAO 对象

final database = await $FloorFlutterDataBase
.databaseBuilder('file://./flutter_database.db')
.build();
final dao = database.taskDao;

注意 可以在 databaseBuilder 中传入数据库地址

在数据库更新时刷新组件

使用 FutureBuilder 构造组件只能用一次 future ,这样的话组件不会感知到数据库的更新 为了解决这个问题,我们将获取数据库数据的结果定义为 Stream ,再用 StreamBuilder 来构造

首先,是重新定义一个数据库的查询方法,在 task_dao.dart 中

@Query('select * from task')
Stream<List<Task>> findAllTasksAsStream();

之后,重新生成代码 flutter pub run build_runner build

再是 StreamBuilder 传入 stream 与 builder

StreamBuilder<List<Task>>(
  stream: dao.findAllTasksAsStream(),
  builder: (_, snapshot) {
    if (!snapshot.hasData) return Container();
    final tasks = snapshot.requireData;
    return ListView.builder(
      itemCount: tasks.length,
      itemBuilder: (_, index) {
  return TaskListCell(
    task: tasks[index],