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,加入依赖:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.2

然后运行 flutter pub get 安装即可。

2. 基本 API 速览

SharedPreferences 的 API 设计得很直观:

// 获取实例
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 本身是单例,但为了方便管理,我们通常会再封装一层:

class PreferencesService {
  static late final SharedPreferences _prefs;
  
  static Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }
  
  static SharedPreferences get instance => _prefs;
}

代码实现:一个完整的示例应用

下面我们一步步构建一个实际可运行的应用,把上面的概念都串起来。

1. 应用入口与初始化

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 的操作封装到一个服务类里,这样业务逻辑会更清晰:

/// 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 界面实现

界面部分主要负责显示和交互,这里我们做一个设置页面:

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. 做好错误处理和降级

存储有可能失败(比如磁盘空间不足),做好错误处理很重要:

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. 考虑类型安全的封装

如果你希望代码更健壮,可以封装一个类型安全的版本:

/// 类型安全的 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 里存了什么:

/// 调试小工具
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 成为你应用中一个可靠的数据持久化工具。记住,没有完美的存储方案,只有最适合当前场景的选择。

Logo

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

更多推荐