在 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],