持久化儲存架構:SQL
大多數的 Flutter 應用程式,無論大小,都可能需要在某個時間點將資料儲存在使用者的裝置上。例如,API 金鑰、使用者偏好設定或應該離線可用的資料。
在此食譜中,您將學習如何使用 SQL 在 Flutter 應用程式中整合複雜資料的持久化儲存,並遵循 Flutter 架構設計模式。
若要了解如何儲存更簡單的鍵值資料,請參考食譜:持久儲存架構:鍵值資料。
要閱讀本食譜,您應該熟悉 SQL 和 SQLite。如果您需要協助,可以在閱讀本食譜之前先閱讀使用 SQLite 持久化資料食譜。
此範例使用 sqflite
和 sqflite_common_ffi
外掛程式,兩者結合支援行動裝置和桌面裝置。網頁支援由實驗性外掛程式 sqflite_common_ffi_web
提供,但未包含在此範例中。
範例應用程式:ToDo 清單應用程式
#此範例應用程式包含一個單一畫面,頂部有一個應用程式列,一個項目清單,以及底部的文字欄位輸入。
應用程式的主體包含 TodoListScreen
。此畫面包含 ListView
的 ListTile
項目,每個項目代表一個待辦事項。在底部,TextField
允許使用者透過輸入任務描述,然後點擊「新增」FilledButton
來建立新的待辦事項。
使用者可以點擊刪除 IconButton
來刪除待辦事項。
待辦事項清單使用資料庫服務在本機儲存,並在使用者啟動應用程式時還原。
使用 SQL 儲存複雜資料
#此功能遵循建議的 Flutter 架構設計,包含 UI 層和資料層。此外,在領域層中,您會找到使用的資料模型。
- UI 層包含
TodoListScreen
和TodoListViewModel
- 領域層包含
Todo
資料類別 - 資料層包含
TodoRepository
和DatabaseService
ToDo 清單呈現層
#TodoListScreen
是一個 Widget,其中包含負責顯示和建立待辦事項的 UI。它遵循 MVVM 模式,並搭配 TodoListViewModel
,其中包含待辦事項清單和三個用於載入、新增和刪除待辦事項的命令。
此畫面分為兩個部分,一個部分包含待辦事項清單,使用 ListView
實作,另一個部分是 TextField
和 Button
,用於建立新的待辦事項。
ListView
由 ListenableBuilder
包裝,該 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
清單。
待辦事項包含由資料庫產生的唯一識別碼。這就是為什麼檢視模型不建立待辦事項,而是由 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() {
// Clear the text field when the add command completes.
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();
}
}
ToDo 清單網域層
#此範例應用程式的領域層包含待辦事項資料模型。
項目由不可變的資料類別呈現。在此範例中,應用程式使用 freezed
套件來產生程式碼。
該類別有兩個屬性,一個是由 int
表示的 id,以及一個由 String
表示的任務描述。
@freezed
class Todo with _$Todo {
const factory Todo({
/// The unique identifier of the Todo item.
required int id,
/// The task description of the Todo item.
required String task,
}) = _Todo;
}
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('No todo found with id $id'));
}
return Result.ok(null);
} on Exception catch (e) {
return Result.error(e);
}
}
將所有內容整合在一起
#在您的應用程式的 main()
方法中,首先初始化 DatabaseService
,這需要在不同的平台上使用不同的初始化程式碼。然後,將新建立的 DatabaseService
傳遞到 TodoRepository
,然後再將其作為建構函式引數相依性傳遞到 MainApp
。
void main() {
late DatabaseService databaseService;
if (kIsWeb) {
throw UnsupportedError('Platform not supported');
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
// Initialize FFI SQLite
sqfliteFfiInit();
databaseService = DatabaseService(
databaseFactory: databaseFactoryFfi,
);
} else {
// Use default native SQLite
databaseService = DatabaseService(
databaseFactory: databaseFactory,
);
}
runApp(
MainApp(
// ···
todoRepository: TodoRepository(
database: databaseService,
),
),
);
}
然後,當建立 TodoListScreen
時,也建立 TodoListViewModel
並將 TodoRepository
作為相依性傳遞給它。
TodoListScreen(
viewModel: TodoListViewModel(
todoRepository: widget.todoRepository,
),
)
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面最後更新於 2024-11-24。 檢視原始碼 或 回報問題。