ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Flutter在OpenHarmony上的实战:商家管理模块开发与踩坑

Flutter在OpenHarmony上的实战:商家管理模块开发与踩坑 先说一个实际的判断如果你手头有一批OpenHarmony设备想快速做出一个能用的工具类AppFlutter目前是脚本最少、见效最快的方案。我最近正好给一套智能家居中控设备做了一个家具购买记录App其中最核心的模块就是商家管理——记录你在哪家店买的沙发、哪家定的柜子、保修期到什么时候还要能随时翻出商家的电话和地址。这个模块看着不起眼但踩坑不少特别是把Flutter项目跑到OpenHarmony上的时候很多Android/iOS上的经验直接失效。这篇文章把我的完整实现过程、数据模型设计、组件通信经验和真机调试踩坑都写出来。如果你正好要开发类似的记录/管理类工具App或者想在OpenHarmony设备上跑Flutter却心里没底这篇文章可以帮你少走一大段弯路。1. 为什么这个App最合适的落点不是鸿蒙OS而是OpenHarmony1.1 OpenHarmony与HarmonyOS NEXT在实际开发中的区别先说个很多人容易混淆的概念。OpenHarmony是一个开源操作系统底座你可以理解成操作系统的地基和公共设施HarmonyOS NEXT是基于OpenHarmony做的商业发行版像精装修的房子。作为开发者我们做工具类App经常直接面向OpenHarmony设备而不是必须上商用系统。我做这个家具购买记录App的起因是给一套智能家居中控台设备配一个内置工具。那台设备跑的就是OpenHarmonyGPU性能一般内存也不算宽裕但它有个好处屏幕尺寸固定、系统纯净、没有手机上的各种推送和权限轰炸。这种场景特别适合一个Flutter应用包解决所有UI的做法。如果你手头的目标设备是OpenHarmony的开发板、智能终端、行业平板那么Flutter for OpenHarmony就是完全可行的路线。它和鸿蒙NEXT上的开发不一样NEXT那边有官方声明支持的生态而OpenHarmony这边更多是靠社区维护的Flutter引擎适配分支走的还是标准Flutter那套Dart代码。1.2 Flutter for OpenHarmony的成熟度评估说实话这个适配分支现在已经到了能用、能跑、能上真机的程度但它不是一马平川。我的体感评估如下能力成熟度说明Dart UI框架高大部分组件渲染正常Flex布局、滚动、路由都能用平台通道中高MethodChannel可用可以调一些系统能力数据库中高drift/sqflite的OpenHarmony适配包可以跑通相机/相册中需要配合平台通道自实现或使用社区插件细节有坑复杂PlatformView低原生嵌入视图的可用性弱视频类、地图类慎用热重载中可用偶尔不稳定真机调试要耐心也就是说凡是以Dart UI为主、依赖原生能力少的工具型App非常适合。凡是要大量嵌入原生控件、涉及硬件编解码、高性能图形渲染的就别硬上了先小范围验证再说。我这个App的商家管理、购买记录、保修提醒、发票照片存储都属于标准表单列表数据库少量图片正好在舒适区里。2. 商家模块的数据模型设计字段规划与数据库选型2.1 家具购买场景下商家表该怎么建商家管理这个模块核心不是能增删改查就完了而是要把家具购买业务中反复要用到的信息一次设计到位。我最初想的很简单商家名、电话、地址。但真正开始整理自己家里的家具购买记录时发现这样远远不够。举个真实例子一张实木餐桌是在线下门店定的门店地址在红星美凯龙一张床垫是在网店买的订单在手机App里一套定制衣柜是厂家上门量尺做的联系人是设计师小张。这三种商家后续需要的追溯信息完全不同。门店商家需要门店地址、营业时间网店商家需要店铺链接、快递发货地址定制商家需要联系人、生产线备注。如果只用三个字段后期找保修凭证、问补货、找售后的时候会非常痛苦。所以我最终设计的商家表如下字段类型说明idINTEGER 主键自增内部主键nameTEXT 非空商家名称用于列表展示和搜索categoryINTEGER商家类型0线下门店1线上网店2定制厂家contact_nameTEXT联系人姓名phoneTEXT联系电话addressTEXT地址store_urlTEXT网店链接或官网ratingREAL评分默认3.0remarkTEXT备注created_atINTEGER创建时间戳updated_atINTEGER更新时间戳这个表刻意把联系人和商家名分开。因为家具定制商家很多时候你记住的是设计师或者销售顾问的名字而不是公司全名。搜索的时候我会让列表同时匹配name和contact_name这样输入小张也能把定制厂家搜出来。2.2 家具购买记录与商家的关联设计商家表不是孤立存在的它要和购买记录表也就是这个App的核心数据建立关联。购买记录表的关键字段我这样设计CREATE TABLE purchase_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, merchant_id INTEGER NOT NULL, product_name TEXT NOT NULL, category TEXT, price REAL, purchase_date TEXT, warranty_end TEXT, invoice_path TEXT, merchant_name_snapshot TEXT, FOREIGN KEY (merchant_id) REFERENCES merchants(id) ON DELETE CASCADE );注意这个merchant_name_snapshot字段这是我踩过坑之后补上的。最初我只存了merchant_id列表展示时需要join商家表拿名字。后来我发现一个真实问题如果你删掉一个无效商家所有历史购买记录的外键会跟着变脏或者显示未知商家。但对用户来说哪怕这个商家倒闭了他在2019年买的那张床垫的保修期还没过他仍然需要知道当年是在哪买的、电话是多少。所以我在记录表里冗余存了一份商家的名称快照列表展示优先用快照不用每次都去关联查询。这是典型的读多写少、数据不常变场景冗余带来的收益远大于风险。购买记录表里还存了保修到期日期后续做保修即将到期提醒时需要按月扫描这张表索引要建在warranty_end上。2.3 数据库方案为什么我选择了drift的OpenHarmony适配数据模型定完接下来是数据库选型。Flutter侧常见的方案有三个sqflite、drift、Hive。Hive是纯Dart的NoSQL方案遇上商家和购买记录外键关联、按条件筛选排序这类SQL强需求写起来反而绕。sqflite是老牌关系型方案SQL操作直接但代码需要手写好多映射样板。drift以前叫moor有一个明显优势表结构用Dart代码声明查询语句类型安全编译期就能抓到字段名拼写错误。这版本对OpenHarmony也有对应的适配包用的还是sqflite那套底层SQLite能力只要按适配包的说明配置好NativeDatabase真机和模拟器都能跑。我最终选了drift是因为这个App后续还要加按品牌汇总花费按月份查看购买趋势这类统计查询。如果字段全是手写字符串拼SQL维护成本会随时间线性增长drift的查询可以写成像Dart方法链的格式后期加字段、加索引都安全很多。提示无论选哪个库都要确认你接的是OpenHarmony适配版而不是原生包。直接用默认的sqflite包在OpenHarmony真机上会报 MissingPluginException这是新手最常踩的第一个坑。3. 商家列表页实现从静态数据到动态搜索过滤3.1 列表页的UI布局与组件拆分商家列表页是这个模块的门面用户90%的时间都停留在这里。我采用的布局是顶部固定搜索栏下方是商家卡片列表右下角悬浮添加按钮列表支持下拉刷新。整体UI用CustomScrollView SliverToBoxAdapter SliverList来实现搜索栏固定在顶部不随列表滚动悬浮按钮用Stack叠在右下角。列表项的卡片设计我参考了联系人名片的思路左侧是商家类型图标中间是商家名称和地址/店铺链接右侧是评分和箭头。类型图标用三个不同的Icon线下门店、线上网店、定制厂家一眼就能分清。组件拆成两层MerchantListPage负责状态管理和数据加载。MerchantCard负责单条商家的展示接收一个Merchant对象。写Widget时我习惯把所有可能变化的子组件拆成const构造这样重建列表时只有真正变化的部分触发重绘。这个表后面有性能优化先说结论列表项里如果用了非const的Text样式对象哪怕只刷新一个评分也会导致整项重新build。所以我在定义卡片的圆角、边距、字体样式时全部用了常量。3.2 搜索、排序与筛选的完整实现搜索是列表页最核心的交互。我的做法是输入框监听文本变化300毫秒防抖后再去过滤数据。为什么需要防抖因为用户输入红木的时候中间会经过红、红木两个状态如果每次输入都查询数据库低端OpenHarmony设备上明显能感觉到掉帧。防抖逻辑用Timer实现Timer? _debounce; void _onSearchChanged(String value) { _debounce?.cancel(); _debounce Timer(const Duration(milliseconds: 300), () { _loadMerchants(keyword: value); }); }过滤条件除了关键字还有排序和筛选。排序方式我给了两个按评分从高到低、按创建时间从新到旧。筛选是商家类型的三段选择用ChoiceChip做在搜索栏下方一排。每次筛选条件变化后我都重新组装查询参数并加载数据。这里我采用每次查询走数据库而不是一次性全部加载到内存再过滤因为家具记录App虽然单个商家表数据量不大但如果后续购买记录多了列表连带要显示最近的购买时间和购买件数内存里做关联会越写越乱。数据库查询的核心代码大致是这样StreamListMerchant watchFilteredMerchants({ String? keyword, int? category, MerchantSort sort MerchantSort.ratingDesc, }) { final query _select(_merchants) ..orderBy([(m) OrderingTerm.desc(sort MerchantSort.createdAtDesc ? m.createdAt : m.rating)]); if (category ! null) { query.where((m) m.category.equalsValue(category)); } if (keyword ! null keyword.isNotEmpty) { final like %$keyword%; query.where((m) m.name.like(like) | m.contactName.like(like)); } return query.watch(); }watch()返回Stream的好处是表单页保存了商家之后不需要任何手动通知列表会自动感知数据库变化并刷新。这也是drift这类响应式数据层的核心价值。3.3 下拉刷新与空状态的细节处理下拉刷新用Flutter自带的RefreshIndicatorOnRefresh回调里去重新加载数据。这里有一个容易忽略的点RefreshIndicator要求它的child必须是可滚动的所以不要把它包在ListView外面而是让RefreshIndicator直接包住ListView并且ListView的physics要设置成AlwaysScrollableScrollPhysics否则内容不满一屏时下拉手势会失效。空状态也要单独设计。我在列表数据为空的时候展示一个居中的空状态一个简单的Icon 还没有商家点右下角添加的提示文字。这个看起来不起眼但实际使用中空状态能直接引导新用户完成第一个操作避免用户打开App后发呆。4. 新增与编辑商家的表单实战校验、照片、保存链路4.1 表单组织和输入校验新增和编辑商家我复用了同一个表单页面区别只在于进入时是否携带空商家对象。用同一个页面的好处是校验逻辑、保存逻辑只维护一份后续加字段不会出现新增能存、编辑不能存的割裂。表单用FormTextFormField组织四个必填字段是商家名称、联系人、电话、类型。其余地址、网店链接、评分、备注都是选填。校验器写在字段的validator参数里核心规则如下商家名称不能为空长度2到30个字符。电话允许座机和手机只做宽松的正则匹配比如^[0-9\-()]{5,20}$不做归属地校验。因为有些商家的联系电话是400客服号用严格手机号正则会误杀。网店链接选填但如果填了就必须以http://或https://开头做一次基础格式校验避免后期点击跳转时崩溃。评分用Slider组件1分到5分步长0.5显示当前分数。比输入框输入数字体验好得多也避免了输入5.999这种脏数据。Form校验时机有个细节不要等用户点保存时才validate那样用户会被一连串报错砸蒙。我在每个字段的onChanged里调用validator单独校验该字段只对已填写过的字段显示错误未参与过的字段保持静默。这样既做到了即时反馈又不会在页面刚打开时把所有红字错误全弹出来。4.2 商家Logo/发票照片的选取与本地存储商家管理里有个很多人会忽略的需求给商家拍一张门头照或者存一张发票照片。等到几年后要找保修凭证的时候你会非常感谢当年存过这张照片。照片处理是这个模块里在OpenHarmony上坑最多的部分。原生image_picker在适配版上能跑通但有两个问题第一相册选择器弹出来的是OpenHarmony系统的选择界面风格和Flutter页面会有点割裂。如果你追求统一体验可以后期自己用PlatformView或平台通道封装原生相册。我这版先用系统选择器把体验割裂放在可接受一档。第二权限问题。在Android上Android 13之后读相册可以不申请权限用系统的Photo Picker就行。OpenHarmony上不同版本的权限策略不完全一致最稳妥的做法是应用只在需要选择照片的那一刻才通过平台通道调起系统选择器不要提前申请一堆权限。照片存储我放在应用私有目录appdata/com.example.furniture_app/files/images/下数据库只存相对路径。为什么不用公共相册因为发票照片和商家门头照属于私密数据用户未必希望它们混进系统相册里而且私有目录在卸载应用时自动清理不会给用户留下困惑的残留文件。读取时用rootBundle不需要用dart:io的File读取即可。这里注意OpenHarmony上的文件路径前缀和Android不完全一样写路径时不要硬编码/storage/emulated/0/要用PathProvider获取到的真实根目录。4.3 保存链路表单校验 - 组装Model - 写库 - 刷新列表保存按钮的完整链路我整理为四步formKey.currentState!.validate()校验所有字段。把TextFormField的控制器值组装成Merchant对象。字段多的场景别一个个手写Merchant(name: _nameController.text...)那样既啰嗦又容易漏字段。我写了一个fromForm的工厂方法统一组装。写数据库新增用insert编辑用update。编辑时要注意只更新必要的字段不要整行覆盖。返回列表页因为列表用的watch()响应式查询数据库变化会自动触发界面刷新不需要手动从当前页传结果回去。这里涉及一个容易被忽略的异步细节。表单保存通常在按钮点击回调里写await _saveMerchant(); if (!mounted) return; Navigator.pop(context);如果你省略了mounted判断在OpenHarmony真机上如果保存过程超过几百毫秒用户可能已经手动切走了页面这时再pop会直接导致Unhandled Exception。这类崩溃在调试期很少遇到因为调试机性能好、数据库小真用户设备上数据多了以后保存超过100毫秒是常态崩溃率会突然上来。关于Future.then回调的执行顺序有人问过then回调是放入微任务队列吗这里正好可以解释是的。Dart里await操作符之后的代码会被调度为微任务优先于事件循环里的其他事件执行。所以在保存链路里写多个await时不要假设每个await之间一定会插入其他UI事件它们会连续执行完。如果你想给用户保存中的中间态需要显式用setState把按钮置灰并显示进度圈而不是指望两个await之间有时间窗口。5. 组件通信与状态同步Flutter侧与OpenHarmony侧的桥接经验5.1 列表页到表单页的传参方式列表页点击新增进入空白表单点击某个商家卡片进入编辑表单。传参方式我用的是最简单的构造函数传参编辑时传入一个Merchant对象表单页initState时用它的值回填所有控制器。为什么不优先用路由参数因为路由参数本质上是字符串映射传整个Model要么序列化、要么单独存共享状态都不如构造函数直接传对象来的干净。Flutter的路由是可以携带任意对象的Navigator.push(MaterialPageRoute(builder: (_) MerchantFormPage(merchant: merchant)))对象引用直接可用没有序列化开销。这个场景不需要考虑页面被系统回收后路由参数丢失的问题因为MaterialPageRoute在栈里的页面不会主动销毁状态。如果你用的是GoRouter这类声明式路由可能就要额外处理了。5.2 跨组件状态同步的选型SetState、Provider 还是 Riverpod做列表页 搜索 筛选 表单四个组件的状态同步选型时可以分两个层面看页面内部临时状态比如搜索框文本用setState足够。跨页面共享的数据商家列表、当前筛选条件用响应式数据源drift的watch就够了不一定非要引入全局状态库。实际写下来我这个模块没有引入Provider或Riverpod直接用drift的watch()解决了最大头的数据变更通知。组件树里搜索条件变化的通信因为发生在同一个页面用StatefulWidget setState就能搞定。全局状态库在这个规模的项目里是负担大于收益它引入context依赖、provider层级调试时还要多跳一层。但如果你确定后续App要加多个复杂页面设置页、统计页、提醒页并且这些页面都要共享商家和购买记录数据那建议早点接Riverpod或Provider。判断标准很简单如果页面A改了数据页面B要看到变化的场景超过两个就用状态库否则先别上YAGNI原则在中小型项目里非常适用。5.3 Flutter侧与OpenHarmony系统能力交互商家管理里用到的系统能力不多主要是两个照片选择和电话拨号。但这两个足够演示Flutter与OpenHarmony平台交互的完整套路。MethodChannel的注册分两步Flutter侧MethodChannel(com.example.furniture_app/merchant)调用invokeMethod(pickImage)或invokeMethod(callPhone, {phone: 138...})。OpenHarmony侧ArkTS或C用适配分支提供的入口注册对应的handler接收method name和参数执行系统能力后返回结果。我踩过的坑MethodChannel的channel名一定要和Flutter侧完全一致包括大小写和点号。有一次我Flutter侧写的是merchant_channelOpenHarmony侧写的是merchant_channel_ohos真机上调用时静默失败没有任何日志提示排查了很久。后来用debugPrint在handler入口打日志才定位到是名字不匹配。PlatformView的适配在OpenHarmony上目前是薄弱项。我最初想用原生地图控件嵌入到商家地址查看页面试了几天发现适配分支对PlatformView的支持还不稳定最后改成用URL在WebView里打开地图链接。如果你做的App必须内嵌复杂原生视图建议先做一个最小demo验证PlatformView在你目标设备上的真实表现再决定技术路线。6. 在OpenHarmony真机上运行的踩坑与调试6.1 环境搭建中的几个关键步骤把Flutter项目跑到OpenHarmony真机上环境搭建比普通Flutter要麻烦一些但并不神秘。按顺序做四件事确认Flutter SDK版本。Flutter for OpenHarmony的适配分支不能随便用官方最新stable要跟随适配仓库的release tag走先查清楚你clone的适配仓库当前推荐哪个版本再决定本地Flutter版本。安装OpenHarmony SDK。这一步通常在DevEco Studio里完成需要你根据目标设备的API版本勾选对应的SDK。克隆项目后先跑一次flutter pub get确认所有依赖都有OpenHarmony对应的包。构建应用包并安装。OpenHarmony的构建产物和Android不同命令行构建要调用适配分支提供的脚本不要假设flutter build apk能用。第一次接触这块的开发者最容易卡在第四步因为Flutter官方命令在OpenHarmony适配分支上并不能像Android那样一站式。我的建议是按照适配仓库的README把构建脚本跑通一次生成安装包后再考虑能不能把它集成到DevEco Studio的IDE流程里降低日常构建成本。6.2 编译与运行时的报错记录这一节分享几个真机调试中高频出现的报错和我的定位方法。第一个是典型的Unhandled Exception日志类似E/flutter [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] unhandled exception: ...这个日志看着吓人但它只是Dart VM捕获未处理异常的通用打印真正有用的信息在异常类型和堆栈里。我遇到过的两个高频原因一是在mounted判断缺失时对已经销毁的页面调用了Navigator或setState二是异步回调里访问了空对象。定位方法很简单看日志里dart:...堆栈的第一个业务文件把对应的onPressed或then回调补上判空和mounted保护就行。第二个是渲染相关的。Flutter新版本默认开启Impeller渲染引擎在OpenHarmony适配分支上如果你的真机GPU驱动比较旧可能出现白色屏幕、字体模糊、圆角矩形发黑这类现象。排查顺序是关掉Impeller通过gradle/构建配置回退到Skia如果恢复正常说明问题是GPU渲染兼容性再打开Impeller看是否只是个别组件问题。如果你的App对性能要求不高在低端OpenHarmony设备上直接保留Skia可能比折腾Impeller更省心。第三个和文本有关。OpenHarmony设备字体渲染和Android有细微差别中英文混排的行高、字重偶尔会和UI设计稿对不上。这个没有捷径只能给Text组件增加textScaler和style.height的适配开关并在多个目标设备上截图比对。6.3 调试小技巧和性能优化真机调试时日志过滤是关键。OpenHarmony设备上系统日志夹杂着各种底层输出我一般用adb logcat | grep flutter只看Flutter侧日志或者用--verbose参数启动应用时输出更详细的渲染信息。列表性能这块商家列表如果只有几十条几乎不需要优化。但如果后续做到几百条有三件小事建议提前做ListView加itemExtent固定条目高度滚动性能会好很多。所有列表项的图片、图标加上缓存不要在build方法里反复创建ImageProvider。不要用ListView.builder的childCount里做耗时计算把需要计算的数据都提前算好构建方法只负责展示。我实际体验下来OpenHarmony低端设备上的Flutter性能比同时期Android低端机大概弱一档。原因不在Flutter本身而是系统图形栈和GPU驱动还不是那么成熟。所以写UI时尽量克制少用阴影、少用半透明叠加、少用复杂的Hero动画。不是不能用而是要先跑真机看帧率再决定要不要保留。最后再分享一个实用的小技巧。我在这类记录 管理App上吃过一次亏只考虑新增没考虑删除。用户真的会删掉一个已经搬走的商家但删除之后他可能后悔因为他需要查那个商家的地址去旧门店开发票。所以在商家管理的删除操作里我只是把is_deleted字段置为1而不是物理删行。列表查询默认加WHERE is_deleted 0。这样既保证了列表干净又保留了恢复数据的可能性。建议你在设计任何管理类数据模型时都考虑一下软删除这个习惯能帮你少挖很多坑。做OpenHarmony上的Flutter应用本质上是用一套已经成熟的技术栈去适配一个还在快速演化的系统底座。最核心的经验就一句话把应用逻辑和平台细节分开数据层、业务层、UI层都不要直接依赖OpenHarmony的私有能力这样当底层适配版本更新时你的迁移成本会小很多。商家管理只是这个思路下的一个落地案例但思路可以复制到整个App。
返回列表