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

在本篇教程中,您將學習如何在 Flutter 應用中整合持久化鍵值資料儲存,該應用採用了推薦的 Flutter 架構設計。如果您對將資料儲存到磁碟完全不熟悉,可以閱讀 將鍵值資料儲存到磁碟 教程。

鍵值儲存通常用於儲存簡單資料,例如應用配置,在本篇教程中,您將使用它來儲存深色模式偏好設定。如果您想學習如何儲存複雜資料到裝置,您可能會想使用 SQL。在這種情況下,可以檢視本教程之後的 持久化儲存架構:SQL 教程。

示例應用:帶有主題選擇的應用

#

示例應用由一個螢幕組成,頂部有一個應用欄,一個專案列表,底部有一個文字輸入框。

ToDo application in light mode

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

ToDo application in dark mode

儲存主題選擇的鍵值資料

#

此功能遵循推薦的 Flutter 架構設計模式,分為展示層和資料層。

  • 展示層包含 ThemeSwitch widget 和 ThemeSwitchViewModel
  • 資料層包含 ThemeRepositorySharedPreferencesService

主題選擇展示層

#

ThemeSwitch 是一個 StatelessWidget,包含一個 Switch widget。開關的狀態由 ThemeSwitchViewModel 中的公共欄位 isDarkMode 表示。當用戶點選開關時,程式碼會在 view model 中執行 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 模式中描述的 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。

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

主題選擇資料層

#

遵循架構指南,資料層被分成兩部分:ThemeRepositorySharedPreferencesService

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

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

ThemeRepository 依賴於 SharedPreferencesService。repository 從服務獲取儲存的值,並在值更改時進行儲存。

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

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 _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;
  }
}

整合所有概念

#

在此示例中,ThemeRepositorySharedPreferencesServicemain() 方法中建立,並作為建構函式引數依賴項傳遞給 MainApp

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

然後,在建立 ThemeSwitch 時,還會建立 ThemeSwitchViewModel 並將 ThemeRepository 作為依賴項傳遞。

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

示例應用程式還包括 MainAppViewModel 類,該類監聽 ThemeRepository 的更改,並將深色模式設定暴露給 MaterialApp widget。

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: //...
)