对于许多 Flutter 应用来说,为了提供极佳的离线体验(Offline-first)或是缓存大量的结构化数据,一个健壮的本地数据库是必不可少的。而在 Flutter 生态中,基于 SQLite 的方案是最主流的选择。
今天,我们将以 sqflite 和优秀的 ORM 框架 Floor 为切入点,探讨如何在 Flutter 中设计一个类型安全、支持数据迁移且多线程安全的本地存储架构。
1. 为什么选择 Floor?
虽然原生的 sqflite 插件足够强大,但直接编写 SQL 字符串、手动映射 Map 到 Dart 对象不仅繁琐,而且容易在运行时出错。
Floor 是一个深受 Android Room 启发的 SQLite 抽象层。它通过代码生成(Code Generation)提供了:
- 强类型安全:编译期检查 SQL 语句。
- 响应式支持 :可以将查询结果作为
Stream返回,自动更新 UI。 - 整洁的 DAO 架构:分离数据库逻辑。
2. Floor 核心架构与实战
让我们从零搭建一个基于 Floor 的待办事项(Todo)数据库。
2.1 添加依赖
在 pubspec.yaml 中添加:
yaml
dependencies:
flutter:
sdk: flutter
floor: ^1.4.2
sqflite: ^2.3.0
dev_dependencies:
floor_generator: ^1.4.2
build_runner: ^2.4.6
2.2 定义实体 (Entity)
实体对应数据库中的表。
kotlin
// lib/entity/todo.dart
import 'package:floor/floor.dart';
@entity
class Todo {
@PrimaryKey(autoGenerate: true)
final int? id;
final String title;
@ColumnInfo(name: 'is_completed')
final bool isCompleted;
Todo(this.id, this.title, this.isCompleted);
}
2.3 编写 DAO (Data Access Object)
DAO 包含了访问数据库的方法。
java
// lib/dao/todo_dao.dart
import 'package:floor/floor.dart';
import '../entity/todo.dart';
@dao
abstract class TodoDao {
// 返回 Stream,当表中数据变化时自动推送新数据!
@Query('SELECT * FROM Todo')
Stream<List<Todo>> findAllTodosAsStream();
@Query('SELECT * FROM Todo WHERE id = :id')
Future<Todo?> findTodoById(int id);
@insert
Future<void> insertTodo(Todo todo);
@update
Future<void> updateTodo(Todo todo);
@delete
Future<void> deleteTodo(Todo todo);
}
2.4 组装 Database 与生成代码
scala
// lib/database/app_database.dart
import 'dart:async';
import 'package:floor/floor.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
import '../dao/todo_dao.dart';
import '../entity/todo.dart';
part 'app_database.g.dart'; // 生成的代码将存放于此
@Database(version: 1, entities: [Todo])
abstract class AppDatabase extends FloorDatabase {
TodoDao get todoDao;
}
运行构建命令生成 .g.dart 文件:
arduino
flutter pub run build_runner build
3. 进阶:数据库迁移 (Migrations)
随着应用版本的迭代,表结构不可避免地会发生变化(如新增字段)。处理好 Migration 是防止用户覆盖安装后 Crash 的关键。
假设我们在版本 2 中给 Todo 增加了一个 priority 字段:
ini
// 1. 定义 Migration
final migration1to2 = Migration(1, 2, (database) async {
await database.execute('ALTER TABLE Todo ADD COLUMN priority INTEGER NOT NULL DEFAULT 0');
});
// 2. 初始化时传入
final database = await $FloorAppDatabase
.databaseBuilder('app_database.db')
.addMigrations([migration1to2])
.build();
4. 性能优化:多线程与 Isolate 安全
在 Flutter 中,主 Isolate 负责 UI 渲染。如果进行大量的批量插入或复杂的 SQL 联表查询,可能会阻塞主线程导致丢帧(Jank)。
虽然 sqflite 的多数操作是异步的(底层由 Native 端的后台线程执行),但将大量数据反序列化为 Dart 对象的过程仍然发生在主 Isolate。
优化方案:使用 compute 或单独的 Isolate 处理大数据量运算
javascript
import 'package:flutter/foundation.dart';
// 一个顶层函数,用于在后台 Isolate 处理复杂转换
List<Todo> _parseHeavyData(List<Map<String, dynamic>> rawData) {
// 复杂的映射逻辑、排序等...
return rawData.map((e) => Todo(/*...*/)).toList();
}
// 在业务代码中调用
Future<void> loadMassiveData() async {
final rawData = await fetchRawDataFromDb();
// 将计算转移到后台 Isolate
final todos = await compute(_parseHeavyData, rawData);
// 更新UI...
}
5. 总结
使用 Floor 可以极大提升 Flutter 数据库操作的开发体验和代码质量。结合 Stream 的响应式更新,配合规范的数据库迁移和合理的性能优化,构建一个稳定、高效的本地持久化层其实并不困难。
希望这篇文章能为你的 Flutter 架构设计带来启发!