
本地存储用于保存用户数据、缓存信息、应用设置等,让用户下次打开App时能恢复之前的状态。今天我们来讲解本地存储的实现方式。
本地存储的使用场景
在我们的App中,本地存储主要用于:保存登录token、保存用户设置、缓存搜索历史、保存草稿等。这些数据不需要每次都从服务器获取,存在本地可以加快App启动速度,也能在离线状态下使用。
使用shared_preferences
shared_preferences是Flutter官方推荐的轻量级存储方案,适合存储简单的键值对数据。
yaml
# pubspec.yaml
dependencies:
shared_preferences: ^2.2.2
此处为pubspec.yaml文件的依赖配置,引入shared_preferences包并指定版本号为2.2.2。该依赖是Flutter官方维护的轻量级存储库,能适配iOS、Android及OpenHarmony多端,通过简单的键值对形式存储基础数据类型,是本地轻量存储的首选方案。配置完成后需执行flutter pub get命令拉取依赖包,才能在代码中引入使用。
这个依赖包在iOS上使用NSUserDefaults,在Android上使用SharedPreferences,在OpenHarmony上也有对应的实现。添加依赖后执行flutter pub get即可使用。
封装存储类
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);
}
首先引入shared_preferences库,然后定义一个名为Storage的静态工具类。类中声明了静态的SharedPreferences实例变量_prefs,init方法用于初始化该实例,通过SharedPreferences.getInstance()异步获取实例并赋值给_prefs。setToken方法则是封装了token的存储逻辑,接收字符串类型的token,调用_prefs的setString方法将其以键名'token'存储,保证token数据的持久化。
dart
static String? getToken() {
return _prefs?.getString('token');
}
static Future<void> removeToken() async {
await _prefs?.remove('token');
}
// 用户信息
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');
if (json != null) {
return jsonDecode(json);
}
return null;
}
getToken方法用于读取存储的token,通过_prefs的getString方法根据键名'token'获取值,返回值为可空字符串类型。removeToken方法则是移除存储的token数据。针对用户信息,由于SharedPreferences不直接支持存储Map类型,setUserInfo方法先通过jsonEncode将Map转为JSON字符串,再以键名'userInfo'存储;getUserInfo方法则读取字符串后通过jsonDecode转回Map,实现复杂用户信息的存储与读取。
dart
// 搜索历史
static Future<void> setSearchHistory(List<String> history) async {
await _prefs?.setStringList('searchHistory', history);
}
static List<String> getSearchHistory() {
return _prefs?.getStringList('searchHistory') ?? [];
}
static Future<void> addSearchHistory(String keyword) async {
final history = getSearchHistory();
history.remove(keyword); // 去重
history.insert(0, keyword); // 插入到最前面
setSearchHistory和getSearchHistory方法封装了搜索历史列表的存储与读取,利用SharedPreferences的getStringList方法直接操作字符串列表,getSearchHistory方法设置了空列表作为默认值,避免空指针问题。addSearchHistory方法是核心逻辑,先获取当前搜索历史,移除重复的关键词实现去重,再将新关键词插入到列表首位,保证最新搜索的关键词排在最前面。
dart
if (history.length > 10) {
history.removeLast(); // 最多保存10条
}
await setSearchHistory(history);
}
static Future<void> clearSearchHistory() async {
await _prefs?.remove('searchHistory');
}
// 设置项
static Future<void> setNotificationEnabled(bool enabled) async {
await _prefs?.setBool('notificationEnabled', enabled);
}
继续完善addSearchHistory方法,判断列表长度是否超过10,若超过则移除最后一条数据,限制搜索历史最多保存10条,避免数据过多占用存储。clearSearchHistory方法用于清空搜索历史,通过remove方法删除'searchHistory'对应的键值对。后续开始封装应用设置项的存储,setNotificationEnabled方法用于存储消息通知开关状态,利用setBool方法保存布尔值。
dart
static bool getNotificationEnabled() {
return _prefs?.getBool('notificationEnabled') ?? true;
}
static Future<void> setSoundEnabled(bool enabled) async {
await _prefs?.setBool('soundEnabled', enabled);
}
static bool getSoundEnabled() {
return _prefs?.getBool('soundEnabled') ?? true;
}
static Future<void> setVibrationEnabled(bool enabled) async {
getNotificationEnabled方法读取消息通知开关状态,设置默认值为true,保证未配置时默认开启通知。接着封装声音开关的存储与读取方法setSoundEnabled和getSoundEnabled,逻辑与通知开关一致,默认值也为true。随后开始封装震动开关的存储方法setVibrationEnabled,同样使用setBool方法存储布尔类型的开关状态。
dart
await _prefs?.setBool('vibrationEnabled', enabled);
}
static bool getVibrationEnabled() {
return _prefs?.getBool('vibrationEnabled') ?? false;
}
// 清除所有数据
static Future<void> clear() async {
await _prefs?.clear();
}
}
getVibrationEnabled方法读取震动开关状态,默认值设为false,符合多数应用震动默认关闭的交互习惯。最后定义clear方法,调用SharedPreferences的clear方法清除所有存储的键值对,适用于用户退出登录等需要清空本地数据的场景。整个Storage类封装完成,所有方法均为静态,无需创建实例即可调用,简化使用流程。
我们把SharedPreferences封装成一个静态工具类,这样调用时直接Storage.getToken()就行,不用每次都获取实例。类中定义了token、用户信息、搜索历史、设置项等常用存储方法。搜索历史的处理比较有意思,新搜索的关键词会插入到最前面,重复的会先删除再插入,最多保存10条。设置项都提供了默认值,比如通知默认开启、震动默认关闭。
初始化
在App启动时初始化:
dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Storage.init();
runApp(const MyApp());
}
main函数作为App入口,首先通过async标记为异步函数。WidgetsFlutterBinding.ensureInitialized()确保Flutter引擎完成初始化,这是调用异步方法前的必要操作,否则会导致异步调用失败。接着await等待Storage.init()完成初始化,保证SharedPreferences实例加载完成后再启动MyApp,避免后续读取数据时出现实例未初始化的问题,最后调用runApp启动应用。
WidgetsFlutterBinding.ensureInitialized()这行代码确保Flutter引擎初始化完成,在调用任何异步方法之前必须先调用它。Storage.init()是异步的,需要await等待完成后再启动App,否则后续读取数据可能会出错。
在搜索页面使用
dart
class _SearchPageState extends State<SearchPage> {
List<String> _historySearches = [];
@override
void initState() {
super.initState();
_historySearches = Storage.getSearchHistory();
}
void _search(String keyword) {
if (keyword.isEmpty) return;
// 保存搜索历史
Storage.addSearchHistory(keyword);
在搜索页面的状态类中,定义_historySearches列表存储搜索历史数据。initState生命周期方法中,调用Storage.getSearchHistory()读取本地存储的搜索历史并赋值给列表,实现页面初始化时加载历史记录。_search方法接收搜索关键词,先判断关键词是否为空,为空则直接返回,非空则调用Storage.addSearchHistory()保存关键词到本地,完成搜索历史的实时存储。
dart
setState(() {
_historySearches = Storage.getSearchHistory();
_isSearching = true;
// 执行搜索...
});
}
void _clearHistory() {
Storage.clearSearchHistory();
setState(() {
_historySearches = [];
});
}
}
调用setState更新页面状态,重新获取最新的搜索历史赋值给_historySearches,同时标记_isSearching为true(可用于控制搜索中状态展示),后续可补充实际的搜索逻辑。_clearHistory方法用于清空搜索历史,先调用Storage.clearSearchHistory()删除本地存储的历史数据,再通过setState将_historySearches置为空列表,实时更新页面展示的历史记录。
搜索页面在initState中从本地读取历史记录并显示。用户每次搜索时,关键词会自动保存到本地,下次打开App还能看到之前搜过什么。清空历史时调用clearSearchHistory方法,同时更新UI状态。
在设置页面使用
dart
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool _notificationEnabled = true;
bool _soundEnabled = true;
bool _vibrationEnabled = false;
@override
void initState() {
super.initState();
_loadSettings();
}
设置页面首先定义为有状态组件,在状态类中声明三个布尔类型变量,分别对应消息通知、声音、震动的开关状态,并赋予初始值。initState方法中调用_loadSettings()方法,用于从本地加载已保存的设置项,保证页面初始化时展示用户之前配置的状态,而非默认初始值。
dart
void _loadSettings() {
setState(() {
_notificationEnabled = Storage.getNotificationEnabled();
_soundEnabled = Storage.getSoundEnabled();
_vibrationEnabled = Storage.getVibrationEnabled();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
SwitchListTile(
title: const Text('消息通知'),
value: _notificationEnabled,
_loadSettings方法通过setState更新页面状态,分别调用Storage的get方法读取消息通知、声音、震动的开关状态,并赋值给对应的变量,完成本地设置的加载。build方法构建设置页面布局,使用Scaffold作为根组件,AppBar展示标题,body使用ListView承载设置项,第一个SwitchListTile为消息通知开关,绑定_notificationEnabled的值作为开关状态。
dart
onChanged: (value) {
setState(() => _notificationEnabled = value);
Storage.setNotificationEnabled(value);
},
),
SwitchListTile(
title: const Text('声音'),
value: _soundEnabled,
onChanged: (value) {
setState(() => _soundEnabled = value);
Storage.setSoundEnabled(value);
},
),
消息通知开关的onChanged回调中,先通过setState更新_notificationEnabled的值,让开关UI实时响应切换操作,再调用Storage.setNotificationEnabled()将新的开关状态保存到本地,实现设置的持久化。接着定义声音开关的SwitchListTile,逻辑与消息通知开关一致,绑定_soundEnabled变量,切换时更新UI并保存状态到本地。
dart
SwitchListTile(
title: const Text('震动'),
value: _vibrationEnabled,
onChanged: (value) {
setState(() => _vibrationEnabled = value);
Storage.setVibrationEnabled(value);
},
),
],
),
);
}
}
最后定义震动开关的SwitchListTile,绑定_vibrationEnabled变量,onChanged回调中先更新UI状态,再调用Storage.setVibrationEnabled()保存震动开关状态到本地。整个设置页面通过SwitchListTile实现开关组件的展示与交互,先更新UI再保存数据的方式,保证用户操作时的流畅体验,避免等待存储操作完成导致的UI卡顿。
设置页面用SwitchListTile展示开关选项,页面加载时从本地读取设置值。用户切换开关时,先更新UI状态让开关立即响应,然后异步保存到本地。这种先更新UI再保存的方式能让用户感觉操作很流畅。
使用Hive存储复杂数据
如果需要存储复杂的对象数据,可以用hive:
yaml
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
dev_dependencies:
hive_generator: ^2.0.1
build_runner: ^2.4.6
此处为Hive存储的依赖配置,hive是核心库,提供NoSQL数据库功能;hive_flutter是适配Flutter的扩展库;hive_generator用于生成对象序列化/反序列化代码,build_runner是代码生成的构建工具,需放在dev_dependencies中。Hive相比shared_preferences更适合存储复杂对象,性能更优,适配结构化数据存储场景。
Hive是一个轻量级的NoSQL数据库,比shared_preferences更强大。它支持存储自定义对象,性能也更好,适合存储浏览历史、收藏列表这类结构化数据。
定义数据模型:
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;
首先引入hive库,part指令关联自动生成的product.g.dart文件,该文件包含序列化相关代码。通过@HiveType注解标记Product类为Hive可存储的对象,typeId设为0(需保证全局唯一)。类中字段通过@HiveField注解标记,括号内的数字为字段唯一标识,用于序列化/反序列化映射,id字段为商品唯一标识,title为商品标题,均为必填参数。
dart
@HiveField(2)
final double price;
@HiveField(3)
final String image;
Product({
required this.id,
required this.title,
required this.price,
required this.image,
});
}
继续完善Product数据模型,price字段存储商品价格(浮点型),image字段存储商品图片路径/链接(字符串型),分别标记对应的HiveField标识。构造方法中所有字段均为required,保证实例化时必须传入完整的商品信息,满足结构化数据存储的完整性要求。定义完成后需执行flutter pub run build_runner build命令生成序列化代码。
用注解标记类和字段,typeId是类的唯一标识,HiveField的数字是字段的唯一标识。定义好后运行flutter pub run build_runner build命令,会自动生成product.g.dart文件,里面包含序列化和反序列化的代码。
初始化和使用:
dart
void main() async {
await Hive.initFlutter();
Hive.registerAdapter(ProductAdapter());
runApp(const MyApp());
}
// 保存浏览历史
class BrowseHistoryService {
static const _boxName = 'browseHistory';
在App入口main函数中,先调用Hive.initFlutter()初始化Hive,再注册ProductAdapter(生成的序列化适配器),确保Hive能识别并处理Product对象。接着定义BrowseHistoryService类封装浏览历史的存储逻辑,_boxName常量指定Hive的Box名称,Box相当于数据库的表,用于隔离不同类型的数据存储。
dart
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);
}
static Future<List<Product>> getAll() async {
final box = await _getBox();
return box.values.toList();
}
_getBox方法封装打开Box的逻辑,异步获取指定名称的Product类型Box,避免重复打开Box的冗余操作。add方法接收Product对象,先获取Box实例,再通过put方法以商品id为键、商品对象为值存储数据,利用id作为键可自动实现去重,同一商品多次浏览仅保留最新记录。getAll方法读取Box中所有商品对象,转为列表返回,用于展示全部浏览历史。
dart
static Future<void> clear() async {
final box = await _getBox();
await box.clear();
}
}
clear方法用于清空浏览历史,获取Box实例后调用clear方法删除所有存储的Product对象,适用于用户手动清空浏览记录的场景。整个BrowseHistoryService类封装了Hive存储浏览历史的核心逻辑,方法均为静态,调用简洁,且基于商品id去重,符合浏览历史的使用需求。
Hive用Box来存储数据,类似于数据库的表。openBox打开一个Box,put方法存入数据,values获取所有数据。用商品ID作为key,可以自动去重,同一个商品多次浏览只保存一条记录。
草稿保存
发布页面可以保存草稿:
dart
class DraftService {
static Future<void> saveDraft({
required String title,
required String description,
required double? price,
required String category,
required List<String> images,
}) async {
final draft = {
'title': title,
'description': description,
'price': price,
'category': category,
'images': images,
定义DraftService类封装发布草稿的存储逻辑,saveDraft方法为异步方法,接收标题、描述、价格、分类、图片列表等发布商品的核心参数,参数均为required保证完整性(价格为可空浮点型,适配未填写价格的场景)。方法内部将参数封装为Map类型的draft对象,包含各字段的键值对,便于后续序列化为JSON字符串。
dart
'savedAt': DateTime.now().toIso8601String(),
};
final prefs = await SharedPreferences.getInstance();
await prefs.setString('publishDraft', jsonEncode(draft));
}
static Future<Map<String, dynamic>?> getDraft() async {
final prefs = await SharedPreferences.getInstance();
final json = prefs.getString('publishDraft');
在draft对象中补充savedAt字段,记录草稿保存时间,通过DateTime.now().toIso8601String()生成标准化的时间字符串,便于后续展示"上次编辑时间"。接着获取SharedPreferences实例,将draft对象通过jsonEncode转为JSON字符串,以键名'publishDraft'存储到本地,完成草稿的持久化。getDraft方法用于读取草稿,先获取SharedPreferences实例,再读取'publishDraft'对应的JSON字符串。
dart
if (json != null) {
return jsonDecode(json);
}
return null;
}
static Future<void> clearDraft() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('publishDraft');
}
}
getDraft方法中,判断读取到的JSON字符串是否为空,非空则通过jsonDecode转回Map类型返回,为空则返回null,便于页面判断是否存在草稿数据。clearDraft方法用于发布成功后清空草稿,获取SharedPreferences实例后,调用remove方法删除'publishDraft'对应的键值对,避免草稿残留。
草稿功能对用户很友好,填写到一半不小心退出了,下次进来还能继续编辑。我们把所有表单数据打包成一个Map,转成JSON字符串存储。savedAt记录保存时间,可以提示用户"上次编辑于xxx"。发布成功后记得调用clearDraft清除草稿。
小结
这篇讲解了本地存储的实现方式,使用shared_preferences存储简单的键值对数据,使用hive存储复杂的对象数据。封装成统一的存储类,调用更方便。本地存储能提升用户体验,让用户下次打开App时能恢复之前的状态。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net