尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenHarmony上的Flutter实践:从Provider到SharedPreferences的收藏功能实现

OpenHarmony上的Flutter实践:从Provider到SharedPreferences的收藏功能实现 我一直觉得把Flutter跑在OpenHarmony上本身就是一件挺折腾的事但真正把一个App功能从头到尾做完才发现折腾里全是值得记录的细节。这篇文章就以我最近调试通的一个垃圾分类指南App为例讲讲“我的收藏”这个模块是怎么从数据结构设计、状态管理到最终在OpenHarmony设备上稳定跑起来的。如果你正好在搞Flutter for OpenHarmony或者想了解Provider在真实项目里怎么落地那这部分内容应该能帮你少踩不少坑。这个App一开始要做的事情并不复杂搜索垃圾名称确认它属于可回收、有害、厨余还是其他垃圾顺便给出投放建议。但产品提了一个很常见又很核心的需求——用户收藏常用的垃圾条目之后在首页或者独立列表里一眼看到自己常扔的类别。听上去只是个列表加标记可真到了多页面共享状态、OpenHarmony和Flutter版本匹配、本地缓存读写这些环节坑一个接一个。我会把整个过程拆成设计、存储、实现、适配、排查五个部分来聊重点放在“我的收藏”的实现过程环境相关的问题也会一并交代。1. 项目整体设计与技术选型1.1 为什么在OpenHarmony上选Flutter而不是ArkUI我参与的这个版本一开始OpenHarmony和Android要同时交付。OpenHarmony原生应用开发现在主推ArkTS和ArkUI如果只跑一个设备直接用ArkUI写会更正统开发工具DevEco Studio对ArkUI的支持也足够好。但问题在于这套垃圾分类指南App已经有了完整的Flutter页面库设计稿也是按Flutter组件体系输出的单独再维护一套ArkUI页面意味着所有后续改动都要做两遍列表布局、主题色、交互状态、路由跳转每个地方都可能出现偏差。Flutter for OpenHarmony的生态虽然没有ArkUI原生那么“亲儿子”但它把“一套代码多平台运行”的价值落实得很实在。这里需要说明一下Flutter for OpenHarmony用的不是Flutter官方主线默认支持的分支而是OpenHarmony SIG维护的flutter_flutter分支配合OpenHarmony上的引擎适配壳。OpenHarmony OS的系统主体是C/C应用开发层常见的API是ArkTS而Flutter负责在引擎层把Dart代码桥接到OpenHarmony的C接口上。实际操作时绝大多数纯Dart代码可以原样跑起来但一些和平台底层交互的Plugin需要检查是否有OpenHarmony实现。比如shared_preferences、path_provider在OpenHarmony分支上都有对应实现而某些商业地图SDK、推送SDK就未必支持。所以我的选型结论比较保守如果项目像垃圾分类指南这样核心页面是列表、详情、搜索、收藏而且需要多端交付Flutter for OpenHarmony是划算的。如果业务大量依赖OpenHarmony独有的系统服务比如分布式软总线、系统级卡片那还是踏踏实实用ArkUI更稳。这个判断不是拍脑袋是把这个App的页面结构和系统能力依赖清单拉出来对比后得出的页面级代码占了九成系统独占能力几乎没有Flutter适配的成本被摊得很薄。1.2 状态管理用Provider的落地考量“我的收藏”这个需求最麻烦的不是写一个ListView而是跨页面状态同步。用户在指南详情页点了收藏收藏列表页要马上多一条在收藏列表页左滑删除详情页的爱心图标也要跟着变灰。如果只在每个页面内部用setState页面之间就只能靠构造参数来回传传一两层还好一旦碰到“首页搜索列表点进去的详情页”和“指南分类点进去的详情页”这种多入口场景回调路径会变得很乱。最后我选了Provider ChangeNotifier。理由很直接ChangeNotifier本身只负责数据通知Provider负责配合Widget树做依赖注入和重建控制不需要引入事件流或者Action的概念对团队里熟悉Flutter StatefulWidget的同事来说心智负担是最小的。对比Riverpod它在编译期安全性和功能性上更强但要多学一套Provider家族的概念对一个小工具类App有点杀鸡用牛刀。Bloc就更不用说了如果只是维护一个收藏ID集合硬上Bloc会导致每个状态变更都写Event再加Reducer代码量直接翻倍。具体结构上我在main函数里创建SharedPreferences实例然后用MultiProvider一次性挂载FavoritesProvider和AppSettingsProvider。FavoritesProvider内部维护一个Set 用于存放垃圾条目ID对外暴露isFavorite、toggle、clear等方法。所有用到收藏状态的地方要么通过context.watch ()来监听要么通过context.read ()去触发事件。这样详情页和收藏页共享同一个对象实例状态变化后通知所有监听者列表和按钮自动重建完全不需要手动刷新。下面是一段挂载代码可以当成模板直接用void main() async { WidgetsFlutterBinding.ensureInitialized(); final prefs await SharedPreferences.getInstance(); runApp(MultiProvider( providers: [ ChangeNotifierProvider(create: (_) FavoritesProvider(prefs)), ], child: const GarbageGuideApp(), )); }这个写法确保了所有路由页面都能拿到FavoritesProvider。如果你只在某一条路由链上挂Provider那离开这条链的页面就读不到收藏状态会变“灵异事件”——同一个App有的页面还能收藏有的页面直接报ProviderNotFound。挂在runApp外面是最不容易出错的。2. 数据模型与本地存储设计2.1 垃圾分类条目的数据模型怎么定义先交代一下App的基础数据长什么样。垃圾分类指南的数据是从assets目录下打包的JSON文件加载的每条记录包含唯一ID、垃圾名称、类别、详细说明、投放注意事项和一个图标名。类别我固定成四个枚举值recyclable、hazardous、kitchen、other对应可回收、有害、厨余和其他。这样定义的好处是列表页可以根据类别直接映射颜色和Tag文本不需要每个页面重复写switch。Dart这边的模型类我并没有用json_serializable这类代码生成库因为数据字段固定手写fromJson和toJson也就十几行反而更直观。当然如果项目后期条目涨到几千条并且有后端动态下发我会考虑引入freezed或者json_serializable减少手工维护字段的体力活。收藏功能并不需要把完整的GarbageItem对象序列化存储到本地因为全部数据已经在assets里我只需要保存垃圾条目的ID运行时通过ID去数据表里查完整信息。这样最省存储也避免了两份数据不一致的问题。需要特别注意的一个点OpenHarmony上的Flutter Appassets相关内容在构建时就要跟随bundle打包如果后期更新了asset数据没有重新安装或者热更新旧版App拿到的还是旧数据。所以“收藏ID 静态数据”的方案在离线型工具类App里是一个很好的组合但如果数据是服务端动态配置的那收藏模型就要改成保存整个JSON快照否则联网之后ID可能失效。2.2 收藏列表的持久化方案本地存储的方案我在SQLite和SharedPreferences之间纠结过一阵。SQLite在大数据量、复杂查询下有优势但收藏列表的形态比较固定一个ID集合最多也就几百条不涉及到按分类SQL过滤所有的过滤可以在内存里完成。所以最终选SharedPreferences理由是它在这个场景下足够、部署简单同时OpenHarmony分支上对shared_preferences的支持很完整不用再处理SQLitePlugin的兼容问题。方案优点缺点本项目结论SQLite查询强、事务可靠、适合复杂关系插件适配和代码复杂度高不选SharedPreferences简单、键值语义清晰、插件适配完善只适合小量非结构化数据选文件 JSON灵活可控、可自定义格式需要自管路径和异常恢复不选我用了shared_preferences插件来保存一个List key命名为favorite_ids。每次用户点击收藏按钮我先更新内存里的Set再异步把整个ID列表写入preferences。这里的异步写入不需要等它完成才能刷新界面但如果写入失败会导致App杀掉后收藏丢失所以我会在写完后加一个Debug日志确认。如果你想做得更严谨可以给写入操作加个try/catch失败时弹一个SnackBar提示“收藏保存失败”同时保留内存状态等下一次操作再重试。这里有一个小坑SharedPreferences在OpenHarmony上第一次调用getInstance时底层会去读配置文件如果在App启动时急着用它会拿不到实例。我的做法是在main函数里await SharedPreferences.getInstance()拿到之后把它作为参数传进FavoritesProvider的构造函数。这样构造出来的Provider内部一定有一个可用的prefs实例后续读写都不会出现空指针。3. “我的收藏”功能实现全过程3.1 收藏状态管理类FavoritesProvider这里直接放核心代码FavoritesProvider我写成这样class FavoritesProvider extends ChangeNotifier { FavoritesProvider(this._prefs) { _load(); } final SharedPreferences _prefs; final SetString _ids {}; final ListString _orderedIds []; static const String _prefsKey favorite_ids; ListString get orderedIds List.unmodifiable(_orderedIds); bool get isLoaded _loaded; bool isFavorite(String itemId) _ids.contains(itemId); Futurevoid _load() async { final stored _prefs.getStringList(_prefsKey); if (stored ! null) { _ids.addAll(stored); _orderedIds.addAll(stored); } _loaded true; notifyListeners(); } Futurevoid toggle(String itemId) async { if (!_ids.add(itemId)) { _ids.remove(itemId); _orderedIds.remove(itemId); } else { _orderedIds.add(itemId); } notifyListeners(); await _prefs.setStringList(_prefsKey, _orderedIds.toList()); } Futurevoid removeAll() async { _ids.clear(); _orderedIds.clear(); notifyListeners(); await _prefs.remove(_prefsKey); } }几个容易被忽略的点Set只用于判断当前是否收藏保证isFavorite是O(1)List用于维持收藏顺序保证ListView展示稳定。_load在构造函数里发起但由于SharedPreferences实例已经就绪所以没有竞态问题。我没有用FutureBuilder去等Provider完成加载而是在收藏列表页面里用一个isLoaded标志位控制空状态避免刚打开页面时因还没加载完而误显示“暂无收藏”。3.2 收藏列表页面与空状态收藏列表页我用的不是复杂的长列表优化方案就是Scaffold AppBar Consumer嵌套一个ListView.separated。数据源通过context.watch ().orderedIds拿到再用GarbageRepository根据ID查完整数据拼成List 。这里有个细节查询时不能直接在build方法里去遍历几千条原始数据否则每次状态变化都要全量匹配。我提前按ID建了一个HashMapString, GarbageItem查询复杂度降为O(1)列表rebuild时也流畅。空状态是整个页面最容易做得像“半成品”的地方。当收藏列表为空时我返回一个居中的Column上面是一个灰色心形图标下面写“还没有收藏内容”再给一个“去看看垃圾指南”的按钮点击后跳转指南首页。因为OpenHarmony和Android的字体渲染不太一样中文文本偶尔会回退成系统默认字体所以空状态文案长度要控制好不要写太长溢出屏幕。列表项我长这样左侧是一个圆形的图标占位中间是垃圾名称和类别Tag右侧是取消收藏的IconButton。列表项点击进入详情按钮点击只处理取消收藏。为了避免点击IconButton时触发整行的onTap我把IconButton单独包在一个GestureDetector里并且用InkWell包住整行而不是直接用ListTile。这里有个实战经验在OpenHarmony的Flutter分支上InkWell的点击水波纹表现和Android原生有细微差异但功能正常不用太纠结。3.3 收藏按钮交互与反馈收藏按钮的完整交互分成三块状态展示、点击动效、操作反馈。状态展示是用Icon的两种形态isFavorite ? Icons.favorite : Icons.favorite_border颜色用主题红和灰色。动效我用AnimatedSwitcher里面根据状态切换Icon组件切换时带一个ScaleTransition让心形有一个“弹一下”的感觉。代码大致是AnimatedSwitcher( duration: const Duration(milliseconds: 200), transitionBuilder: (child, animation) { return ScaleTransition(scale: animation, child: child); }, child: Icon( isFav ? Icons.favorite : Icons.favorite_border, key: ValueKey(isFav), color: isFav ? Colors.redAccent : Colors.grey, ), )点击触发FavoritesProvider.toggle后我会用SnackBar给一个反馈“已收藏到我的列表”或“已取消收藏”。SnackBar在OpenHarmony的Flutter分支上默认显示在底部样式和Android一致但连续多次点击时会出现SnackBar排队看起来很傻。我加了ScaffoldMessenger.of(context).removeCurrentSnackBar()先移除旧的再显示新的这个细节建议你也加上。还有一个操作性问题收藏按钮如果放在AppBar的actions里点击事件是安全的。但如果你把收藏按钮和列表项放在同一个Row里一定要注意点击热区至少48x48否则OpenHarmony上的触摸采样差异会让用户觉得“老点不中”。我实际把IconButton的padding调大了一圈同时给外层加了一个Tooltip真机手测没有误触问题。另外为了防止用户在一秒内连续点击导致存储写入覆盖我加了一个轻量防抖在toggle开始后置一个_toggleDebounce标记Dart单线程模型下await之前的同步段不会被插队但为了UI安全性我还是忽略了短时间内重复触发。3.4 从列表进入详情页的数据传递收藏列表页点击条目后要跳到详情页。最直接的办法是把整个GarbageItem对象通过构造函数传给下一个页面但这样列表页和详情页会强耦合一旦详情页还需要额外数据构造函数就要改。这个项目我用了路由参数传递ID然后在详情页内部通过Repository读取完整数据。好处是收藏列表页只关心“我要打开IDxx的详情”详情页自己负责数据加载方便后续做深链或者通知栏跳转。在OpenHarmony上Navigator.push的页面转场动画和Android原生类似但不同版本会有差异。我没有用自定义PageRouteBuilder只用了默认MaterialPageRoute保证兼容性。而且如果从收藏列表进入详情再清理App进程然后重新打开App应该直接落在首页而不是停留在详情页详情页的返回逻辑不需要特殊处理因为收藏列表本来就在Navigation栈里返回行为天然是回到列表。4. OpenHarmony平台适配与差异处理4.1 Flutter for OpenHarmony的环境准备要在OpenHarmony上跑Flutter第一步就不是flutter create那么简单。我在项目里用的是OpenHarmony SIG维护的flutter_flutter分支把它checkout到本地并配置到PATH然后用这个分支的flutter命令创建工程。创建完成后工程目录里会多出ohos/目录对应OpenHarmony应用工程结构需要配合DevEco Studio安装的OpenHarmony SDK来构建。环境依赖这块有两点值得记下来。第一flutter命令不能和官方版本混用否则plugin的注册代码路径会出现奇怪错乱。第二OpenHarmony的SDK环境变量和构建工具链要确认好hdc命令要能连上设备IDE里要能看到设备列表。我遇到最多的问题是本地明明装了OpenHarmony SDKflutter build却报找不到SDK最后定位到local.properties里的sdk.dir路径写错了。如果你也遇到先检查这个文件别一上来就重装环境。4.2 存储路径与插件适配OpenHarmony的沙箱目录和Android不完全一致path_provider在不同平台上返回的路径也不同。OpenHarmony分支的path_provider实现里getApplicationDocumentsDirectory返回的是应用沙箱下的files目录或对应文档目录。如果直接把Android开发时硬编码的路径用在OpenHarmony上大概率会读不到文件。我的收藏功能因为用的是shared_preferences没有涉及手写文件路径所以没踩太深。但如果你后续要导出收藏列表到CSV或者备份建议统一通过path_provider获取目录不要把路径写死在代码里。还有一个插件适配的习惯问题Flutter官方插件市场的插件很多只有Android/iOS实现OpenHarmony需要额外确认ohos目录下的实现类是否存在。shared_preferences、path_provider、url_launcher这些常用插件都在SIG仓库里有适配但类似flutter_secure_storage这种依赖硬件安全能力的插件就要多看两眼了。我这次没有用安全存储因为收藏列表也不算敏感数据。4.3 渲染引擎Impeller与其他性能问题OpenHarmony分支的Flutter引擎在渲染底层上还在持续适配。根据我使用的社区分支情况Skia是当前比较稳的渲染路径Impeller适配还不完整。如果在OpenHarmony设备上运行时发现文字模糊、圆角区域出现黑边、或者滚动时偶发闪屏不用怀疑自己的代码先检查是不是渲染引擎的问题。我在真机上跑收藏列表时Flutter日志里出现过一次与Impeller相关的warning后来改为Skia渲染就正常了。这个改动不是通过代码完成的而是在启动时给Flutter引擎传入--no-enable-impeller参数或者在OpenHarmony对应配置里关掉。具体位置取决于你用的移植壳我在项目里是改的engine_layer配置。如果你是在官方主线跑Android设备Impeller默认开启是没问题的但切到OpenHarmony分支不要盲目把Android的优化经验照搬过来。5. 常见问题与调试心得5.1 新建项目后跑不起来的排查思路这个标题也是我踩得最深的坑。新建Flutter项目之后第一次执行flutter runOpenHarmony设备上经常会启动失败。我见到的原因集中在三类Gradle仓库没有配置到OpenHarmony需要的地址、OpenHarmony SDK版本分支与Flutter分支不匹配、以及hdc未连接。处理方法是先跑一条flutter doctor检查Flutter分支和第二平台支持是否正常。如果显示OpenHarmony相关项没有问题再手动执行hdc list targets确认设备可见。之后用flutter run -d 带上完整日志看到类似“Unable to load native library”的报错多半是产物库没打进去可以在ohos模块里检查libs目录下是否有libflutter.so。这个文件不存在App根本起不来。不要一看到红色日志就认为是Dart代码问题OpenHarmony的插件机制和Android的aar/jar不是同一套多看看构建产物。我补充一个和XTS认证相关的经验如果后续要上架OpenHarmony应用市场兼容性测试里对应用接口调用有约束。Flutter适配层如果用了系统私有接口很可能在XTS测试中挂掉。所以写业务时尽量走标准Flutter plugin接口不要自己去调OpenHarmony私有API这个原则在收藏功能上也适用。5.2 Unhandled Exception的定位思路我在开发过程中经常能看到这种日志e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)这种抛错最直接的原因是shared_preferences的OpenHarmony实现没有被注册。OpenHarmony分支的Flutter插件注册方式和Android略有区别有时候需要在ohos目录下手动初始化插件实例。我当时是在MainApplication的onCreate里看到的模板代码它实际上会扫描插件但插件如果不在依赖列表里或channel名字不一致就会出现MissingPluginException。遇到Unhandled Exception千万别只盯着最后一行。我自己的习惯是先看栈顶判断异常是来自Dart层还是平台层然后看channel名称判断哪个插件报错最后检查是不是插件版本与Flutter分支不匹配。比如shared_preferences你如果用了pub上最新版而SIG分支的Plugin化框架只适配到某个旧接口就会在运行时找不到实现。解决办法就是将该插件版本锁定到SIG仓库示例工程里对应的版本。5.3 收藏状态丢失与页面不同步排查收藏状态丢失现象很多但原因基本可归为四类现象原因解决办法冷启动后收藏列表空白异步加载未完成页面先渲染了空态增加isLoaded标志加载完成前显示loading点击收藏后页面没刷新build里用了read而不是watch统一build方法里用watch回调里用read快速连续点击后数据丢失多次写入覆盖了本地存储给toggle加防抖或串行写入队列多页面状态不一致Provider挂载层级不对用MultiProvider挂在runApp外层其中第一类最常见。FavoritesProvider里_load是异步的App一启动页面就能构建但prefs的读取结果要过一会儿才回来。如果页面默认展示空列表用户会看到一瞬间的“还没有收藏内容”闪烁。我给收藏页面加了一个初始化判断isLoaded为false时显示一个轻量的CircularProgressIndicator加载完成后再展示真正的列表这个体验问题就解决了。5.4 给后来者的一点建议如果让我把这次实战浓缩成几句话我最想说的是Flutter for OpenHarmony目前更像一个“可用但需要耐心”的适配方案不要拿完整版Flutter在Android上的体验去衡量它。写“我的收藏”这种功能难的不是UI而是环境和对齐。在动手写第一个页面之前先把Flutter分支版本、OpenHarmony SDK版本、所有插件版本固定住并跑通一个包含shared_preferences、path_provider和Navigator的最小示例。这套验证通过之后再往上堆业务效率高得多。另外调试工具也别一味照搬Android经验。OpenHarmony上很多日志不会进Logcat我用的是hdc shell hilog命令过滤Flutter关键字效率很高。开发过程中如果发现页面交互卡顿不要急着优化Dart代码先看看是不是渲染引擎或GPU驱动在OpenHarmony上的兼容问题。针对性降级Skia后列表滚动就稳定了。个人习惯上我还给收藏功能加了简单的角标统计收藏数量大于0时首页底部Tab上显示一个小数字。这个改动虽然只有几行但在真机上对“用户感知收藏是否成功”帮助很大。虽然这个模块一开始只是“我的收藏”但一个顺手的小反馈往往比功能本身更能打动用户。
返回列表