Flutter for OpenHarmony二手物品置换App实战 - 本地存储实现

本地存储用于保存用户数据、缓存信息、应用设置等,让用户下次打开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

相关推荐
程序员Ctrl喵16 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难17 小时前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡18 小时前
flutter列表中实现置顶动画
flutter
始持19 小时前
第十二讲 风格与主题统一
前端·flutter
始持19 小时前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持19 小时前
第十三讲 异步操作与异步构建
前端·flutter
新镜19 小时前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴20 小时前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区20 小时前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎21 小时前
树形选择器组件封装
前端·flutter