跳到主要內容

持久化儲存架構:鍵值資料

大多數 Flutter 應用程式,無論大小,在某些時候都需要在使用者裝置上儲存資料,例如 API 金鑰、使用者偏好設定或應該離線可用的資料。

在這個食譜中,您將學習如何將鍵值資料的持久性儲存整合到使用建議的 Flutter 架構設計的 Flutter 應用程式中。如果您完全不熟悉將資料儲存到磁碟,您可以閱讀 在磁碟上儲存鍵值資料 的食譜。

鍵值儲存通常用於儲存簡單資料,例如應用程式設定,在這個食譜中,您將使用它來儲存深色模式偏好設定。如果您想學習如何在裝置上儲存複雜資料,您可能需要使用 SQL。在這種情況下,請查看此食譜之後的食譜,名為 持久性儲存架構:SQL

範例應用程式:具有主題選擇功能的應用程式

#

範例應用程式包含一個單一畫面,頂部有一個應用程式列,一個項目列表,以及底部的文字欄位輸入。

ToDo application in light mode

AppBar 中,Switch 允許使用者在深色和淺色主題模式之間切換。此設定會立即應用,並使用鍵值資料儲存服務儲存在裝置中。當使用者再次啟動應用程式時,該設定會還原。

ToDo application in dark mode

儲存主題選擇鍵值資料

#

此功能遵循建議的 Flutter 架構設計模式,具有呈現層和資料層。

  • 呈現層包含 ThemeSwitch 小工具和 ThemeSwitchViewModel
  • 資料層包含 ThemeRepositorySharedPreferencesService

主題選擇呈現層

#

ThemeSwitch 是一個 StatelessWidget,其中包含一個 Switch 小工具。開關的狀態由 ThemeSwitchViewModel 中的公用欄位 isDarkMode 表示。當使用者點擊開關時,程式碼會執行視圖模型中的 toggle 命令。

dart
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 模式中描述的視圖模型。此視圖模型包含 ThemeSwitch 小工具的狀態,由布林變數 _isDarkMode 表示。

視圖模型使用 ThemeRepository 來儲存和載入深色模式設定。

它包含兩個不同的命令動作:load,從儲存庫載入深色模式設定,以及 toggle,在深色模式和淺色模式之間切換狀態。它透過 isDarkMode getter 公開狀態。

_load 方法實現了 load 命令。此方法呼叫 ThemeRepository.isDarkMode 以取得儲存的設定,並呼叫 notifyListeners() 來刷新 UI。

_toggle 方法實現了 toggle 命令。此方法呼叫 ThemeRepository.setDarkMode 來儲存新的深色模式設定。此外,它會更改 _isDarkMode 的本地狀態,然後呼叫 notifyListeners() 來更新 UI。

dart
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 Command0 load;

  late Command0 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();
    }
  }
}

主題選擇資料層

#

根據架構指南,資料層分為兩個部分:ThemeRepositorySharedPreferencesService

ThemeRepository 是所有主題設定組態的單一事實來源,並處理來自服務層的任何可能錯誤。

在此範例中,ThemeRepository 也透過可觀察的 Stream 公開深色模式設定。這允許應用程式的其他部分訂閱深色模式設定的變更。

ThemeRepository 依賴 SharedPreferencesService。儲存庫從服務取得儲存的值,並在變更時儲存它。

setDarkMode() 方法將新值傳遞給 StreamController,以便任何監聽 observeDarkMode 資料流的元件

dart
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() 方法來儲存深色模式設定,將這個第三方依賴項隱藏在應用程式的其他部分之外。

dart
class SharedPreferencesService {
  static const String _kDartMode = 'darkMode';

  Future<void> setDarkMode(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_kDartMode, value);
  }

  Future<bool> isDarkMode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_kDartMode) ?? false;
  }
}

整合所有部分

#

在此範例中,ThemeRepositorySharedPreferencesService 是在 main() 方法中建立的,並作為建構函式引數相依性傳遞給 MainApp

dart
void main() {
// ···
  runApp(
    MainApp(
      themeRepository: ThemeRepository(
        SharedPreferencesService(),
      ),
// ···
    ),
  );
}

然後,當建立 ThemeSwitch 時,也建立 ThemeSwitchViewModel 並將 ThemeRepository 作為相依性傳遞。

dart
ThemeSwitch(
  viewmodel: ThemeSwitchViewModel(
    widget.themeRepository,
  ),
)

範例應用程式還包含 MainAppViewModel 類別,該類別監聽 ThemeRepository 中的變更,並向 MaterialApp 小工具公開深色模式設定。

dart
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();
  }
}
dart
ListenableBuilder(
  listenable: _viewModel,
  builder: (context, child) {
    return MaterialApp(
      theme: _viewModel.isDarkMode ? ThemeData.dark() : ThemeData.light(),
      home: child,
    );
  },
  child: //...
)