Flutter键值对存储完全指南:深入理解与实践SharedPreferences
引言:为什么需要轻量级本地存储?
开发移动应用时,我们经常需要把一些数据"记下来"------比如用户是不是登录了、选择了什么主题、或者临时的浏览记录。这些看似简单的需求,背后都需要一个可靠且高效的本地存储方案来支撑。
Flutter 作为跨平台开发框架,给了我们不少存储选择。对于保存简单的配置或状态,SharedPreferences 往往是第一选择。它足够轻量,用起来也简单,就像给应用提供了一个方便的"小笔记本"。
其实 SharedPreferences 并不是 Flutter 独创的,它借鉴了 Android 平台上同名的 API,在 iOS 上对应的则是 NSUserDefaults。Flutter 团队通过 shared_preferences 插件把它们统一封装成了一套 Dart API,让我们能用同样的方式在各个平台上存取键值对数据。
这篇文章会带你深入 SharedPreferences 的工作原理,手把手实践完整的使用示例,并分享一些性能优化和避坑经验。
技术分析:SharedPreferences 是怎么工作的?
1. 架构设计:桥接原生平台
SharedPreferences 的核心在于它的跨平台设计。简单说,Flutter 的 shared_preferences 插件扮演了一个"翻译官"的角色:
Dart层 (shared_preferences) → 平台通道 → Native层
↑ ↓
Flutter应用 Android: SharedPreferences
│ iOS: NSUserDefaults
└────────── 统一API ──────────┘
这样一来,我们写的 Dart 代码就可以通过同一套接口,去调用不同平台底层的存储能力。具体到各个平台:
- Android :数据以 XML 文件形式存储,路径一般是
/data/data/<package_name>/shared_prefs - iOS:使用 plist 文件格式,存放在应用沙盒内
- Web:直接调用浏览器的 localStorage API
- Windows/Linux/Mac:用 JSON 文件来存储
2. 数据存储机制
SharedPreferences 以键值对的形式存储数据,支持以下几种基础类型:
int:整数double:浮点数bool:布尔值String:字符串List<String>:字符串列表
在底层,所有数据最终都会被转换成字符串存起来。不过我们在用的时候,完全不用关心这个过程,因为 API 已经提供了类型安全的读写方法。
3. 线程安全与异步操作
SharedPreferences 的所有写入 操作都是异步的。也就是说,当你调用 set 方法时,它会立刻返回,而实际的磁盘写入会在后台悄悄进行。这样做的好处是避免阻塞 UI 线程,让应用保持流畅。
但这也带来一点需要留意的地方:
- 数据不会立刻写入磁盘,可能会有微小延迟
- 如果应用突然崩溃或退出,最近一次写入的数据可能会丢
- 读取操作通常是同步的,因为数据已经缓存在内存里了
快速开始:集成与基本使用
1. 添加依赖
打开 pubspec.yaml,加入依赖:
yaml
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.2.2
然后运行 flutter pub get 安装即可。
2. 基本 API 速览
SharedPreferences 的 API 设计得很直观:
dart
// 获取实例
final prefs = await SharedPreferences.getInstance();
// 写数据
await prefs.setInt('counter', 10);
await prefs.setString('username', 'John');
await prefs.setBool('isDarkMode', true);
// 读数据
int? counter = prefs.getInt('counter');
String? username = prefs.getString('username');
bool? isDarkMode = prefs.getBool('isDarkMode');
// 删数据
await prefs.remove('counter');
// 清空全部
await prefs.clear();
// 检查是否存在某个 key
bool hasKey = prefs.containsKey('username');
3. 初始化与单例模式
SharedPreferences 本身是单例,但为了方便管理,我们通常会再封装一层:
dart
class PreferencesService {
static late final SharedPreferences _prefs;
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
static SharedPreferences get instance => _prefs;
}
代码实现:一个完整的示例应用
下面我们一步步构建一个实际可运行的应用,把上面的概念都串起来。
1. 应用入口与初始化
dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
// 确保 Flutter 引擎初始化完成
WidgetsFlutterBinding.ensureInitialized();
// 初始化 SharedPreferences
await PreferencesService.init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SharedPreferences Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const PreferencesDemo(),
debugShowCheckedModeBanner: false,
);
}
}
2. 服务层封装
我们把 SharedPreferences 的操作封装到一个服务类里,这样业务逻辑会更清晰:
dart
/// SharedPreferences 服务封装
class PreferencesService {
static late final SharedPreferences _instance;
/// 初始化
static Future<void> init() async {
try {
_instance = await SharedPreferences.getInstance();
print('SharedPreferences 初始化成功');
} catch (e) {
print('SharedPreferences 初始化失败: $e');
// 这里可以根据需要添加降级逻辑,比如改用内存缓存
rethrow;
}
}
/// 获取实例
static SharedPreferences get instance => _instance;
/// 保存用户设置(多个字段一起保存)
static Future<bool> saveUserSettings({
required String username,
required bool isDarkMode,
required int themeColor,
required double fontSize,
}) async {
try {
final results = await Future.wait([
_instance.setString('username', username),
_instance.setBool('isDarkMode', isDarkMode),
_instance.setInt('themeColor', themeColor),
_instance.setDouble('fontSize', fontSize),
]);
// 检查是否全部成功
return results.every((result) => result == true);
} catch (e) {
print('保存用户设置失败: $e');
return false;
}
}
/// 读取用户设置
static UserSettings getUserSettings() {
return UserSettings(
username: _instance.getString('username') ?? 'Guest',
isDarkMode: _instance.getBool('isDarkMode') ?? false,
themeColor: _instance.getInt('themeColor') ?? Colors.blue.value,
fontSize: _instance.getDouble('fontSize') ?? 14.0,
);
}
/// 清除全部数据
static Future<bool> clearAll() async {
try {
return await _instance.clear();
} catch (e) {
print('清除数据失败: $e');
return false;
}
}
/// 获取存储概况
static StorageStats getStorageStats() {
final allKeys = _instance.getKeys();
return StorageStats(
totalKeys: allKeys.length,
keys: allKeys.toList(),
);
}
}
/// 用户设置的数据模型
class UserSettings {
final String username;
final bool isDarkMode;
final int themeColor;
final double fontSize;
UserSettings({
required this.username,
required this.isDarkMode,
required this.themeColor,
required this.fontSize,
});
@override
String toString() {
return 'UserSettings{用户名: $username, 深色模式: $isDarkMode, 主题色: $themeColor, 字体大小: $fontSize}';
}
}
/// 存储统计信息
class StorageStats {
final int totalKeys;
final List<String> keys;
StorageStats({
required this.totalKeys,
required this.keys,
});
}
3. UI 界面实现
界面部分主要负责显示和交互,这里我们做一个设置页面:
dart
class PreferencesDemo extends StatefulWidget {
const PreferencesDemo({Key? key}) : super(key: key);
@override
_PreferencesDemoState createState() => _PreferencesDemoState();
}
class _PreferencesDemoState extends State<PreferencesDemo> {
late UserSettings _userSettings;
late TextEditingController _usernameController;
late TextEditingController _fontSizeController;
bool _isSaving = false;
String _saveResult = '';
@override
void initState() {
super.initState();
_loadSettings();
_usernameController = TextEditingController();
_fontSizeController = TextEditingController();
}
@override
void dispose() {
_usernameController.dispose();
_fontSizeController.dispose();
super.dispose();
}
/// 加载设置
void _loadSettings() {
setState(() {
_userSettings = PreferencesService.getUserSettings();
_usernameController.text = _userSettings.username;
_fontSizeController.text = _userSettings.fontSize.toString();
});
}
/// 保存设置
Future<void> _saveSettings() async {
if (_isSaving) return;
setState(() {
_isSaving = true;
_saveResult = '保存中...';
});
try {
final username = _usernameController.text.isNotEmpty
? _usernameController.text
: 'Guest';
final fontSize = double.tryParse(_fontSizeController.text) ?? 14.0;
final success = await PreferencesService.saveUserSettings(
username: username,
isDarkMode: _userSettings.isDarkMode,
themeColor: _userSettings.themeColor,
fontSize: fontSize,
);
setState(() {
_saveResult = success ? '保存成功!' : '保存失败';
if (success) {
_loadSettings(); // 刷新显示
}
});
} catch (e) {
setState(() {
_saveResult = '保存出错: $e';
});
} finally {
setState(() {
_isSaving = false;
});
// 3秒后自动清除提示
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_saveResult = '';
});
}
});
}
}
/// 切换深色模式
void _toggleDarkMode() async {
final success = await PreferencesService.instance.setBool(
'isDarkMode',
!_userSettings.isDarkMode,
);
if (success && mounted) {
_loadSettings();
}
}
/// 清除所有数据(带确认提示)
Future<void> _clearAllData() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认清除'),
content: const Text('确定要清除所有存储的数据吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('清除', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
final success = await PreferencesService.clearAll();
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('数据已清除')),
);
_loadSettings();
}
}
}
@override
Widget build(BuildContext context) {
final stats = PreferencesService.getStorageStats();
return Scaffold(
appBar: AppBar(
title: const Text('SharedPreferences 演示'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('存储统计'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('总键值对数量: ${stats.totalKeys}'),
const SizedBox(height: 8),
const Text('当前存储的键:'),
...stats.keys.map((key) => Text(' • $key')).toList(),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
),
);
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
// 用户设置输入区
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'用户设置',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: '用户名',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 16),
TextField(
controller: _fontSizeController,
decoration: const InputDecoration(
labelText: '字体大小',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.text_fields),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('深色模式'),
value: _userSettings.isDarkMode,
onChanged: (value) => _toggleDarkMode(),
secondary: Icon(
_userSettings.isDarkMode
? Icons.dark_mode
: Icons.light_mode,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isSaving ? null : _saveSettings,
icon: _isSaving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.save),
label: Text(_isSaving ? '保存中...' : '保存设置'),
),
),
if (_saveResult.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_saveResult,
style: TextStyle(
color: _saveResult.contains('成功')
? Colors.green
: Colors.red,
),
),
),
],
),
),
),
const SizedBox(height: 16),
// 当前设置展示区
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'当前设置',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildInfoRow('用户名', _userSettings.username),
_buildInfoRow('深色模式', _userSettings.isDarkMode.toString()),
_buildInfoRow('主题色', '#${_userSettings.themeColor.toRadixString(16)}'),
_buildInfoRow('字体大小', '${_userSettings.fontSize}pt'),
],
),
),
),
const SizedBox(height: 16),
// 危险操作区
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _clearAllData,
icon: const Icon(Icons.delete_outline, color: Colors.red),
label: const Text(
'清除所有数据',
style: TextStyle(color: Colors.red),
),
),
),
const SizedBox(height: 8),
const Text(
'注意:清除后数据无法恢复',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
),
// 使用提示
const SizedBox(height: 24),
const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'使用提示',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text('• SharedPreferences 适合存储简单的键值对数据'),
SizedBox(height: 4),
Text('• 不适合存储大量数据或复杂对象'),
SizedBox(height: 4),
Text('• 数据会持久化到设备本地'),
SizedBox(height: 4),
Text('• 应用卸载后数据会被清除'),
],
),
),
),
],
),
),
);
}
// 辅助方法:构建信息展示行
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
SizedBox(
width: 80,
child: Text(
label,
style: const TextStyle(color: Colors.grey),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
性能优化与最佳实践
1. 注意数据大小限制
SharedPreferences 不是为大数据设计的,使用时最好遵循以下约定:
- 单个键值对别超过 1MB
- 总数据量控制在 10MB 以内
- 键(key)的名字尽量短且有意义
2. 优化写入频率
频繁写入会影响性能。如果一次要改多个值,尽量集中操作。虽然 SharedPreferences 没有显式的批量提交 API,但我们可以自己组织代码,减少不必要的写入。
3. 做好错误处理和降级
存储有可能失败(比如磁盘空间不足),做好错误处理很重要:
dart
class SafePreferences {
final SharedPreferences _prefs;
final Map<String, dynamic> _memoryCache = {}; // 内存降级缓存
SafePreferences(this._prefs);
Future<bool> safeSetString(String key, String value) async {
try {
return await _prefs.setString(key, value);
} catch (e) {
// 磁盘写入失败时,先缓存在内存里
_memoryCache[key] = value;
print('SharedPreferences 写入失败,暂存到内存: $e');
return false;
}
}
String? safeGetString(String key) {
try {
return _prefs.getString(key) ?? _memoryCache[key];
} catch (e) {
print('SharedPreferences 读取失败: $e');
return _memoryCache[key];
}
}
}
4. 考虑类型安全的封装
如果你希望代码更健壮,可以封装一个类型安全的版本:
dart
/// 类型安全的 Preferences 封装
class TypedPreferences {
final SharedPreferences _prefs;
TypedPreferences(this._prefs);
T get<T>(String key, T defaultValue) {
try {
if (T == int) {
return (_prefs.getInt(key) as T?) ?? defaultValue;
} else if (T == double) {
return (_prefs.getDouble(key) as T?) ?? defaultValue;
} else if (T == bool) {
return (_prefs.getBool(key) as T?) ?? defaultValue;
} else if (T == String) {
return (_prefs.getString(key) as T?) ?? defaultValue;
} else if (T == List<String>) {
return (_prefs.getStringList(key) as T?) ?? defaultValue;
}
return defaultValue;
} catch (e) {
return defaultValue;
}
}
}
5. 调试与检查
开发时,可以快速查看 SharedPreferences 里存了什么:
dart
/// 调试小工具
class PreferencesDebugger {
static void printAll(SharedPreferences prefs) {
print('=== SharedPreferences 内容 ===');
prefs.getKeys().forEach((key) {
final value = prefs.get(key);
print('$key: $value (${value.runtimeType})');
});
print('============================');
}
}
总结
SharedPreferences 是 Flutter 里最简单直接的本地存储方案,在合适的场景下非常好用。通过上面的介绍和实践,我们可以总结出以下几点:
1. 它适合做什么?
- 保存用户偏好设置(主题、语言、字号等)
- 记录简单的应用状态(比如是否第一次启动)
- 缓存登录令牌(注意要加密)
- 临时保存用户输入
2. 它不适合做什么?
- 存储大量结构化数据(考虑用 SQLite 或 Sembast)
- 存复杂对象(可以考虑 Hive 或 ObjectBox)
- 需要频繁读写大量数据的场景
- 需要复杂查询的情况
3. 主要优点
- 简单:API 直观,上手快
- 跨平台:一套代码,多端运行
- 开箱即用:不需要额外配置
- 轻量:对应用体积影响小
4. 需要注意的
- 数据默认是明文存储,敏感信息记得加密
- 写入是异步的,重要操作最好确认一下
- 不同平台的底层实现略有差异,需要测试
- 应用卸载时,数据会被一起清理
5. 最后一点建议
在实际项目中,根据需求选择合适的存储方案很重要。SharedPreferences 在它擅长的领域------也就是简单的键值对存储------表现非常出色。对于更复杂的需求,你可能需要结合其他存储方案一起使用。
好的架构设计、适当的封装和严谨的错误处理,能让 SharedPreferences 成为你应用中一个可靠的数据持久化工具。记住,没有完美的存储方案,只有最适合当前场景的选择。