Flutter for OpenHarmony:从零搭建今日资讯App(二十六)本地存储实现
本文介绍了使用SharedPreferences实现本地数据存储的方法。主要内容包括:保存简单数据(字符串、数字、布尔值等)、通过JSON序列化保存复杂对象、清除数据的操作方式。文章强调了异步操作的重要性,并提供了最佳实践建议,如提供默认值、及时保存和错误处理。最后还介绍了支持的数据类型和技术要点,包括JSON序列化处理。适合需要实现应用设置、收藏和历史记录等本地存储功能的开发者参考。

概述
使用 SharedPreferences 实现应用的本地数据存储,能够高效处理应用设置、用户收藏、操作历史等轻量级数据的持久化需求。SharedPreferences 以键值对形式存储数据,操作简单且性能优异,是 Flutter 开发中实现本地轻量存储的核心方案。
学习目标
- 掌握 SharedPreferences 的初始化与核心读写操作方法
- 理解数据持久化的底层逻辑,确保应用重启后数据不丢失
- 掌握复杂对象与 JSON 序列化 / 反序列化的结合使用方式
- 学会合理管理存储空间,规避存储异常与性能问题
核心实现
1. 保存简单数据
SharedPreferences 的所有操作均基于其实例,需先通过异步方法获取实例,再执行读写操作:
// 初始化SharedPreferences实例
final prefs = await SharedPreferences.getInstance();
// 保存不同类型的简单数据
await prefs.setString('key', 'value'); // 存储字符串
await prefs.setInt('count', 10); // 存储整数
await prefs.setBool('enabled', true); // 存储布尔值
await prefs.setDouble('price', 99.9); // 存储浮点数(补充常用类型)
// 读取数据并设置默认值(避免null异常)
final value = prefs.getString('key') ?? 'default';
final count = prefs.getInt('count') ?? 0;
final enabled = prefs.getBool('enabled') ?? false;
final price = prefs.getDouble('price') ?? 0.0;
关键说明:
- 获取实例的
getInstance()是异步方法,必须使用await等待初始化完成; - 读取数据时通过
??空合并操作符设置默认值,防止 key 不存在时返回 null 导致程序异常。
2. 保存复杂对象
SharedPreferences 不支持直接存储自定义对象,需通过 JSON 序列化将对象转为字符串列表后存储:
步骤 1:定义实体类并实现序列化 / 反序列化
// 示例:新闻文章实体类
class NewsArticle {
final String id;
final String title;
final String content;
// 构造函数
NewsArticle({required this.id, required this.title, required this.content});
// 序列化:对象转JSON Map
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
};
}
// 反序列化:JSON Map转对象
factory NewsArticle.fromJson(Map<String, dynamic> json) {
return NewsArticle(
id: json['id'],
title: json['title'],
content: json['content'],
);
}
}
关键说明:
- 实体类需严格定义与业务匹配的字段,构造函数使用
required修饰必传参数; toJson方法负责将对象转为可序列化的 Map,fromJson工厂方法负责从 Map 还原对象,字段需一一对应。
步骤 2:保存自定义对象列表
// 模拟收藏列表数据
List<NewsArticle> _favorites = [
NewsArticle(id: '1', title: 'Flutter入门', content: 'SharedPreferences使用教程'),
NewsArticle(id: '2', title: 'Dart基础', content: '异步编程详解')
];
// 将对象列表转为JSON字符串列表
final favoritesStrList = _favorites.map((article) =>
jsonEncode(article.toJson())
).toList();
// 存入SharedPreferences
await prefs.setStringList('favorites', favoritesStrList);
关键说明:
- 需导入
dart:convert库才能使用jsonEncode方法,该方法将 Map 转为 JSON 字符串; - 借助
map方法遍历对象列表,统一完成序列化转换,最终转为字符串列表符合 SharedPreferences 存储要求。
步骤 3:读取并还原对象列表
// 读取字符串列表(无数据时返回空列表)
final favoritesJsonList = prefs.getStringList('favorites') ?? [];
// 还原为自定义对象列表
_favorites = favoritesJsonList.map((jsonStr) =>
NewsArticle.fromJson(jsonDecode(jsonStr))
).toList();
关键说明:
getStringList读取不到数据时返回 null,需用??设置默认空列表,避免后续遍历报错;jsonDecode将 JSON 字符串转回 Map,再通过实体类的fromJson方法还原为自定义对象,完成反序列化。
3. 清除数据
根据业务需求选择精准删除或批量清除数据,操作前可增加 key 存在性校验提升代码健壮性:
// 删除指定key对应的单个数据(如取消收藏单条内容)
await prefs.remove('key');
// 清除当前SharedPreferences实例下的所有数据(如用户退出登录)
await prefs.clear();
// 可选:删除前检查key是否存在,避免无效操作
bool hasKey = prefs.containsKey('key');
if (hasKey) {
await prefs.remove('key');
}
关键说明:
remove方法仅删除指定 key 的键值对,不影响其他存储数据,适合单条数据清理场景;clear方法会清空所有存储数据,使用前建议增加用户二次确认,避免误删;containsKey方法快速校验 key 是否存在,减少无效的 IO 操作。
技术要点
1. 支持的数据类型
SharedPreferences 原生仅支持以下基础类型,复杂数据(自定义对象、非字符串列表等)需转换后存储:
- String(字符串)
- int(整数)
- double(浮点数)
- bool(布尔值)
- List(字符串列表)
2. JSON 序列化
- 核心正向逻辑:自定义对象 → Map(toJson) → JSON 字符串(jsonEncode) → 存储;
- 核心反向逻辑:读取 JSON 字符串 → Map(jsonDecode) → 自定义对象(fromJson);
- 依赖:Flutter 内置
dart:convert库,无需额外在pubspec.yaml中配置,直接导入即可使用。
3. 异步操作
-
所有
set/get/remove/clear/getInstance方法均为异步操作,必须配合async/await处理,确保 IO 操作完成后再执行后续逻辑; -
特殊场景处理:Flutter 的initState
方法不能标记为async,若需在页面初始化时读取存储数据,需封装独立的异步方法调用:
void initState() { super.initState(); _initLocalData(); // 调用异步初始化方法,避免阻塞UI } Future<void> _initLocalData() async { final prefs = await SharedPreferences.getInstance(); // 执行数据读取、页面初始化逻辑 final userInfo = prefs.getString('user_info') ?? ''; }
4. 数据持久化底层原理
- Android 端:基于 XML 文件存储,文件路径为
/data/data/应用包名/shared_prefs/,每个 SharedPreferences 实例对应一个 XML 文件; - iOS 端:基于 plist 文件存储,文件位于应用沙盒的
Library/Preferences/目录下; - 持久化特性:数据会永久保存到设备本地,除非主动调用
remove/clear方法删除,或应用被卸载,否则应用重启、设备关机后数据不会丢失。
最佳实践
1. 强制设置默认值
读取数据时必须通过??空合并操作符设置默认值,杜绝 null 安全异常,这是 Flutter 空安全特性的基本要求:
// 推荐写法:所有读取操作都设置合理默认值
final userName = prefs.getString('user_name') ?? '游客';
final loginStatus = prefs.getBool('is_login') ?? false;
final pageSize = prefs.getInt('page_size') ?? 10;
// 不推荐写法:可能返回null,触发空安全编译错误或运行时异常
final userName = prefs.getString('user_name');
2. 数据变更即时保存
用户操作导致数据变化时(如收藏、修改设置、记录历史),立即执行保存逻辑,避免应用意外退出(闪退、强制关闭)导致数据丢失:
// 示例:收藏按钮点击事件,数据变更后即时保存
void onFavoriteTap(NewsArticle article) {
if (_favorites.contains(article)) {
_favorites.remove(article);
} else {
_favorites.add(article);
}
_saveFavoritesToLocal(); // 即时执行本地保存,不延迟
}
// 封装收藏列表保存逻辑,单独抽离便于维护
Future<void> _saveFavoritesToLocal() async {
final prefs = await SharedPreferences.getInstance();
final favoritesStrList = _favorites.map((e) => jsonEncode(e.toJson())).toList();
await prefs.setStringList('favorites', favoritesStrList);
}
3. 捕获存储异常
本地 IO 操作可能触发各类异常(如存储权限不足、设备磁盘空间不足、文件损坏等),需添加try-catch异常捕获,避免程序崩溃,并给用户友好提示:
Future<void> saveUserData(String key, String value) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
// 可选:保存成功后的回调,如提示用户、更新页面状态
} catch (e) {
// 捕获所有存储异常,打印日志便于调试
print('本地存储失败:$e');
// 友好提示用户,提升体验
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('数据保存失败,请检查设备存储')));
}
}
4. 限制存储数据大小
SharedPreferences 设计初衷是存储轻量级配置数据,不适合存储大体积、大量级数据,否则会导致 IO 操作缓慢、应用卡顿:
- 适用场景:用户偏好设置(如主题、字体大小)、小体积状态数据(如登录状态、当前页码)、≤100 条的轻量列表(如短收藏、最近浏览);
- 不适用场景:完整文章内容、图片 / 视频二进制数据、超大列表(≥1000 条)、大体积 JSON 数据;
- 替代方案:大量数据使用 Hive、SQFlite 等本地数据库;大文件(图片、视频)直接使用文件系统存储,配合
path_provider库管理文件路径。
5. 封装工具类(进阶推荐)
将 SharedPreferences 的所有操作封装为全局工具类,统一管理存储逻辑,减少代码冗余,便于后续维护和修改:
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
/// 本地存储工具类 - 基于SharedPreferences
class StorageUtil {
static SharedPreferences? _prefs;
/// 初始化工具类(建议在应用启动时调用,如main方法中)
static Future<void> init() async {
if (_prefs == null) {
_prefs = await SharedPreferences.getInstance();
}
}
// ********** 基础类型存储 **********
/// 保存字符串
static Future<void> setString(String key, String value) async {
await _prefs?.setString(key, value);
}
/// 读取字符串,默认值为空字符串
static String getString(String key, {String defaultValue = ''}) {
return _prefs?.getString(key) ?? defaultValue;
}
/// 保存整数
static Future<void> setInt(String key, int value) async {
await _prefs?.setInt(key, value);
}
/// 读取整数,默认值为0
static int getInt(String key, {int defaultValue = 0}) {
return _prefs?.getInt(key) ?? defaultValue;
}
/// 保存布尔值
static Future<void> setBool(String key, bool value) async {
await _prefs?.setBool(key, value);
}
/// 读取布尔值,默认值为false
static bool getBool(String key, {bool defaultValue = false}) {
return _prefs?.getBool(key) ?? defaultValue;
}
// ********** 复杂对象存储 **********
/// 保存自定义对象列表
/// [toJson]:对象转Map的回调方法(即实体类的toJson方法)
static Future<void> setObjectList<T>(String key, List<T> list, Map<String, dynamic> Function(T) toJson) async {
final strList = list.map((e) => jsonEncode(toJson(e))).toList();
await _prefs?.setStringList(key, strList);
}
/// 读取自定义对象列表
/// [fromJson]:Map转对象的回调方法(即实体类的fromJson工厂方法)
static List<T> getObjectList<T>(String key, T Function(Map<String, dynamic>) fromJson) {
final strList = _prefs?.getStringList(key) ?? [];
return strList.map((str) => fromJson(jsonDecode(str))).toList();
}
// ********** 通用操作 **********
/// 删除指定key的存储数据
static Future<void> remove(String key) async {
await _prefs?.remove(key);
}
/// 清空所有存储数据
static Future<void> clear() async {
await _prefs?.clear();
}
/// 检查指定key是否存在
static bool containsKey(String key) {
return _prefs?.containsKey(key) ?? false;
}
}
工具类使用示例
void main() async {
// 应用启动时初始化存储工具类
WidgetsFlutterBinding.ensureInitialized();
await StorageUtil.init();
runApp(const MyApp());
}
// 保存数据
await StorageUtil.setString('user_name', '张三');
await StorageUtil.setBool('is_login', true);
await StorageUtil.setObjectList('favorites', _favorites, (article) => article.toJson());
// 读取数据
String userName = StorageUtil.getString('user_name');
bool isLogin = StorageUtil.getBool('is_login');
List<NewsArticle> favorites = StorageUtil.getObjectList('favorites', (json) => NewsArticle.fromJson(json));
// 删除数据
await StorageUtil.remove('user_name');
// 清空所有数据
await StorageUtil.clear();
工具类优势:
- 单例设计:仅初始化一次 SharedPreferences 实例,减少 IO 开销;
- 统一入口:所有存储操作都通过该类完成,后续更换存储方案(如改为 Hive)时,只需修改工具类内部,无需改动所有业务代码;
- 简化调用:封装了重复的序列化 / 反序列化逻辑,业务代码中无需重复写 map、jsonEncode 等代码;
- 可扩展性强:按需添加 double、List等方法,满足不同业务需求。
常见问题与解决方案
问题 1:getInstance () 返回 null 或初始化失败
-
原因:应用未完成 Flutter 初始化就调用、设备存储权限被拒绝、应用沙盒损坏;
-
解决方案:
- 初始化前调用
WidgetsFlutterBinding.ensureInitialized()(如 main 方法中); - 检查设备存储权限,确保应用拥有本地存储权限;
- 封装工具类,确保仅初始化一次,避免重复调用 getInstance。
- 初始化前调用
问题 2:数据存储后,应用重启丢失
-
原因:未使用
await等待异步存储操作完成、存储后应用立即闪退导致 IO 操作中断、存储的是临时对象未持久化; -
解决方案:
- 所有 set/remove/clear 方法必须配合
await,确保 IO 操作完成; - 数据变更后即时保存,不要延迟到页面销毁时保存;
- 避免在
dispose方法中执行存储操作,该方法中异步操作可能被中断。
- 所有 set/remove/clear 方法必须配合
问题 3:复杂对象序列化 / 反序列化失败
-
原因:实体类未实现 toJson/fromJson 方法、字段名不匹配(如 JSON 中的 key 是 userName,实体类是 user_name)、字段类型不一致(如 JSON 是 int,实体类是 String);
-
解决方案:
- 严格实现 toJson/fromJson 方法,确保字段名、类型完全一致;
- 序列化前通过
print(article.toJson())调试 Map 格式,反序列化前通过print(jsonStr)调试 JSON 字符串; - 对可能为 null 的字段,在 fromJson 中增加非空判断,如
id: json['id'] ?? ''。
问题 4:读取数据时触发空安全异常
- 原因:未设置默认值,get 方法返回 null 后直接使用;
- 解决方案:严格遵循 “强制设置默认值” 最佳实践,所有 get 方法都通过
??设置默认值。
下一步
下一章将学习图片缓存的实现方案,这是本地存储的进阶应用场景:
- SharedPreferences 仅适合存储图片 URL、缓存过期时间等轻量信息,不适合存储图片文件本身;
- 图片文件需结合
path_provider库操作本地文件系统,将图片保存为本地文件; - 实现 LRU(最近最少使用)缓存策略,解决图片重复下载、缓存空间不足、缓存清理等问题;
- 学习缓存过期机制、图片压缩存储,进一步提升应用的加载性能和用户体验。
更多推荐

所有评论(0)