Flutter 本地存储利器:手把手带你玩转 sqflite 数据库
前言
开发移动应用时,把数据存在用户手机本地,是构建流畅、可靠应用的关键一步。无论是为了缓存网络请求的结果、记住用户的个人设置,还是在没网时也能让应用正常工作,一个好用的本地数据库方案都不可或缺。对于 Flutter 开发者来说,sqflite 插件就是我们连接 Dart 世界和强大轻量级数据库 SQLite 的桥梁。
SQLite 本身无需复杂配置,是一个进程内的数据库引擎,凭借出色的跨平台能力和对事务的可靠支持,在移动端开发中备受青睐。而 sqflite 则为 Flutter 量身打造,它通过 Flutter 的插件机制调用平台原生的 SQLite 能力,同时提供了一套纯 Dart 编写的友好 API,让我们能轻松在应用里集成本地数据库功能。
在这篇指南里,我们将从 sqflite 的基本原理出发,一直讲到如何在实际项目中用好它。为了让你有更直观的感受,我们会一起构建一个功能完整的任务管理器示例。无论你是刚接触 Flutter 存储的新手,还是正在寻找性能优化方案的老手,相信都能在这里找到有用的东西。
一、sqflite 是如何工作的?
1.1 理解它的三层架构
sqflite 之所以能同时在 iOS 和 Android 上稳定运行,得益于其清晰的三层设计:
最上层是 Dart 接口层 ,这也是我们开发者直接打交道的部分。它提供了一整套符合 Dart 语言习惯的异步 API(返回 Future),让我们能用 async/await 以非常直观的方式操作数据库,比如构建 SQL、传递参数和解析查询结果。
中间层是 平台通道层,这是 Flutter 插件系统的核心。它负责在 Dart 和原生平台(Java/Kotlin、Objective-C/Swift)之间传递消息,包括把我们的数据库操作请求"翻译"过去,再把原生代码的执行结果"翻译"回来。
最底层是 原生实现层 。在 Android 上,它封装了系统自带的 android.database.sqlite.SQLiteDatabase 类;在 iOS 上,则直接使用 sqlite3 C 语言库。这意味着我们能天然享受到各平台 SQLite 的所有高级特性,比如完整的事务、外键约束,以及性能更好的 WAL 模式。
1.2 全异步操作与 UI 流畅性
sqflite 的所有数据库操作默认都是异步的,这个设计非常契合 Flutter 的响应式框架。它保证了耗时的数据库读写永远不会阻塞 UI 主线程。
看看下面这个例子,我们是如何在 ChangeNotifier 这类状态管理工具中无缝使用它的:
dart
// 基于 Future 的 API 用起来很直接
Future<List<Map<String, dynamic>>> queryData() async {
final db = await database; // 异步获取数据库实例
return await db.query('tasks'); // 异步执行查询
}
// 轻松融入状态管理流程
class TaskProvider extends ChangeNotifier {
List<Task> _tasks = [];
Future<void> loadTasks() async {
final data = await queryData(); // 等待数据查询
_tasks = data.map((map) => Task.fromMap(map)).toList();
notifyListeners(); // 数据到手,通知 UI 更新
}
}
这样的设计有个很大的好处:就算你在处理复杂的查询或导入大量数据,应用界面依然能保持丝滑响应。
1.3 保障数据安全的事务
数据库事务对于确保数据的一致性至关重要,比如转账操作必须同时完成"扣款"和"收款"。sqflite 对此提供了完备的支持。
dart
Future<void> transferPoints(int fromId, int toId, int points) async {
await database.transaction((txn) async { // 开启一个事务
// 1. 检查发送方余额是否足够
final fromUser = await txn.query(
'users',
where: 'id = ? AND points >= ?',
whereArgs: [fromId, points],
);
if (fromUser.isEmpty) {
throw Exception('余额不足');
}
// 2. 扣除发送方点数
await txn.update(
'users',
{'points': Raw('points - ?', [points])},
where: 'id = ?',
whereArgs: [fromId],
);
// 3. 增加接收方点数
await txn.update(
'users',
{'points': Raw('points + ?', [points])},
where: 'id = ?',
whereArgs: [toId],
);
}); // 只有以上三步全部成功,事务才会提交,否则自动回滚
}
在金融、电商等对数据准确性要求极高的场景里,这种"要么全做,要么不做"的机制是必不可少的。
二、实战:构建一个任务管理应用
光说不练假把式,我们通过一个具体的任务管理应用,来看看如何将上述知识落地。
2.1 项目起步:配置依赖
首先,在 pubspec.yaml 里引入必要的包:
yaml
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0 # 核心数据库插件
path: ^1.8.0 # 用于处理路径
provider: ^6.0.0 # 这里选用 provider 做状态管理(你可按喜好替换)
intl: ^0.18.0 # 日期格式化
2.2 定义数据模型
模型类 (lib/models/task.dart) 是业务的基石,它负责在 Dart 对象和数据库表记录之间互相转换。
dart
class Task {
int? id; // 自增主键,插入前为 null
String title;
String description;
bool isCompleted;
DateTime createdAt;
DateTime? dueDate;
Priority priority;
Task({
this.id,
required this.title,
this.description = '',
this.isCompleted = false,
DateTime? createdAt,
this.dueDate,
this.priority = Priority.medium,
}) : createdAt = createdAt ?? DateTime.now();
// 将对象转为 Map,方便插入数据库
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'description': description,
'is_completed': isCompleted ? 1 : 0, // SQLite 用整数存储布尔值
'created_at': createdAt.toIso8601String(),
'due_date': dueDate?.toIso8601String(),
'priority': priority.index,
};
}
// 从数据库查询结果的 Map 构造对象
factory Task.fromMap(Map<String, dynamic> map) {
return Task(
id: map['id'],
title: map['title'],
description: map['description'],
isCompleted: map['is_completed'] == 1,
createdAt: DateTime.parse(map['created_at']),
dueDate: map['due_date'] != null
? DateTime.parse(map['due_date'])
: null,
priority: Priority.values[map['priority']],
);
}
}
enum Priority { low, medium, high, urgent }
2.3 编写数据库服务层
这是整个数据存取的核心 (lib/services/database_service.dart)。我们在这里初始化数据库、创建升级表结构,并封装所有增删改查操作。
dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/task.dart';
class DatabaseService {
static const String _databaseName = 'task_manager.db';
static const int _databaseVersion = 3; // 版本号,用于控制数据库升级
static Database? _database; // 单例数据库实例
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
// 获取设备上存储数据库的文档目录
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, _databaseName);
// 打开数据库,并指定创建和升级的回调
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
onConfigure: _onConfigure,
);
}
Future<void> _onConfigure(Database db) async {
// 推荐开启外键约束,保证数据关联的完整性
await db.execute('PRAGMA foreign_keys = ON');
// 启用 WAL 模式,可以提升读写并发性能
await db.execute('PRAGMA journal_mode = WAL');
}
Future<void> _onCreate(Database db, int version) async {
// 1. 创建任务表
await db.execute('''
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
is_completed INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
due_date TEXT,
priority INTEGER DEFAULT 1,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
)
''');
// 2. 创建分类表
await db.execute('''
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#2196F3'
)
''');
// 3. 为常用查询字段创建索引,大幅提升查询速度
await db.execute('CREATE INDEX idx_tasks_completed ON tasks(is_completed)');
await db.execute('CREATE INDEX idx_tasks_due_date ON tasks(due_date)');
await db.execute('CREATE INDEX idx_tasks_priority ON tasks(priority)');
// 4. 初始化一些默认分类
await db.insert('categories', {'name': 'Personal', 'color': '#FF4081'});
await db.insert('categories', {'name': 'Work', 'color': '#536DFE'});
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// 逐步升级数据库结构,应对应用迭代
if (oldVersion < 2) {
await db.execute('ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 1');
}
if (oldVersion < 3) {
await db.execute('ALTER TABLE tasks ADD COLUMN category_id INTEGER');
await db.execute('''
CREATE TABLE categories (...)
'''); // 建表语句同上,此处省略
}
}
// --- 核心 CRUD 操作 ---
Future<int> insertTask(Task task) async {
final db = await database;
return await db.insert('tasks', task.toMap());
}
Future<List<Task>> getAllTasks({bool? completed, Priority? priority, int? categoryId}) async {
final db = await database;
final where = <String>[];
final whereArgs = <dynamic>[];
// 动态构建查询条件
if (completed != null) {
where.add('is_completed = ?');
whereArgs.add(completed ? 1 : 0);
}
if (priority != null) {
where.add('priority = ?');
whereArgs.add(priority.index);
}
if (categoryId != null) {
where.add('category_id = ?');
whereArgs.add(categoryId);
}
final result = await db.query(
'tasks',
where: where.isNotEmpty ? where.join(' AND ') : null,
whereArgs: whereArgs,
orderBy: 'due_date ASC, priority DESC', // 按截止日期升序,优先级降序排列
);
return result.map(Task.fromMap).toList();
}
Future<int> updateTask(Task task) async {
final db = await database;
return await db.update(
'tasks',
task.toMap(),
where: 'id = ?',
whereArgs: [task.id],
);
}
Future<int> deleteTask(int id) async {
final db = await database;
return await db.delete(
'tasks',
where: 'id = ?',
whereArgs: [id],
);
}
// --- 实用功能示例 ---
// 批量操作:将所有任务标记为完成
Future<void> markAllAsComplete() async {
final db = await database;
await db.update(
'tasks',
{'is_completed': 1},
where: 'is_completed = 0',
);
}
// 复杂查询:获取任务统计信息
Future<Map<String, dynamic>> getTaskStatistics() async {
final db = await database;
final total = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM tasks')
) ?? 0;
final completed = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM tasks WHERE is_completed = 1')
) ?? 0;
final overdue = Sqflite.firstIntValue(
await db.rawQuery('''
SELECT COUNT(*) FROM tasks
WHERE due_date IS NOT NULL
AND due_date < datetime('now')
AND is_completed = 0
''')
) ?? 0;
return {
'total': total,
'completed': completed,
'pending': total - completed,
'overdue': overdue,
'completionRate': total > 0 ? (completed / total) * 100 : 0,
};
}
}
2.4 连接 UI 与状态管理
最后,我们需要一个界面 (lib/screens/task_list_screen.dart) 来展示和操作任务。这里使用 provider 来管理任务列表的状态,界面会随状态自动更新。
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/task_provider.dart'; // 假设有一个 TaskProvider 继承自 ChangeNotifier
import '../widgets/task_item.dart'; // 展示单个任务的 Widget
class TaskListScreen extends StatefulWidget {
@override
_TaskListScreenState createState() => _TaskListScreenState();
}
class _TaskListScreenState extends State<TaskListScreen> {
final TextEditingController _searchController = TextEditingController();
bool _showCompleted = false;
@override
void initState() {
super.initState();
// 页面初始化后加载数据
WidgetsBinding.instance!.addPostFrameCallback((_) {
context.read<TaskProvider>().loadTasks();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Task Manager')),
body: Column(
children: [
// 搜索框
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索任务...',
prefixIcon: Icon(Icons.search),
),
onChanged: (value) => context.read<TaskProvider>().filterTasks(value),
),
),
// 统计信息卡片
Consumer<TaskProvider>(
builder: (context, provider, child) {
final stats = provider.statistics;
return Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('总计', stats['total'].toString()),
_buildStatItem('待办', stats['pending'].toString()),
_buildStatItem('逾期', stats['overdue'].toString()),
_buildStatItem('完成率', '${stats['completionRate'].toStringAsFixed(1)}%'),
],
),
),
);
},
),
// 任务列表主体
Expanded(
child: Consumer<TaskProvider>(
builder: (context, provider, child) {
if (provider.isLoading) return Center(child: CircularProgressIndicator());
final tasks = _showCompleted ? provider.allTasks : provider.pendingTasks;
if (tasks.isEmpty) {
return Center(child: Text(_showCompleted ? '暂无已完成任务' : '暂无待办任务'));
}
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) => TaskItem(
task: tasks[index],
onToggle: (task) => provider.toggleTaskCompletion(task),
onDelete: (task) => _showDeleteDialog(context, task, provider),
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTaskDialog(context),
child: Icon(Icons.add),
),
);
}
// 添加任务、删除确认等对话框方法在此省略,与上文逻辑一致
}
三、让应用更高效:优化与最佳实践
当应用变得复杂、数据量增长后,一些优化技巧能显著提升体验。
3.1 数据库查询性能优化
善用索引: 如果你的应用经常需要按"是否完成"和"截止日期"来筛选任务,那么创建一个复合索引能极大加速这类查询。
dart
await db.execute('CREATE INDEX idx_tasks_status_due ON tasks(is_completed, due_date)');
批量操作: 需要插入大量数据时(比如首次同步),务必使用事务包裹,或者利用 Batch 对象,这比循环单条插入快一个数量级。
dart
await db.transaction((txn) async {
final batch = txn.batch();
for (final task in bigTaskList) {
batch.insert('tasks', task.toMap());
}
await batch.commit();
});
3.2 稳健的数据库升级策略
随着功能迭代,数据库表结构难免要修改。一个清晰的升级管理类能让版本迁移更可控。
dart
class MigrationManager {
static const migrations = {
2: _addPriorityColumn,
3: _addCategoriesTable,
// ... 每个版本对应一个迁移函数
};
static Future<void> migrate(Database db, int oldVersion, int newVersion) async {
for (int v = oldVersion + 1; v <= newVersion; v++) {
await migrations[v]!(db); // 执行当前版本的迁移
}
}
static Future<void> _addCategoriesTable(Database db) async {
await db.execute('CREATE TABLE categories (...)');
// ... 可能的旧数据迁移逻辑
}
}
// 在 openDatabase 的 onUpgrade 回调中调用 MigrationManager.migrate
3.3 错误处理要到位
网络可能断开,磁盘可能写满,良好的错误处理能让应用更健壮。
dart
Future<void> safeDatabaseOperation(Function operation) async {
try {
await operation();
} on DatabaseException catch (e) {
if (e.isUniqueConstraintError()) {
// 处理重复数据错误
print('数据已存在: $e');
} else if (e.isOpenFailedError()) {
// 处理数据库无法打开错误
print('数据库打开失败,可能需要恢复: $e');
} else {
// 其他未知数据库错误,重新抛出或上报
rethrow;
}
} catch (e) {
// 处理其他非数据库异常
print('操作失败: $e');
}
}
结语
sqflite 作为 Flutter 生态中最成熟稳定的本地数据库解决方案,完美平衡了易用性、性能和可靠性。通过本指南,希望你已经掌握了从基础使用到高级优化的完整路径。记住,关键在于根据你应用的实际数据规模和访问模式,合理地设计表结构、创建索引,并处理好数据库的升级与错误。
真正的熟练来自于实践,不妨就从改造这个任务管理器开始,或者在你自己的下一个 Flutter 项目中尝试使用 sqflite 吧。如果在实践中遇到具体问题,Flutter 和 sqflite 的开发者社区都是寻找答案的好地方。