在 Flutter 中使用数据库
Table of Contents
在 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], dao: dao, ); }, ); }, ),
疑问
snapshot.requireData
能否使用 snapshot.data!
来替代 ?