持久化儲存架構:鍵值資料
大多數 Flutter 應用,無論大小,都需要在使用者裝置上儲存資料,例如 API 金鑰、使用者偏好設定或應離線可用的資料。
在本篇教程中,您將學習如何在 Flutter 應用中整合持久化鍵值資料儲存,該應用採用了推薦的 Flutter 架構設計。如果您對將資料儲存到磁碟完全不熟悉,可以閱讀 將鍵值資料儲存到磁碟 教程。
鍵值儲存通常用於儲存簡單資料,例如應用配置,在本篇教程中,您將使用它來儲存深色模式偏好設定。如果您想學習如何儲存複雜資料到裝置,您可能會想使用 SQL。在這種情況下,可以檢視本教程之後的 持久化儲存架構:SQL 教程。
示例應用:帶有主題選擇的應用
#示例應用由一個螢幕組成,頂部有一個應用欄,一個專案列表,底部有一個文字輸入框。

在 AppBar 中,一個 Switch 允許使用者在深色和淺色主題模式之間切換。此設定會立即應用,並使用鍵值資料儲存服務儲存在裝置上。當用戶重新啟動應用程式時,會恢復此設定。

儲存主題選擇的鍵值資料
#此功能遵循推薦的 Flutter 架構設計模式,分為展示層和資料層。
- 展示層包含
ThemeSwitchwidget 和ThemeSwitchViewModel。 - 資料層包含
ThemeRepository和SharedPreferencesService。
主題選擇展示層
#ThemeSwitch 是一個 StatelessWidget,包含一個 Switch widget。開關的狀態由 ThemeSwitchViewModel 中的公共欄位 isDarkMode 表示。當用戶點選開關時,程式碼會在 view model 中執行 toggle 命令。
class ThemeSwitch extends StatelessWidget {
const ThemeSwitch({super.key, required this.viewmodel});
final ThemeSwitchViewModel viewmodel;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text('Dark Mode'),
ListenableBuilder(
listenable: viewmodel,
builder: (context, _) {
return Switch(
value: viewmodel.isDarkMode,
onChanged: (_) {
viewmodel.toggle.execute();
},
);
},
),
],
),
);
}
}ThemeSwitchViewModel 實現了 MVVM 模式中描述的 view model。此 view model 包含 ThemeSwitch widget 的狀態,由布林變數 _isDarkMode 表示。
view model 使用 ThemeRepository 來儲存和載入深色模式設定。
它包含兩個不同的命令操作:load,用於從 repository 載入深色模式設定;toggle,用於在深色模式和淺色模式之間切換狀態。它透過 isDarkMode getter 公開狀態。
_load 方法實現了 load 命令。此方法呼叫 ThemeRepository.isDarkMode 來獲取儲存的設定,並呼叫 notifyListeners() 來重新整理 UI。
_toggle 方法實現了 toggle 命令。此方法呼叫 ThemeRepository.setDarkMode 來儲存新的深色模式設定。它還更改了 _isDarkMode 的本地狀態,然後呼叫 notifyListeners() 來更新 UI。
class ThemeSwitchViewModel extends ChangeNotifier {
ThemeSwitchViewModel(this._themeRepository) {
load = Command0(_load)..execute();
toggle = Command0(_toggle);
}
final ThemeRepository _themeRepository;
bool _isDarkMode = false;
/// If true show dark mode
bool get isDarkMode => _isDarkMode;
late final Command0<void> load;
late final Command0<void> toggle;
/// Load the current theme setting from the repository
Future<Result<void>> _load() async {
try {
final result = await _themeRepository.isDarkMode();
if (result is Ok<bool>) {
_isDarkMode = result.value;
}
return result;
} on Exception catch (e) {
return Result.error(e);
} finally {
notifyListeners();
}
}
/// Toggle the theme setting
Future<Result<void>> _toggle() async {
try {
_isDarkMode = !_isDarkMode;
return await _themeRepository.setDarkMode(_isDarkMode);
} on Exception catch (e) {
return Result.error(e);
} finally {
notifyListeners();
}
}
}主題選擇資料層
#遵循架構指南,資料層被分成兩部分:ThemeRepository 和 SharedPreferencesService。
ThemeRepository 是所有主題配置設定的單一事實來源,並處理來自服務層的任何可能的錯誤。
在此示例中,ThemeRepository 還透過可觀察的 Stream 公開深色模式設定。這允許應用程式的其他部分訂閱深色模式設定的更改。
ThemeRepository 依賴於 SharedPreferencesService。repository 從服務獲取儲存的值,並在值更改時進行儲存。
setDarkMode() 方法將新值傳遞給 StreamController,以便任何監聽 observeDarkMode stream 的元件
class ThemeRepository {
ThemeRepository(this._service);
final _darkModeController = StreamController<bool>.broadcast();
final SharedPreferencesService _service;
/// Get if dark mode is enabled
Future<Result<bool>> isDarkMode() async {
try {
final value = await _service.isDarkMode();
return Result.ok(value);
} on Exception catch (e) {
return Result.error(e);
}
}
/// Set dark mode
Future<Result<void>> setDarkMode(bool value) async {
try {
await _service.setDarkMode(value);
_darkModeController.add(value);
return Result.ok(null);
} on Exception catch (e) {
return Result.error(e);
}
}
/// Stream that emits theme config changes.
/// ViewModels should call [isDarkMode] to get the current theme setting.
Stream<bool> observeDarkMode() => _darkModeController.stream;
}SharedPreferencesService 封裝了 SharedPreferences 外掛的功能,並透過呼叫 setBool() 和 getBool() 方法來儲存深色模式設定,從而隱藏了此第三方依賴項與應用程式其餘部分。
class SharedPreferencesService {
static const String _kDarkMode = 'darkMode';
Future<void> setDarkMode(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_kDarkMode, value);
}
Future<bool> isDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_kDarkMode) ?? false;
}
}整合所有概念
#在此示例中,ThemeRepository 和 SharedPreferencesService 在 main() 方法中建立,並作為建構函式引數依賴項傳遞給 MainApp。
void main() {
// ···
runApp(
MainApp(
themeRepository: ThemeRepository(SharedPreferencesService()),
// ···
),
);
}然後,在建立 ThemeSwitch 時,還會建立 ThemeSwitchViewModel 並將 ThemeRepository 作為依賴項傳遞。
ThemeSwitch(
viewmodel: ThemeSwitchViewModel(widget.themeRepository),
),示例應用程式還包括 MainAppViewModel 類,該類監聽 ThemeRepository 的更改,並將深色模式設定暴露給 MaterialApp widget。
class MainAppViewModel extends ChangeNotifier {
MainAppViewModel(this._themeRepository) {
_subscription = _themeRepository.observeDarkMode().listen((isDarkMode) {
_isDarkMode = isDarkMode;
notifyListeners();
});
_load();
}
final ThemeRepository _themeRepository;
StreamSubscription<bool>? _subscription;
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
Future<void> _load() async {
try {
final result = await _themeRepository.isDarkMode();
if (result is Ok<bool>) {
_isDarkMode = result.value;
}
} on Exception catch (_) {
// handle error
} finally {
notifyListeners();
}
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}ListenableBuilder(
listenable: _viewModel,
builder: (context, child) {
return MaterialApp(
theme: _viewModel.isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: child,
);
},
child: //...
)