MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

基于 Flutter 的 SharedPreferences 数据缓存策略

2022-10-201.5k 阅读

1. Flutter 中的数据缓存需求

在 Flutter 应用开发中,数据缓存是一个至关重要的环节。许多应用场景都需要对数据进行临时存储或持久化保存,以提高应用的性能和用户体验。例如,在一些具有用户登录功能的应用中,我们可能希望将用户的登录状态、用户名等信息进行缓存,这样用户下次打开应用时就无需再次登录,能够快速进入应用主界面。又比如,对于一些频繁请求网络数据的应用,如新闻类应用,缓存新闻列表数据可以避免在网络不佳或者重复打开页面时反复请求相同的数据,从而节省用户流量并加快页面加载速度。

数据缓存还可以用于保存应用的配置信息,比如用户设置的主题模式(夜间模式或日间模式)、字体大小等个性化配置。这样在应用重启后,能够快速恢复到用户之前设置的状态,为用户提供连贯的使用体验。如果没有数据缓存机制,每次应用启动或者页面切换时都去重新获取这些数据,不仅会增加系统开销,还可能因为网络问题导致数据获取失败,影响应用的正常使用。

2. SharedPreferences 简介

SharedPreferences 是 Flutter 中一种轻量级的数据持久化存储方式,它以键值对(key - value)的形式将数据存储在设备的本地文件中。这种存储方式非常适合存储一些简单的、少量的数据,如布尔值(用于记录开关状态)、整型(如计数)、字符串(如用户名)等。SharedPreferences 提供了简单易用的 API,使得开发者可以方便地对数据进行读取和写入操作。

它的底层实现依赖于 Android 平台的 SharedPreferences 和 iOS 平台的 NSUserDefaults。在 Android 上,数据是以 XML 文件的形式存储在设备的 /data/data/<package name>/shared_prefs/ 目录下;在 iOS 上,数据存储在应用的偏好设置文件中。这种跨平台的实现方式,使得 Flutter 开发者无需针对不同平台编写大量特定的存储代码,大大提高了开发效率。

3. 集成 SharedPreferences 到 Flutter 项目

要在 Flutter 项目中使用 SharedPreferences,首先需要在 pubspec.yaml 文件中添加依赖:

dependencies:
  shared_preferences: ^2.0.15

然后运行 flutter pub get 命令来获取该依赖。

在 Dart 代码中导入 shared_preferences 库:

import 'package:shared_preferences/shared_preferences.dart';

4. 基本的数据写入操作

4.1 写入布尔值

写入布尔值到 SharedPreferences 非常简单。假设我们要记录用户是否同意了应用的隐私政策,代码如下:

Future<void> savePrivacyPolicyAccepted() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setBool('is_privacy_policy_accepted', true);
}

这里,我们首先通过 SharedPreferences.getInstance() 异步获取 SharedPreferences 的实例。然后使用 setBool 方法,将键 is_privacy_policy_accepted 与布尔值 true 关联并保存。

4.2 写入整型值

例如,我们要记录用户已经打开应用的次数,可以这样写:

Future<void> incrementAppOpenCount() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  int count = (prefs.getInt('app_open_count')?? 0) + 1;
  await prefs.setInt('app_open_count', count);
}

在这段代码中,我们先通过 getInt 方法获取当前的应用打开次数,如果不存在则默认为 0。然后将次数加 1,并使用 setInt 方法将新的次数保存回 SharedPreferences。

4.3 写入字符串值

假设我们要保存用户的昵称,代码如下:

Future<void> saveUserName(String name) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setString('user_name', name);
}

这里通过 setString 方法将用户昵称以键 user_name 保存到 SharedPreferences 中。

5. 基本的数据读取操作

5.1 读取布尔值

要读取用户是否同意隐私政策的布尔值,代码如下:

Future<bool> isPrivacyPolicyAccepted() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  return prefs.getBool('is_privacy_policy_accepted')?? false;
}

getBool 方法会尝试获取指定键对应的布尔值,如果该键不存在,则返回 false

5.2 读取整型值

读取应用打开次数的代码如下:

Future<int> getAppOpenCount() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  return prefs.getInt('app_open_count')?? 0;
}

同样,如果 app_open_count 键不存在,返回默认值 0。

5.3 读取字符串值

读取用户昵称的代码如下:

Future<String?> getUserName() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  return prefs.getString('user_name');
}

这里返回值是 String?,因为如果 user_name 键不存在,getString 方法会返回 null

6. 复杂数据的缓存策略

6.1 缓存列表数据

有时候我们可能需要缓存一个字符串列表,比如用户收藏的文章标题列表。可以通过将列表转换为 JSON 字符串来进行存储。首先,定义一个将列表转换为 JSON 字符串的方法:

String listToJson(List<String> list) {
  return '["${list.join('","')}"]';
}

然后保存列表的方法如下:

Future<void> saveFavoriteArticleTitles(List<String> titles) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String jsonTitles = listToJson(titles);
  await prefs.setString('favorite_article_titles', jsonTitles);
}

读取列表的方法如下:

Future<List<String>> getFavoriteArticleTitles() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String? jsonTitles = prefs.getString('favorite_article_titles');
  if (jsonTitles == null) {
    return [];
  }
  jsonTitles = jsonTitles.substring(1, jsonTitles.length - 1);
  return jsonTitles.split('","');
}

6.2 缓存对象数据

对于更复杂的对象,比如一个用户信息对象,假设我们有如下的用户信息类:

class UserInfo {
  String name;
  int age;
  bool isPremium;

  UserInfo({required this.name, required this.age, required this.isPremium});

  Map<String, dynamic> toMap() {
    return {
      'name': name,
      'age': age,
      'isPremium': isPremium,
    };
  }

  factory UserInfo.fromMap(Map<String, dynamic> map) {
    return UserInfo(
      name: map['name']?? '',
      age: map['age']?? 0,
      isPremium: map['isPremium']?? false,
    );
  }
}

保存用户信息对象的方法如下:

Future<void> saveUserInfo(UserInfo userInfo) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  Map<String, dynamic> userMap = userInfo.toMap();
  await prefs.setString('user_info', jsonEncode(userMap));
}

读取用户信息对象的方法如下:

Future<UserInfo?> getUserInfo() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String? jsonUser = prefs.getString('user_info');
  if (jsonUser == null) {
    return null;
  }
  Map<String, dynamic> userMap = jsonDecode(jsonUser);
  return UserInfo.fromMap(userMap);
}

这里我们借助 jsonEncodejsonDecode 方法来实现对象与 JSON 字符串之间的转换,从而实现对象数据的缓存。

7. 数据缓存的有效期管理

7.1 手动设置缓存有效期

一种简单的方法是在保存数据时,同时保存一个时间戳,用于记录数据的保存时间。例如,我们保存用户登录信息时,同时记录登录时间:

Future<void> saveLoginInfo(String token, String username) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  DateTime now = DateTime.now();
  await prefs.setString('login_token', token);
  await prefs.setString('login_username', username);
  await prefs.setInt('login_time', now.millisecondsSinceEpoch);
}

在读取数据时,检查当前时间与保存时间的差值是否超过了设定的有效期。假设有效期为 1 小时(3600000 毫秒):

Future<bool> isLoginInfoValid() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  int? loginTime = prefs.getInt('login_time');
  if (loginTime == null) {
    return false;
  }
  DateTime now = DateTime.now();
  int elapsedTime = now.millisecondsSinceEpoch - loginTime;
  return elapsedTime < 3600000;
}

7.2 基于时间间隔的自动清理

我们可以在应用启动时,检查缓存数据的时间戳,并根据设定的时间间隔来自动清理过期数据。例如,对于一些缓存的新闻数据,我们希望每 12 小时更新一次。可以在应用启动的 main 函数中添加如下逻辑:

void main() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  int? newsCacheTime = prefs.getInt('news_cache_time');
  if (newsCacheTime!= null) {
    DateTime now = DateTime.now();
    int elapsedTime = now.millisecondsSinceEpoch - newsCacheTime;
    if (elapsedTime >= 43200000) { // 12 小时
      prefs.remove('news_cache');
      prefs.remove('news_cache_time');
    }
  }
  runApp(MyApp());
}

8. 缓存数据的同步与冲突处理

8.1 多平台同步

在一些跨平台应用中,可能需要在不同设备或者同一设备的不同应用实例之间同步缓存数据。对于 Flutter 应用,可以借助云服务(如 Firebase Realtime Database)来实现。首先,在项目中集成 Firebase:

  1. pubspec.yaml 文件中添加依赖:
dependencies:
  firebase_core: ^2.15.0
  firebase_database: ^10.2.5
  1. 初始化 Firebase:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';

Future<void> initFirebase() async {
  await Firebase.initializeApp();
}
  1. 同步缓存数据: 假设我们要同步用户的收藏列表,在保存本地收藏列表时,同时更新 Firebase 数据库:
Future<void> saveFavoriteArticleTitles(List<String> titles) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String jsonTitles = listToJson(titles);
  await prefs.setString('favorite_article_titles', jsonTitles);

  DatabaseReference ref = FirebaseDatabase.instance.ref('users/[user_id]/favorites');
  await ref.set(jsonTitles);
}

在读取本地收藏列表时,同时检查 Firebase 数据库是否有更新:

Future<List<String>> getFavoriteArticleTitles() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String? jsonTitles = prefs.getString('favorite_article_titles');
  if (jsonTitles == null) {
    DatabaseReference ref = FirebaseDatabase.instance.ref('users/[user_id]/favorites');
    DataSnapshot snapshot = await ref.get();
    if (snapshot.exists) {
      jsonTitles = snapshot.value.toString();
      await prefs.setString('favorite_article_titles', jsonTitles);
    }
  }
  if (jsonTitles == null) {
    return [];
  }
  jsonTitles = jsonTitles.substring(1, jsonTitles.length - 1);
  return jsonTitles.split('","');
}

8.2 冲突处理

当多个实例同时尝试修改缓存数据时,可能会发生冲突。在基于 Firebase 的同步中,可以利用 Firebase 的乐观更新策略和版本控制来处理冲突。例如,在更新收藏列表时,我们可以为每次更新添加一个版本号:

Future<void> saveFavoriteArticleTitles(List<String> titles) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String jsonTitles = listToJson(titles);
  int version = (prefs.getInt('favorite_article_titles_version')?? 0) + 1;
  await prefs.setString('favorite_article_titles', jsonTitles);
  await prefs.setInt('favorite_article_titles_version', version);

  DatabaseReference ref = FirebaseDatabase.instance.ref('users/[user_id]/favorites');
  await ref.update({
    'data': jsonTitles,
   'version': version
  });
}

在读取数据时,比较本地版本号和服务器版本号:

Future<List<String>> getFavoriteArticleTitles() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String? jsonTitles = prefs.getString('favorite_article_titles');
  int localVersion = prefs.getInt('favorite_article_titles_version')?? 0;

  DatabaseReference ref = FirebaseDatabase.instance.ref('users/[user_id]/favorites');
  DataSnapshot snapshot = await ref.get();
  if (snapshot.exists) {
    int serverVersion = snapshot.child('version').value?? 0;
    if (serverVersion > localVersion) {
      jsonTitles = snapshot.child('data').value.toString();
      await prefs.setString('favorite_article_titles', jsonTitles);
      await prefs.setInt('favorite_article_titles_version', serverVersion);
    }
  }
  if (jsonTitles == null) {
    return [];
  }
  jsonTitles = jsonTitles.substring(1, jsonTitles.length - 1);
  return jsonTitles.split('","');
}

9. 性能优化与注意事项

9.1 批量操作

尽量避免频繁的单个读写操作,而是将多个相关的读写操作合并为批量操作。例如,在保存用户的多个设置时,可以这样做:

Future<void> saveUserSettings(bool isDarkMode, int fontSize, String language) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setBool('is_dark_mode', isDarkMode);
  await prefs.setInt('font_size', fontSize);
  await prefs.setString('language', language);
}

而不是分别调用三个不同的保存方法,这样可以减少文件 I/O 操作,提高性能。

9.2 避免大数据量存储

虽然理论上 SharedPreferences 可以存储较大的数据,但由于其底层实现机制,存储大数据量可能会导致性能问题。如果需要存储大量数据,建议使用 SQLite 等更适合大数据存储的方案。

9.3 数据一致性检查

在读取数据时,要注意检查数据的一致性。例如,在读取复杂对象数据时,要确保 JSON 字符串的格式正确,避免因为格式错误导致数据解析失败。可以在解析 JSON 字符串前添加一些校验逻辑:

Future<UserInfo?> getUserInfo() async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  String? jsonUser = prefs.getString('user_info');
  if (jsonUser == null) {
    return null;
  }
  try {
    Map<String, dynamic> userMap = jsonDecode(jsonUser);
    return UserInfo.fromMap(userMap);
  } catch (e) {
    print('Error decoding user info: $e');
    return null;
  }
}

9.4 异常处理

在进行 SharedPreferences 的读写操作时,要处理可能出现的异常。例如,在获取 SharedPreferences 实例时,可能会因为设备存储问题等原因导致获取失败。可以使用 try - catch 块来处理异常:

Future<void> saveUserName(String name) async {
  try {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString('user_name', name);
  } catch (e) {
    print('Error saving user name: $e');
  }
}

通过以上性能优化和注意事项,可以使基于 SharedPreferences 的数据缓存策略更加稳定和高效,为 Flutter 应用提供更好的用户体验。

在实际的 Flutter 应用开发中,合理运用 SharedPreferences 进行数据缓存,结合上述的各种策略和技巧,可以有效地提升应用的性能、稳定性以及用户体验。无论是简单的配置信息存储,还是复杂的数据对象缓存,SharedPreferences 都能在一定程度上满足我们的需求。同时,我们也要注意其适用场景和潜在问题,确保数据的安全、可靠存储与高效访问。在面对更复杂的缓存需求时,结合其他存储方案(如 SQLite、云存储等),可以构建出更完善的数据管理体系。