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

相关推荐
向前V2 小时前
Flutter for OpenHarmony轻量级开源记事本App实战:笔记编辑器
开发语言·笔记·python·flutter·游戏·开源·编辑器
zilikew3 小时前
Flutter框架跨平台鸿蒙开发——小语种学习APP的开发流程
学习·flutter·华为·harmonyos·鸿蒙
晚霞的不甘3 小时前
Flutter for OpenHarmony 创意实战:打造一款炫酷的“太空舱”倒计时应用
开发语言·前端·flutter·正则表达式·前端框架·postman
2601_949480063 小时前
Flutter for OpenHarmony音乐播放器App实战:定时关闭实现
javascript·flutter·原型模式
芙莉莲教你写代码3 小时前
Flutter 框架跨平台鸿蒙开发 - 附近手作工具店查询应用开发教程
flutter·华为·harmonyos
一起养小猫3 小时前
OpenHarmony 实战中的 Flutter:深入理解 Widget 核心概念与底层原理
开发语言·flutter
鸣弦artha4 小时前
BottomSheet底部抽屉组件详解
flutter·华为·harmonyos
zilikew5 小时前
Flutter框架跨平台鸿蒙开发——文字朗读器APP的开发流程
flutter·华为·harmonyos
lbb 小魔仙5 小时前
【Harmonyos】开源鸿蒙跨平台训练营DAY3:HarmonyOS + Flutter + Dio:从零实现跨平台数据清单应用完整指南
flutter·开源·harmonyos