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