4、SharedPreferences进阶:存储复杂数据类型、数据加密、监听变化与性能优化
好,咱们接着聊。上一章我们把 SharedPreferences 的基本用法捋了一遍,说白了就是存个 key-value。但实际项目里,你不可能只存个字符串或者数字。用户信息、购物车、配置列表……这些可都是 List 和 Map。那 SharedPreferences 能搞定吗?能,但得动点脑筋。
4.1 存储复杂数据类型:List 和 Map
SharedPreferences 本身只支持基本类型:int、double、bool、String、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();
我在一个金融类项目里,把所有用户 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」。
4.4 性能优化建议
SharedPreferences 用起来简单,但用不好也会卡。我总结了几条实战经验:
| 问题 | 优化建议 | 原因 |
|---|---|---|
| 频繁读写 | 合并写入,减少 setXxx 调用次数 | 每次写入都会触发磁盘 I/O |
| 存储大量数据 | 改用 SQLite 或文件存储 | SharedPreferences 设计用于少量配置 |
| 在主线程读写 | 使用 async/await,避免阻塞 UI | 虽然很快,但极端情况下会卡顿 |
| 重复获取实例 | 缓存 SharedPreferences 实例 | getInstance() 内部有锁,频繁调用有开销 |
另外,千万别把图片 base64 字符串存进 SharedPreferences。 我见过有人这么干,结果一个用户头像就占了几百 KB,整个 App 启动时读配置卡了 3 秒。后来改成文件存储,问题立刻解决。
最后说一句:SharedPreferences 适合存「小而频繁」的数据。如果你要存几百条记录,或者需要复杂查询,请直接上 SQLite 或者 Hive。工具选对了,后面少踩坑。