在这里插入图片描述

概述

使用 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 初始化就调用、设备存储权限被拒绝、应用沙盒损坏;

  • 解决方案:

    1. 初始化前调用WidgetsFlutterBinding.ensureInitialized()(如 main 方法中);
    2. 检查设备存储权限,确保应用拥有本地存储权限;
    3. 封装工具类,确保仅初始化一次,避免重复调用 getInstance。

问题 2:数据存储后,应用重启丢失

  • 原因:未使用await等待异步存储操作完成、存储后应用立即闪退导致 IO 操作中断、存储的是临时对象未持久化;

  • 解决方案:

    1. 所有 set/remove/clear 方法必须配合await,确保 IO 操作完成;
    2. 数据变更后即时保存,不要延迟到页面销毁时保存;
    3. 避免在dispose方法中执行存储操作,该方法中异步操作可能被中断。

问题 3:复杂对象序列化 / 反序列化失败

  • 原因:实体类未实现 toJson/fromJson 方法、字段名不匹配(如 JSON 中的 key 是 userName,实体类是 user_name)、字段类型不一致(如 JSON 是 int,实体类是 String);

  • 解决方案:

    1. 严格实现 toJson/fromJson 方法,确保字段名、类型完全一致;
    2. 序列化前通过print(article.toJson())调试 Map 格式,反序列化前通过print(jsonStr)调试 JSON 字符串;
    3. 对可能为 null 的字段,在 fromJson 中增加非空判断,如id: json['id'] ?? ''

问题 4:读取数据时触发空安全异常

  • 原因:未设置默认值,get 方法返回 null 后直接使用;
  • 解决方案:严格遵循 “强制设置默认值” 最佳实践,所有 get 方法都通过??设置默认值。

下一步

下一章将学习图片缓存的实现方案,这是本地存储的进阶应用场景:

  • SharedPreferences 仅适合存储图片 URL、缓存过期时间等轻量信息,不适合存储图片文件本身
  • 图片文件需结合path_provider库操作本地文件系统,将图片保存为本地文件;
  • 实现 LRU(最近最少使用)缓存策略,解决图片重复下载、缓存空间不足、缓存清理等问题;
  • 学习缓存过期机制、图片压缩存储,进一步提升应用的加载性能和用户体验。
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐