1. 本地存储的核心价值与选型思路
在移动应用开发中,本地存储如同一个可靠的记事本,它让应用能够记住用户的操作习惯和重要信息。想象一下,每次打开应用都需要重新登录,或者搜索历史无法保存,这样的体验多么令人沮丧。本地存储正是为了解决这些问题而生。
1.1 为什么需要本地存储
本地存储主要解决三大核心问题:
- 状态持久化:用户登录后的token、个性化设置等需要长期保存的数据
- 离线可用性:在网络不可用时仍能提供基础功能体验
- 性能优化:减少网络请求,快速加载常用数据
在我们的二手物品置换App中,本地存储的使用场景包括:
- 用户认证:保存登录token,避免频繁登录
- 个性化设置:记住用户的偏好设置(如通知开关)
- 搜索历史:记录最近10条搜索关键词
- 草稿保存:商品发布中途退出时可恢复
- 浏览记录:保存用户查看过的商品信息
1.2 Flutter中的存储方案对比
Flutter生态中有多种本地存储方案,我们需要根据数据类型和复杂度选择合适的工具:
| 方案 | 适用场景 | 优点 | 缺点 | 性能 |
|---|---|---|---|---|
| shared_preferences | 简单键值对 | 官方维护,使用简单 | 仅支持基础类型 | 一般 |
| Hive | 结构化对象 | 支持自定义类型,高性能 | 需要代码生成 | 优秀 |
| SQLite | 关系型数据 | 复杂查询能力 | 使用较复杂 | 良好 |
| 文件存储 | 大文件/二进制 | 灵活 | 需要手动管理 | 取决于文件大小 |
对于我们的App:
- 使用shared_preferences存储:token、设置项等简单数据
- 采用Hive存储:浏览历史、收藏列表等结构化数据
- 草稿数据:虽然可以用Hive,但考虑到数据结构和shared_preferences的JSON支持,两种方案都可选
提示:选择存储方案时,除了考虑数据类型,还要注意数据量大小。shared_preferences适合小数据量(一般不超过1MB),大数据量建议使用Hive或SQLite。
2. shared_preferences深度实践
2.1 基础配置与初始化
首先在pubspec.yaml中添加依赖:
yaml复制dependencies:
shared_preferences: ^2.2.2
执行flutter pub get后,我们需要在App启动时初始化存储:
dart复制void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Storage.init();
runApp(const MyApp());
}
这里有两个关键点:
WidgetsFlutterBinding.ensureInitialized():这是调用任何插件前的必要操作,它建立了与Flutter引擎的连接- 使用
await确保存储初始化完成后再启动App,避免后续读取数据时出现空指针异常
2.2 存储工具类封装
优秀的封装能让代码更易维护。我们创建Storage类统一管理所有存储操作:
dart复制import 'package:shared_preferences/shared_preferences.dart';
class Storage {
static SharedPreferences? _prefs;
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// Token操作
static Future<void> setToken(String token) async {
await _prefs?.setString('token', token);
}
static String? getToken() {
return _prefs?.getString('token');
}
// 用户信息(JSON格式)
static Future<void> setUserInfo(Map<String, dynamic> userInfo) async {
await _prefs?.setString('userInfo', jsonEncode(userInfo));
}
static Map<String, dynamic>? getUserInfo() {
final json = _prefs?.getString('userInfo');
return json != null ? jsonDecode(json) : null;
}
}
封装时注意:
- 使用静态方法和变量,避免重复创建实例
- 对复杂数据类型(如Map)进行JSON序列化
- 所有方法都处理null安全,避免崩溃
2.3 搜索历史的特殊处理
搜索历史需要实现:去重、按时间倒序、数量限制(10条)。这是典型的队列操作:
dart复制static Future<void> addSearchHistory(String keyword) async {
final history = getSearchHistory();
history.remove(keyword); // 去重
history.insert(0, keyword); // 新搜索置顶
if (history.length > 10) {
history.removeLast(); // 限制数量
}
await setSearchHistory(history);
}
这段代码的亮点:
remove+insert组合实现去重和排序- 数量限制避免存储空间无限增长
- 操作都在内存中完成,最后一次性写入磁盘
2.4 设置项的默认值处理
对于开关设置,合理的默认值能提升用户体验:
dart复制static bool getNotificationEnabled() {
return _prefs?.getBool('notificationEnabled') ?? true;
}
static bool getVibrationEnabled() {
return _prefs?.getBool('vibrationEnabled') ?? false;
}
设计原则:
- 通知类:默认开启(用户期望接收)
- 震动类:默认关闭(避免打扰)
- 其他设置:根据产品需求决定
3. Hive存储复杂数据
当需要存储自定义对象时,shared_preferences就显得力不从心了。这时Hive是更好的选择。
3.1 初始配置
首先添加依赖:
yaml复制dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
dev_dependencies:
hive_generator: ^2.0.1
build_runner: ^2.4.6
Hive需要为每个数据模型生成适配器。我们以商品模型为例:
dart复制import 'package:hive/hive.dart';
part 'product.g.dart';
@HiveType(typeId: 0)
class Product {
@HiveField(0)
final int id;
@HiveField(1)
final String title;
@HiveField(2)
final double price;
Product({
required this.id,
required this.title,
required this.price,
});
}
运行生成命令:
bash复制flutter pub run build_runner build
3.2 浏览历史服务
封装一个浏览历史管理服务:
dart复制class BrowseHistoryService {
static const _boxName = 'browseHistory';
static Future<Box<Product>> _getBox() async {
return await Hive.openBox<Product>(_boxName);
}
static Future<void> add(Product product) async {
final box = await _getBox();
await box.put(product.id, product); // 使用id作为key自动去重
}
static Future<List<Product>> getRecent(int limit) async {
final box = await _getBox();
return box.values.take(limit).toList();
}
}
关键点:
- 每个Box相当于一个表,需要唯一名称
put方法使用商品id作为key,天然去重- 读取时使用
take限制数量,避免内存问题
3.3 性能优化技巧
Hive虽然快,但不当使用仍会导致性能问题:
- 批量操作:避免频繁打开关闭Box
dart复制// 错误示范:每次操作都打开Box
void addProduct(Product p) async {
final box = await Hive.openBox('products');
await box.put(p.id, p);
await box.close();
}
// 正确做法:保持Box打开
late final Box productBox;
void init() async {
productBox = await Hive.openBox('products');
}
void addProduct(Product p) {
productBox.put(p.id, p);
}
- 分页加载:大数据集不要一次性读取
dart复制static Future<List<Product>> getProducts(int page, int pageSize) async {
final box = await _getBox();
return box.values
.skip((page - 1) * pageSize)
.take(pageSize)
.toList();
}
4. 实战中的经验与陷阱
4.1 异步操作的正确处理
本地存储基本都是异步操作,常见错误是忽略await:
dart复制// 错误:没有await导致后续读取为空
void saveToken() {
Storage.setToken('new_token');
print('保存完成'); // 实际上可能还未完成
}
// 正确:使用await确保顺序执行
void saveToken() async {
await Storage.setToken('new_token');
print('保存完成'); // 确保此时已保存
}
4.2 数据迁移策略
当数据结构需要变更时,如何平滑迁移?
对于shared_preferences:
- 版本控制:存储一个version字段
- 升级时检查版本号
- 按需转换旧数据格式
dart复制static Future<void> migrate() async {
final version = _prefs?.getInt('data_version') ?? 1;
if (version < 2) {
// 从v1迁移到v2
final oldData = _prefs?.getString('old_format');
if (oldData != null) {
await _prefs?.setString('new_format', convertFormat(oldData));
await _prefs?.remove('old_format');
}
await _prefs?.setInt('data_version', 2);
}
}
对于Hive:
- 使用Hive的迁移API
- 注册适配器时指定版本
- 实现迁移逻辑
dart复制Hive.registerAdapter(
ProductAdapter(),
version: 2,
migration: (oldVersion, newVersion) {
// 迁移逻辑
},
);
4.3 安全注意事项
虽然本地存储很方便,但安全问题不容忽视:
- 敏感信息加密:token等需要加密存储
dart复制static Future<void> setToken(String token) async {
final encrypted = encrypt(token); // 使用加密库
await _prefs?.setString('token', encrypted);
}
- 数据清理:退出登录时要彻底清除
dart复制static Future<void> clearOnLogout() async {
await _prefs?.remove('token');
await _prefs?.remove('userInfo');
// 不要使用clear(),会删除所有设置项
}
- Hive文件保护:可以设置密码
dart复制void init() async {
final dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
await Hive.openBox('secure_box', encryptionCipher: HiveAesCipher('密码'));
}
5. 调试与性能监控
5.1 查看存储内容
调试时查看实际存储内容很有帮助:
对于shared_preferences:
dart复制void printAllPrefs() {
_prefs?.getKeys().forEach((key) {
print('$key: ${_prefs?.get(key)}');
});
}
对于Hive:
dart复制void printBoxContents() async {
final box = await Hive.openBox('test_box');
box.toMap().forEach((key, value) {
print('$key: $value');
});
}
5.2 性能监控
使用dart:developer记录关键操作耗时:
dart复制import 'dart:developer' as developer;
void saveData() async {
final stopwatch = Stopwatch()..start();
// 存储操作
await Storage.setLargeData(bigData);
developer.log('存储耗时: ${stopwatch.elapsedMilliseconds}ms');
}
5.3 常见问题排查
-
数据读取为空:
- 检查是否await初始化
- 确认写入和读取的key一致
- 检查是否在其他地方清除了数据
-
Hive报错"Box not found":
- 确保Box已经打开
- 检查Box名称拼写
- 确认初始化流程正确
-
Android上shared_preferences失效:
- 检查是否在后台被系统清理
- 确认没有使用
clear()意外删除数据 - 测试不同Android版本的表现
在实现本地存储功能时,我最大的体会是:看似简单的功能,要做得健壮可靠需要充分考虑各种边界情况。特别是异步操作的处理和数据一致性的保证,稍不注意就会引入难以发现的bug。建议在开发过程中:
- 为所有存储操作编写单元测试
- 模拟极端场景(如磁盘已满)
- 记录关键操作的性能数据
- 制定清晰的数据迁移策略
本地存储作为App的"记忆"组件,其稳定性和性能直接影响用户体验。通过合理的架构设计和细致的错误处理,我们可以为用户打造流畅、可靠的数据持久化方案。