持久性存储架构:SQL
大多数 Flutter 应用,无论大小,都可能需要在某个时刻将数据存储到用户的设备上。例如,API 密钥、用户偏好或应该离线可用的数据。
在本食谱中,您将学习如何在 Flutter 应用中使用 SQL 集成用于复杂数据的持久性存储,遵循 Flutter 架构设计模式。
要了解如何存储更简单的键值数据,请查看食谱: 持久性存储架构:键值数据。
要阅读本食谱,您应该熟悉 SQL 和 SQLite。如果您需要帮助,可以在阅读本食谱之前阅读使用 SQLite 持久化数据 食谱。
此示例使用sqflite
和sqflite_common_ffi
插件,两者结合起来支持移动端和桌面端。Web 端的支持在实验性插件sqflite_common_ffi_web
中提供,但本示例中未包含。
示例应用:待办事项列表应用
#示例应用包含一个屏幕,顶部有应用栏,一个项目列表和底部有一个文本字段输入。
应用程序的主体包含 TodoListScreen
。此屏幕包含 ListTile
项目的 ListView
,每个项目代表一个待办事项。底部,TextField
允许用户通过编写任务描述然后点击“添加”FilledButton
来创建新的待办事项。
用户可以点击删除 IconButton
来删除待办事项。
待办事项列表使用数据库服务本地存储,并在用户启动应用程序时恢复。
使用 SQL 存储复杂数据
#此功能遵循推荐的Flutter 架构设计,包含 UI 层和数据层。此外,在领域层中,您将找到使用的数据模型。
- UI 层,包含
TodoListScreen
和TodoListViewModel
- 领域层,包含
Todo
数据类 - 数据层,包含
TodoRepository
和DatabaseService
待办事项列表表示层
#TodoListScreen
是一个 Widget,包含负责显示和创建待办事项的 UI。它遵循MVVM 模式,并伴随 TodoListViewModel
,其中包含待办事项列表和三个命令来加载、添加和删除待办事项。
此屏幕分为两部分,一部分包含使用 ListView
实现的待办事项列表,另一部分是 TextField
和 Button
,用于创建新的待办事项。
ListView
由 ListenableBuilder
包裹,它侦听 TodoListViewModel
中的变化,并为每个待办事项显示一个 ListTile
。
ListenableBuilder(
listenable: widget.viewModel,
builder: (context, child) {
return ListView.builder(
itemCount: widget.viewModel.todos.length,
itemBuilder: (context, index) {
final todo = widget.viewModel.todos[index];
return ListTile(
title: Text(todo.task),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => widget.viewModel.delete.execute(todo.id),
),
);
},
);
},
)
待办事项列表在 TodoListViewModel
中定义,并由 load
命令加载。此方法调用 TodoRepository
并获取待办事项列表。
List<Todo> _todos = [];
List<Todo> get todos => _todos;
Future<Result<void>> _load() async {
try {
final result = await _todoRepository.fetchTodos();
switch (result) {
case Ok<List<Todo>>():
_todos = result.value;
return Result.ok(null);
case Error():
return Result.error(result.error);
}
} on Exception catch (e) {
return Result.error(e);
} finally {
notifyListeners();
}
}
按下 FilledButton
,执行 add
命令并传入文本控制器值。
FilledButton.icon(
onPressed: () =>
widget.viewModel.add.execute(_controller.text),
label: const Text('Add'),
icon: const Icon(Icons.add),
)
然后,add
命令使用任务描述文本调用 TodoRepository.createTodo()
方法,并创建一个新的待办事项。
createTodo()
方法返回新创建的 ToDo,然后将其添加到视图模型中的 _todo
列表中。
待办事项包含数据库生成的唯一标识符。这就是为什么视图模型不创建待办事项,而是 TodoRepository
创建的原因。
Future<Result<void>> _add(String task) async {
try {
final result = await _todoRepository.createTodo(task);
switch (result) {
case Ok<Todo>():
_todos.add(result.value);
return Result.ok(null);
case Error():
return Result.error(result.error);
}
} on Exception catch (e) {
return Result.error(e);
} finally {
notifyListeners();
}
}
最后,TodoListScreen
也侦听 add
命令中的结果。当操作完成后,TextEditingController
将被清除。
void _onAdd() {
// 清除添加命令完成时的文本字段。
if (widget.viewModel.add.completed) {
widget.viewModel.add.clearResult();
_controller.clear();
}
}
当用户点击 ListTile
中的 IconButton
时,将执行删除命令。
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => widget.viewModel.delete.execute(todo.id),
)
然后,视图模型调用 TodoRepository.deleteTodo()
方法,传入唯一的待办事项标识符。正确的结果会从视图模型和屏幕中删除待办事项。
Future<Result<void>> _delete(int id) async {
try {
final result = await _todoRepository.deleteTodo(id);
switch (result) {
case Ok<void>():
_todos.removeWhere((todo) => todo.id == id);
return Result.ok(null);
case Error():
return Result.error(result.error);
}
} on Exception catch (e) {
return Result.error(e);
} finally {
notifyListeners();
}
}
待办事项列表领域层
#此示例应用程序的领域层包含待办事项数据模型。
项目由不可变数据类表示。在本例中,应用程序使用 freezed
包来生成代码。
该类具有两个属性,一个由 int
表示的 id,以及一个由 String
表示的任务描述。
@freezed
class Todo with _$Todo {
const factory Todo({
/// 待办事项的唯一标识符。
required int id,
/// 待办事项的任务描述。
required String task,
}) = _Todo;
}
待办事项列表数据层
#此功能的数据层由两个类组成,TodoRepository
和 DatabaseService
。
TodoRepository
充当所有待办事项的真相来源。视图模型必须使用此存储库来访问待办事项列表,并且它不应公开任何关于它们如何存储的实现细节。
在内部,TodoRepository
使用 DatabaseService
,它使用 sqflite
包实现对 SQL 数据库的访问。您可以使用其他存储包(如 sqlite3
、drift
)甚至云存储解决方案(如 firebase_database
)来实现相同的 DatabaseService
。
TodoRepository
在每次请求之前检查数据库是否已打开,如果必要则打开它。
它实现了 fetchTodos()
、createTodo()
和 deleteTodo()
方法。
class TodoRepository {
TodoRepository({
required DatabaseService database,
}) : _database = database;
final DatabaseService _database;
Future<Result<List<Todo>>> fetchTodos() async {
if (!_database.isOpen()) {
await _database.open();
}
return _database.getAll();
}
Future<Result<Todo>> createTodo(String task) async {
if (!_database.isOpen()) {
await _database.open();
}
return _database.insert(task);
}
Future<Result<void>> deleteTodo(int id) async {
if (!_database.isOpen()) {
await _database.open();
}
return _database.delete(id);
}
}
DatabaseService
使用 sqflite
包实现对 SQLite 数据库的访问。
最好将表名和列名定义为常量,以避免编写 SQL 代码时出现错别字。
static const _kTableTodo = 'todo';
static const _kColumnId = '_id';
static const _kColumnTask = 'task';
open()
方法打开现有数据库,如果数据库不存在则创建一个新的数据库。
Future<void> open() async {
_database = await databaseFactory.openDatabase(
join(await databaseFactory.getDatabasesPath(), 'app_database.db'),
options: OpenDatabaseOptions(
onCreate: (db, version) {
return db.execute(
'CREATE TABLE $_kTableTodo($_kColumnId INTEGER PRIMARY KEY AUTOINCREMENT, $_kColumnTask TEXT)',
);
},
version: 1,
),
);
}
请注意,列 id
设置为 primary key
和 autoincrement
;这意味着每个新插入的项目都会为 id
列分配一个新值。
insert()
方法在数据库中创建一个新的待办事项,并返回一个新创建的 Todo 实例。id
如前所述生成。
Future<Result<Todo>> insert(String task) async {
try {
final id = await _database!.insert(_kTableTodo, {
_kColumnTask: task,
});
return Result.ok(Todo(id: id, task: task));
} on Exception catch (e) {
return Result.error(e);
}
}
所有 DatabaseService
操作都使用 Result
类返回一个值,正如Flutter 架构建议 中建议的那样。这有助于处理应用程序代码中后续步骤中的错误。
getAll()
方法执行数据库查询,获取 id
和 task
列中的所有值。对于每个条目,它创建一个 Todo
类实例。
Future<Result<List<Todo>>> getAll() async {
try {
final entries = await _database!.query(
_kTableTodo,
columns: [_kColumnId, _kColumnTask],
);
final list = entries
.map(
(element) => Todo(
id: element[_kColumnId] as int,
task: element[_kColumnTask] as String,
),
)
.toList();
return Result.ok(list);
} on Exception catch (e) {
return Result.error(e);
}
}
delete()
方法根据待办事项 id
执行数据库删除操作。
在这种情况下,如果没有删除任何项目,则返回错误,表明出现问题。
Future<Result<void>> delete(int id) async {
try {
final rowsDeleted = await _database!
.delete(_kTableTodo, where: '$_kColumnId = ?', whereArgs: [id]);
if (rowsDeleted == 0) {
return Result.error(Exception('未找到 id 为 $id 的待办事项'));
}
return Result.ok(null);
} on Exception catch (e) {
return Result.error(e);
}
}
整合
#在应用程序的 main()
方法中,首先初始化 DatabaseService
,这需要在不同的平台上使用不同的初始化代码。然后,将新创建的 DatabaseService
传递给 TodoRepository
,TodoRepository
本身作为构造函数参数依赖项传递给 MainApp
。
void main() {
late DatabaseService databaseService;
if (kIsWeb) {
throw UnsupportedError('不支持的平台');
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
// 初始化 FFI SQLite
sqfliteFfiInit();
databaseService = DatabaseService(
databaseFactory: databaseFactoryFfi,
);
} else {
// 使用默认的原生 SQLite
databaseService = DatabaseService(
databaseFactory: databaseFactory,
);
}
runApp(
MainApp(
// ···
todoRepository: TodoRepository(
database: databaseService,
),
),
);
}
然后,当创建 TodoListScreen
时,也创建 TodoListViewModel
并将 TodoRepository
作为依赖项传递给它。
TodoListScreen(
viewModel: TodoListViewModel(
todoRepository: widget.todoRepository,
),
)
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。