Skip to main content

“持久性存储架构:键值数据”

大多数 Flutter 应用,无论大小,都需要在某个时刻将数据存储在用户的设备上,例如 API 密钥、用户偏好或应该离线可用的数据。

在本食谱中,您将学习如何在使用推荐的Flutter 架构设计的 Flutter 应用中集成键值数据的持久性存储。如果您完全不熟悉将数据存储到磁盘,您可以阅读在磁盘上存储键值数据食谱。

键值存储通常用于保存简单数据,例如应用配置,在本食谱中,您将使用它来保存暗模式偏好设置。如果您想学习如何在设备上存储复杂数据,您可能需要使用 SQL。在这种情况下,请查看此食谱之后的食谱,名为持久性存储架构:SQL

示例应用:带主题选择的应用

#

示例应用程序包含一个屏幕,顶部有应用栏,底部有项目列表和文本字段输入。

ToDo application in light mode

AppBar中,Switch允许用户在深色和浅色主题模式之间切换。此设置会立即应用,并使用键值数据存储服务存储在设备中。当用户再次启动应用程序时,会恢复此设置。

ToDo application in dark mode

存储主题选择键值数据

#

此功能遵循推荐的 Flutter 架构设计模式,具有表示层和数据层。

  • 表示层包含ThemeSwitch小部件和ThemeSwitchViewModel
  • 数据层包含ThemeRepositorySharedPreferencesService

主题选择表示层

#

ThemeSwitch是一个包含Switch小部件的StatelessWidget。开关的状态由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;

  /// 如果为真,则显示暗模式
  bool get isDarkMode => _isDarkMode;

  late Command0 load;

  late Command0 toggle;

  /// 从存储库加载当前主题设置
  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();
    }
  }

  /// 切换主题设置
  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;

  /// 获取暗模式是否启用
  Future<Result<bool>> isDarkMode() async {
    try {
      final value = await _service.isDarkMode();
      return Result.ok(value);
    } on Exception catch (e) {
      return Result.error(e);
    }
  }

  /// 设置暗模式
  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);
    }
  }

  /// 发出主题配置更改的流。
  /// ViewModels 应该调用 [isDarkMode] 来获取当前主题设置。
  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;
  }
}

整合所有内容

#

在本例中,ThemeRepositorySharedPreferencesServicemain()方法中创建,并作为构造函数参数依赖项传递给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 (_) {
      // 处理错误
    } 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: //...
)