4、SharedPreferences进阶:存储复杂数据类型、数据加密、监听变化与性能优化

好,咱们接着聊。上一章我们把 SharedPreferences 的基本用法捋了一遍,说白了就是存个 key-value。但实际项目里,你不可能只存个字符串或者数字。用户信息、购物车、配置列表……这些可都是 List 和 Map。那 SharedPreferences 能搞定吗?能,但得动点脑筋。

4.1 存储复杂数据类型:List 和 Map

SharedPreferences 本身只支持基本类型:int、double、bool、String、List。你没看错,它支持 List,但不支持 List 或者 List。那怎么办?

我的做法是:序列化成 JSON 字符串。

说白了,就是把复杂对象变成字符串存进去,取出来的时候再反序列化。Dart 自带的 dart:convert 库就能搞定。

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

// 存储 List<Map>
Future<void> saveUserList(List<Map<String, dynamic>> users) async {
  final prefs = await SharedPreferences.getInstance();
  String jsonString = jsonEncode(users);
  await prefs.setString('user_list', jsonString);
}

// 读取 List<Map>
Future<List<Map<String, dynamic>>> loadUserList() async {
  final prefs = await SharedPreferences.getInstance();
  String? jsonString = prefs.getString('user_list');
  if (jsonString == null) return [];
  List<dynamic> decoded = jsonDecode(jsonString);
  return decoded.cast<Map<String, dynamic>>();
}
小提示: 我个人习惯把序列化和反序列化封装成一个工具类,比如叫 PrefsUtils。这样业务代码里只需要调用 saveObject(key, value)loadObject(key),清爽很多。

这里有个坑要注意:List<String> 虽然原生支持,但如果你存的是空列表,取出来可能是 null。 我曾经在项目里踩过这个雷,后来统一改成用 JSON 字符串存,反而更稳定。

4.2 数据加密存储:flutter_secure_storage

SharedPreferences 存的数据是明文,存在应用沙盒里。对于 token、密码、支付信息这些敏感数据,绝对不能这么干。那怎么办?用 flutter_secure_storage

这个插件底层利用了各平台的安全存储机制:iOS 用 Keychain,Android 用 EncryptedSharedPreferences。说白了,数据是加密后存进去的,别人拿不到。

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage();

// 存储加密数据
await storage.write(key: 'access_token', value: 'your_token_here');

// 读取加密数据
String? token = await storage.read(key: 'access_token');

// 删除
await storage.delete(key: 'access_token');

// 清空所有
await storage.deleteAll();
注意: flutter_secure_storage 的读写速度比 SharedPreferences 慢,因为它涉及加解密操作。所以别用它存大量数据,只存真正需要保护的敏感信息。

我在一个金融类项目里,把所有用户 token 和支付密码都用这个存。有一次测试同事问我:「为啥登录比之前慢了一点点?」我说:「慢 100 毫秒,换一个安全级别,值不值?」他想了想,点了点头。

4.3 监听数据变化

SharedPreferences 本身没有提供直接的监听回调。但 Flutter 是响应式的,我们可以自己实现。

我的方案是:用 ValueNotifier 或者 InheritedWidget 包装一下。

举个例子,你有一个主题设置存在 SharedPreferences 里,切换主题后,UI 要立刻刷新。怎么做?

class ThemeNotifier extends ChangeNotifier {
  bool _isDark = false;
  bool get isDark => _isDark;

  ThemeNotifier() {
    _loadTheme();
  }

  Future<void> _loadTheme() async {
    final prefs = await SharedPreferences.getInstance();
    _isDark = prefs.getBool('is_dark') ?? false;
    notifyListeners();
  }

  Future<void> toggleTheme() async {
    _isDark = !_isDark;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('is_dark', _isDark);
    notifyListeners();
  }
}

然后在顶层用 ChangeNotifierProvider 包裹,所有子组件就能自动响应变化了。嗯,这个模式在 Flutter 里非常常见,说白了就是「数据驱动 UI」。

核心思路: 不要直接监听 SharedPreferences 的「文件变化」,而是监听「业务数据的变化」。把 SharedPreferences 当作持久化层,用状态管理工具(Provider、Riverpod、Bloc 等)做 UI 响应层。

4.4 性能优化建议

SharedPreferences 用起来简单,但用不好也会卡。我总结了几条实战经验:

问题 优化建议 原因
频繁读写 合并写入,减少 setXxx 调用次数 每次写入都会触发磁盘 I/O
存储大量数据 改用 SQLite 或文件存储 SharedPreferences 设计用于少量配置
在主线程读写 使用 async/await,避免阻塞 UI 虽然很快,但极端情况下会卡顿
重复获取实例 缓存 SharedPreferences 实例 getInstance() 内部有锁,频繁调用有开销
我的习惯: 在 App 启动时,一次性把 SharedPreferences 实例取出来,存到一个全局变量或者依赖注入容器里。后面所有地方都用这个实例,避免反复调用 getInstance()。

另外,千万别把图片 base64 字符串存进 SharedPreferences。 我见过有人这么干,结果一个用户头像就占了几百 KB,整个 App 启动时读配置卡了 3 秒。后来改成文件存储,问题立刻解决。

最后说一句:SharedPreferences 适合存「小而频繁」的数据。如果你要存几百条记录,或者需要复杂查询,请直接上 SQLite 或者 Hive。工具选对了,后面少踩坑。