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

Flutter SharedPreferences的使用技巧:存储与读取简单数据

2021-03-246.1k 阅读

Flutter SharedPreferences 简介

在 Flutter 应用开发中,经常会遇到需要在设备上持久化存储简单数据的场景,例如用户的偏好设置、应用的配置信息等。SharedPreferences 便是 Flutter 提供的一种轻量级数据持久化解决方案。它基于键值对(key - value pairs)的形式来存储数据,支持多种数据类型,如布尔值、整数、浮点数、字符串以及字符串列表等。这种存储方式非常适合存储一些不涉及复杂结构且需要简单快速读写的数据。

SharedPreferences 底层依赖于操作系统提供的本地存储机制。在 Android 平台上,它使用 SharedPreferences 类(来自 Android 的 android.content 包),这是 Android 框架提供的一种标准的轻量级存储方式,数据以 XML 文件的形式保存在设备的文件系统中。在 iOS 平台上,它则使用 NSUserDefaults,这是 iOS 提供的用于存储应用偏好设置和简单数据的机制,数据存储在应用的偏好数据库中。

引入 SharedPreferences 依赖

要在 Flutter 项目中使用 SharedPreferences,首先需要在 pubspec.yaml 文件中添加依赖。打开 pubspec.yaml 文件,在 dependencies 部分添加如下内容:

shared_preferences: ^2.0.15

添加完成后,在终端中运行 flutter pub get 命令来获取该依赖包。这一步操作会将 shared_preferences 包及其所有依赖下载到项目中,并配置好项目,使其能够使用 shared_preferences 库中的功能。

读取数据

  1. 获取 SharedPreferences 实例 在读取数据之前,首先要获取 SharedPreferences 的实例。这可以通过 SharedPreferences.getInstance() 方法来实现,该方法返回一个 Future<SharedPreferences>。由于这是一个异步操作,通常使用 async - await 语法来处理。示例代码如下:
Future<void> readData() async {
  final prefs = await SharedPreferences.getInstance();
  // 后续读取数据操作将在此处进行
}

在上述代码中,SharedPreferences.getInstance() 方法返回一个 Future,通过 await 关键字等待该 Future 完成,从而获取到 SharedPreferences 的实例 prefs

  1. 读取布尔值 假设在 SharedPreferences 中存储了一个布尔类型的用户设置,例如用户是否接受推送通知。可以使用 getBool 方法来读取这个布尔值。示例代码如下:
Future<void> readBoolData() async {
  final prefs = await SharedPreferences.getInstance();
  bool? isNotificationEnabled = prefs.getBool('isNotificationEnabled');
  if (isNotificationEnabled != null) {
    print('Notification is enabled: $isNotificationEnabled');
  } else {
    print('Notification setting not found');
  }
}

在上述代码中,prefs.getBool('isNotificationEnabled') 尝试从 SharedPreferences 中读取键为 isNotificationEnabled 的布尔值。如果读取成功,isNotificationEnabled 将包含对应的值;如果该键不存在,则返回 null

  1. 读取整数 若存储了用户的积分数量等整数值,可以使用 getInt 方法读取。示例代码如下:
Future<void> readIntData() async {
  final prefs = await SharedPreferences.getInstance();
  int? userPoints = prefs.getInt('userPoints');
  if (userPoints != null) {
    print('User has $userPoints points');
  } else {
    print('User points not found');
  }
}

这里 prefs.getInt('userPoints') 用于读取键为 userPoints 的整数值,同样,若键不存在则返回 null

  1. 读取浮点数 对于存储的浮点数,如应用的版本号或者一些精确到小数的设置,可以使用 getDouble 方法。示例代码如下:
Future<void> readDoubleData() async {
  final prefs = await SharedPreferences.getInstance();
  double? appVersion = prefs.getDouble('appVersion');
  if (appVersion != null) {
    print('App version is $appVersion');
  } else {
    print('App version not found');
  }
}

prefs.getDouble('appVersion') 尝试读取键为 appVersion 的浮点数。

  1. 读取字符串 读取字符串类型的数据,比如用户的用户名或者一些文本配置,是非常常见的操作。可以使用 getString 方法。示例代码如下:
Future<void> readStringData() async {
  final prefs = await SharedPreferences.getInstance();
  String? username = prefs.getString('username');
  if (username != null) {
    print('Username is $username');
  } else {
    print('Username not found');
  }
}

prefs.getString('username') 用于读取键为 username 的字符串值。

  1. 读取字符串列表 当需要存储一组相关的字符串,如用户收藏的文章标题列表时,可以使用 getStringList 方法。示例代码如下:
Future<void> readStringListData() async {
  final prefs = await SharedPreferences.getInstance();
  List<String>? favoriteTitles = prefs.getStringList('favoriteTitles');
  if (favoriteTitles != null) {
    print('Favorite titles: $favoriteTitles');
  } else {
    print('Favorite titles not found');
  }
}

prefs.getStringList('favoriteTitles') 用于读取键为 favoriteTitles 的字符串列表。

存储数据

  1. 存储布尔值 假设要存储用户是否同意隐私政策的设置,可以使用 setBool 方法。示例代码如下:
Future<void> saveBoolData() async {
  final prefs = await SharedPreferences.getInstance();
  bool isPrivacyPolicyAccepted = true;
  await prefs.setBool('isPrivacyPolicyAccepted', isPrivacyPolicyAccepted);
  print('Privacy policy acceptance saved');
}

在上述代码中,首先获取 SharedPreferences 实例,然后定义一个布尔变量 isPrivacyPolicyAccepted 表示用户是否接受隐私政策,接着使用 prefs.setBool 方法将该值存储到 SharedPreferences 中,键为 isPrivacyPolicyAccepted。由于 setBool 方法返回一个 Future,所以使用 await 等待操作完成。

  1. 存储整数 若要存储用户的等级,可以使用 setInt 方法。示例代码如下:
Future<void> saveIntData() async {
  final prefs = await SharedPreferences.getInstance();
  int userLevel = 5;
  await prefs.setInt('userLevel', userLevel);
  print('User level saved');
}

这里 prefs.setInt('userLevel', userLevel) 将用户等级 userLevel 存储到 SharedPreferences 中,键为 userLevel

  1. 存储浮点数 例如要存储应用的当前缩放比例,可以使用 setDouble 方法。示例代码如下:
Future<void> saveDoubleData() async {
  final prefs = await SharedPreferences.getInstance();
  double zoomRatio = 1.5;
  await prefs.setDouble('zoomRatio', zoomRatio);
  print('Zoom ratio saved');
}

prefs.setDouble('zoomRatio', zoomRatio) 将缩放比例 zoomRatio 存储到 SharedPreferences 中,键为 zoomRatio

  1. 存储字符串 存储用户的昵称是常见的字符串存储需求,可以使用 setString 方法。示例代码如下:
Future<void> saveStringData() async {
  final prefs = await SharedPreferences.getInstance();
  String nickname = 'JohnDoe';
  await prefs.setString('nickname', nickname);
  print('Nickname saved');
}

prefs.setString('nickname', nickname) 将昵称 nickname 存储到 SharedPreferences 中,键为 nickname

  1. 存储字符串列表 假设用户收藏了一些商品的名称,要将这些名称存储为字符串列表,可以使用 setStringList 方法。示例代码如下:
Future<void> saveStringListData() async {
  final prefs = await SharedPreferences.getInstance();
  List<String> favoriteProducts = ['Product A', 'Product B', 'Product C'];
  await prefs.setStringList('favoriteProducts', favoriteProducts);
  print('Favorite products saved');
}

prefs.setStringList('favoriteProducts', favoriteProducts) 将收藏的商品名称列表 favoriteProducts 存储到 SharedPreferences 中,键为 favoriteProducts

删除数据

  1. 删除单个键值对 当需要删除某个特定的设置时,可以使用 remove 方法。例如,要删除用户的登录令牌,可以这样做:
Future<void> deleteData() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.remove('loginToken');
  print('Login token deleted');
}

在上述代码中,prefs.remove('loginToken') 会从 SharedPreferences 中删除键为 loginToken 的键值对。

  1. 删除所有数据 有时可能需要清除应用在 SharedPreferences 中存储的所有数据,比如在用户注销应用或者进行恢复出厂设置的模拟操作时。可以使用 clear 方法来实现。示例代码如下:
Future<void> clearAllData() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.clear();
  print('All data in SharedPreferences cleared');
}

prefs.clear() 会删除 SharedPreferences 中存储的所有键值对。

注意事项

  1. 数据类型一致性 在存储和读取数据时,必须保证数据类型的一致性。例如,如果存储时使用 setInt 方法,那么读取时必须使用 getInt 方法。如果尝试使用错误的数据类型读取,可能会得到 null 值或者引发运行时错误。比如,若存储的是整数,却使用 getString 方法读取,将得到 null

  2. 异步操作 SharedPreferences 的大多数操作都是异步的,因为它们涉及到与本地存储系统的交互。这意味着在使用 SharedPreferences 时,要妥善处理异步操作。async - await 语法是一种简洁有效的处理方式,但在一些复杂场景下,也可以使用 Future 的其他方法,如 thencatchError 等进行链式调用和错误处理。

  3. 数据持久化时机 虽然 SharedPreferences 声称是持久化存储,但实际上,数据并不是每次调用存储方法时就立即写入存储设备。为了提高性能,操作系统可能会对写入操作进行缓存和批量处理。因此,在某些情况下,即使应用崩溃或者设备重启,数据也有可能还未真正持久化。如果需要确保数据尽快持久化,可以调用 commit 方法(在 Android 平台上)或者 synchronize 方法(在 iOS 平台上),不过这会降低一些性能,因为这些方法会强制立即写入数据。在 Flutter 的 shared_preferences 库中,set 系列方法(如 setBoolsetInt 等)默认会在合适的时机将数据持久化,一般情况下不需要手动调用 commitsynchronize

  4. 键的唯一性SharedPreferences 中,键必须是唯一的。如果使用相同的键存储新的数据,新数据会覆盖旧数据。因此,在设计键名时,要确保其唯一性,以避免数据意外被覆盖。通常可以采用命名空间的方式来命名键,例如对于应用某个模块的设置,可以在键名前加上模块名作为前缀,如 userSettings_nicknameappConfig_theme 等。

  5. 内存占用 虽然 SharedPreferences 适用于存储简单数据,但如果存储的数据量过大,可能会导致内存占用过高。特别是在存储字符串列表时,如果列表中的字符串较多且长度较长,可能会对应用的性能产生一定影响。因此,在使用 SharedPreferences 存储数据时,要对数据量进行合理的评估,对于大量数据的存储,可能需要考虑其他更适合的持久化方案,如 SQLite 数据库。

  6. 跨平台兼容性 虽然 SharedPreferences 在 Android 和 iOS 平台上都有对应的底层实现,但在某些边缘情况下,可能会存在跨平台兼容性问题。例如,不同平台对数据存储的最大限制可能不同,在 Android 上可以存储的数据量可能在 iOS 上就会受到限制。在开发过程中,要针对不同平台进行充分的测试,确保应用在各个平台上都能正常使用 SharedPreferences 进行数据的存储和读取。

  7. 安全性 SharedPreferences 存储的数据是以明文形式保存在设备上的,这意味着如果设备被恶意访问,存储的数据可能会被窃取。对于敏感信息,如用户密码、支付信息等,绝对不能使用 SharedPreferences 进行存储。如果需要存储一些相对敏感但又需要持久化的数据,可以考虑对数据进行加密后再存储,在读取时进行解密操作。

使用场景示例

  1. 用户偏好设置 在许多应用中,用户可以设置自己的偏好,如夜间模式、语言偏好等。以夜间模式为例,可以在用户切换模式时,将设置存储到 SharedPreferences 中。示例代码如下:
Future<void> setNightMode(bool isNightMode) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('isNightMode', isNightMode);
}

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

在应用启动时,可以调用 getNightMode 方法获取用户的夜间模式设置,并根据设置来调整应用的界面主题。

  1. 应用配置信息 应用可能会有一些配置信息,如服务器地址、是否启用某些功能模块等。假设应用有一个是否启用新功能的开关,可以这样存储和读取:
Future<void> setFeatureEnabled(bool isEnabled) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('newFeatureEnabled', isEnabled);
}

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

这样,在应用的相关功能模块加载时,可以通过调用 isFeatureEnabled 方法来决定是否显示或启用该功能。

  1. 用户登录状态 在一些应用中,为了保持用户的登录状态,在用户登录成功后,可以将登录状态存储到 SharedPreferences 中。例如:
Future<void> setLoggedIn(bool isLoggedIn) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('isLoggedIn', isLoggedIn);
}

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

在应用启动时,通过调用 isLoggedIn 方法来判断用户是否已经登录,如果已登录,则直接进入应用主界面,否则显示登录界面。

  1. 游戏进度保存 对于一些简单的游戏,如单机小游戏,可以使用 SharedPreferences 来保存游戏进度。例如,保存玩家的当前关卡数:
Future<void> saveGameLevel(int level) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setInt('gameLevel', level);
}

Future<int> getGameLevel() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getInt('gameLevel') ?? 1;
}

当玩家重新启动游戏时,通过调用 getGameLevel 方法获取上次保存的关卡数,让玩家可以从上次的进度继续游戏。

高级技巧

  1. 使用扩展方法简化操作 为了使 SharedPreferences 的使用更加便捷,可以通过扩展方法来简化代码。例如,定义一个扩展方法来更方便地获取或设置布尔值:
extension SharedPreferencesExtensions on SharedPreferences {
  bool getBoolValue(String key, {bool defaultValue = false}) {
    return getBool(key) ?? defaultValue;
  }

  Future<bool> setBoolValue(String key, bool value) {
    return setBool(key, value);
  }
}

这样在使用时,可以直接通过 SharedPreferences 实例调用这些扩展方法,例如:

Future<void> useExtensions() async {
  final prefs = await SharedPreferences.getInstance();
  bool isDarkMode = prefs.getBoolValue('isDarkMode');
  await prefs.setBoolValue('isDarkMode',!isDarkMode);
}
  1. 结合 Provider 进行状态管理 在大型 Flutter 应用中,通常会使用状态管理库,如 Provider。可以将 SharedPreferences 与 Provider 结合使用,以便更好地管理应用状态。例如,创建一个 SettingsProvider,用于管理用户的设置状态:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SettingsProvider with ChangeNotifier {
  bool _isNightMode = false;

  bool get isNightMode => _isNightMode;

  SettingsProvider() {
    _loadSettings();
  }

  Future<void> _loadSettings() async {
    final prefs = await SharedPreferences.getInstance();
    _isNightMode = prefs.getBool('isNightMode') ?? false;
    notifyListeners();
  }

  Future<void> setNightMode(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isNightMode', value);
    _isNightMode = value;
    notifyListeners();
  }
}

在应用中,可以通过 Provider 来提供 SettingsProvider,以便在不同的组件中方便地获取和更新用户设置:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => SettingsProvider(),
      child: MyApp(),
    ),
  );
}

在组件中,可以通过 Provider 获取 SettingsProvider 并使用其中的方法和状态:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final settingsProvider = Provider.of<SettingsProvider>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Settings Example'),
      ),
      body: Center(
        child: Switch(
          value: settingsProvider.isNightMode,
          onChanged: (value) {
            settingsProvider.setNightMode(value);
          },
        ),
      ),
    );
  }
}
  1. 处理数据迁移 当应用版本更新,数据结构可能需要调整时,需要进行数据迁移。例如,旧版本应用使用字符串存储用户等级,新版本改为使用整数存储。可以在应用启动时进行数据迁移检查:
Future<void> migrateData() async {
  final prefs = await SharedPreferences.getInstance();
  String? oldLevelString = prefs.getString('oldUserLevel');
  if (oldLevelString != null) {
    int newLevel = int.tryParse(oldLevelString) ?? 1;
    await prefs.setInt('userLevel', newLevel);
    await prefs.remove('oldUserLevel');
  }
}

在应用启动时调用 migrateData 方法,就可以将旧版本的数据迁移到新版本的格式。

  1. 数据验证与错误处理 在存储和读取数据时,进行数据验证和错误处理是很重要的。例如,在读取整数时,除了检查是否为 null,还可以验证其是否在合理范围内:
Future<int> getValidatedUserPoints() async {
  final prefs = await SharedPreferences.getInstance();
  int? userPoints = prefs.getInt('userPoints');
  if (userPoints == null || userPoints < 0) {
    return 0;
  }
  return userPoints;
}

在存储数据时,也可以进行一些合法性检查,如存储字符串时检查字符串长度是否符合要求等。

  1. 批量操作优化 如果需要进行多个 SharedPreferences 操作,可以考虑批量处理以提高性能。虽然 SharedPreferences 会在一定程度上对操作进行优化,但对于一些连续的存储操作,可以将它们合并为一个操作。例如:
Future<void> batchSave() async {
  final prefs = await SharedPreferences.getInstance();
  final batch = prefs.edit();
  batch.setBool('isNotificationEnabled', true);
  batch.setInt('userLevel', 5);
  batch.setString('nickname', 'JohnDoe');
  await batch.commit();
}

在上述代码中,通过 prefs.edit() 获取一个 SharedPreferences.Editor 对象,然后可以在这个对象上进行多个设置操作,最后通过 batch.commit() 一次性提交这些操作,这样可以减少与本地存储系统的交互次数,提高性能。

  1. 监听数据变化 虽然 SharedPreferences 本身没有直接提供监听数据变化的机制,但可以通过结合 StreamSharedPreferences 来实现类似的功能。例如,创建一个 Stream 来监听夜间模式设置的变化:
class NightModeStream {
  final StreamController<bool> _controller = StreamController<bool>.broadcast();
  late SharedPreferences _prefs;

  NightModeStream() {
    _init();
  }

  Future<void> _init() async {
    _prefs = await SharedPreferences.getInstance();
    bool isNightMode = _prefs.getBool('isNightMode') ?? false;
    _controller.add(isNightMode);
    // 模拟数据变化监听
    Future.delayed(const Duration(seconds: 5), () async {
      bool newIsNightMode =!isNightMode;
      await _prefs.setBool('isNightMode', newIsNightMode);
      _controller.add(newIsNightMode);
    });
  }

  Stream<bool> get stream => _controller.stream;
}

在使用时,可以订阅这个 Stream 来获取夜间模式设置的变化:

void main() {
  final nightModeStream = NightModeStream();
  nightModeStream.stream.listen((isNightMode) {
    print('Night mode changed to: $isNightMode');
  });
}

这样就可以在数据发生变化时及时收到通知并进行相应的处理。

通过以上对 Flutter 中 SharedPreferences 的详细介绍、代码示例以及各种使用技巧和注意事项,相信开发者能够更加熟练和高效地使用 SharedPreferences 进行简单数据的存储和读取,为应用开发提供更优质的用户体验和数据管理功能。无论是小型应用的简单配置,还是大型应用的用户偏好设置,SharedPreferences 都能发挥重要作用,只要合理运用其特性和技巧,就能满足多种应用场景的需求。同时,在使用过程中要注意数据的安全性、性能优化以及跨平台兼容性等问题,确保应用在不同设备和环境下都能稳定运行。