ARTICLE DETAIL

资讯详情

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

Flutter+OpenHarmony实战:猫咪管家App喂食功能全解析

Flutter+OpenHarmony实战:猫咪管家App喂食功能全解析 标题写的是Flutter for OpenHarmony 猫咪管家App实战 - 添加喂食实现看到这个题目我第一反应是有意思因为市面上聊Flutter的人很多聊OpenHarmony的人也不少但真正把这俩凑一块儿、还落地到一个具体App功能上的实操帖确实不多。我正好前阵子在一款OpenHarmony开发板上把猫咪自动投喂器跑通了从Flutter层到平台通道再到电机驱动整个链路踩了一圈坑这篇就把喂食功能从零到一完整拆开讲清楚。1. 项目整体设计与思路拆解先说清楚这个项目到底在做什么。猫咪管家App核心场景是帮养猫的人解决人不在家猫怎么按时吃饭的问题。传统方案是买一台定时投喂机但市面上大多数投喂机要么定时不精准要么App只支持自家私有云跨平台体验很一般。我当时的想法是用Flutter写一套UI和业务逻辑跑在OpenHarmony设备上设备本身连接一个舵机驱动的出粮结构用户在手机上通过App下发投喂指令设备端收到指令后控制舵机转动出粮再把结果回传。这样一套下来喂食功能就是一个典型的App端 系统平台层 硬件控制层三层结构。技术选型上为什么用Flutter而不是别的第一OpenHarmony原生开发用ArkTS虽然生态在快速起来但UI组件和第三方库的丰富度跟Flutter比还是差一截尤其动画、布局、状态管理这些Flutter的成熟度明显更高。第二Flutter是跨平台的同一套代码以后也能跑到Android、iOS甚至桌面端对于猫咪管家这种需要多端覆盖的IoT场景边际成本很低。第三就是热词里大家一直在关心的Flutter for OpenHarmony适配问题坦白讲现在的适配已经能支撑真实业务开发了MethodChannel、EventChannel这些通道机制在OpenHarmony上都有对应实现只要架构上留好平台通道的接口写起来跟Android端差别不大。喂食这个功能听起来简单但真拆开来看它涉及几个必须想清楚的点投喂量的控制转几圈、停多久、定时任务的持久化设备重启后定时不丢、App和设备之间的状态同步是正在出粮还是空闲、还有异常处理卡粮了、舵机堵转了怎么办。所以我在设计时没有一上来就写代码而是先把喂食功能拆成了四个状态空闲、投喂中、暂停、异常。这样后面无论是UI展示还是平台通道的消息协议都围绕状态机来走逻辑清晰很多。2. 喂食功能的需求拆解与状态设计2.1 用户场景与交互流程喂食这个动作在真实使用中有三种触发方式。第一种是用户人在设备旁边直接按下App里的立即投喂按钮这是最基础的功能第二种是定时投喂用户设置早上8点和晚上6点各出一份粮设备到点自动执行第三种是远程投喂人不在家通过手机远程触发设备出粮给猫咪加餐。这三种场景对技术的要求完全不同立即投喂需要App到设备低延迟通路定时投喂需要在设备本地做时钟管理远程投喂则需要考虑设备不在线、消息丢失这些边角情况。我在做需求拆解时把喂食主流程定为一套通用的调用链用户触发按钮/定时/远程→ 业务层校验状态 → 通过平台通道发指令到OpenHarmony层 → 平台层驱动舵机PWM → 舵机转动带动出粮轮 → 传感器或日志确认出粮完成 → 结果回传给Flutter层 → UI更新状态。这个链路里最关键的是平台通道的消息协议设计我用的是一套简单的JSON字符串协议字段包括actionfeed/stop/getStatus、duration转动时长毫秒、amount出粮档位。2.2 状态机设计状态机是整个喂食功能的地基。我在Flutter层定义了一个枚举enum FeedState { idle, // 空闲 feeding, // 投喂中 paused, // 暂停 error, // 异常 }为什么必须用状态机而不是几个布尔变量因为喂食过程中会有各种中间态比如电机转动到一半用户强制停止这时候不能直接回到idle要先到paused比如出粮口堵了舵机堵转电流异常平台层会抛出errorFlutter层收到后要把UI切成红色告警状态。如果只用一个isFeeding布尔值这些情况根本表达不清楚UI层容易陷入明明停了按钮还亮着这种尴尬局面。状态流转的规则是idle可以进入feedingfeeding可以进入paused或idle或errorpaused只能回到feeding或idleerror只能通过手动reset回到idle。这套规则我放在了一个独立的ChangeNotifier里用Flutter的provider来管理UI组件通过监听它来自动刷新。2.3 定时任务的持久化方案定时投喂最怕的是什么设备断电重启定时设置全丢了。我最初想简单点用Flutter的shared_preferences存一下定时配置后来发现不行因为Flutter层可能因为各种原因被系统回收真正的定时触发应该在OpenHarmony平台层做Flutter层只是把配置项通过通道传下去。平台层用AlarmManager或者心跳任务来管理定时器这样哪怕UI层挂了到点粮照样出。配置项的同步我用了一个思路Flutter层每次修改定时配置都通过MethodChannel调用setSchedule平台层负责持久化到本地数据库并返回确认结果。App冷启动时Flutter层主动调用getSchedule拉取当前生效的配置保证UI显示的和设备执行的完全一致。这一点非常重要很多IoT App死在设置成功但设备没执行的信任危机上。3. 环境搭建与工程初始化实操3.1 Flutter for OpenHarmony的环境准备如果你之前装过Flutter环境搭建这块基本不陌生但有三个坑必须先说。第一Flutter SDK建议直接用社区维护的OpenHarmony版本不要用原版Flutter SDK硬跑因为原版没有OpenHarmony的engine产物编译到一半会报找不到openharmony目录。第二DevEco Studio要装对应OpenHarmony的版本并且SDK里要包含Native API和ArkTS相关的组件只装默认的API是不行的后面写平台通道时需要用到。我当时的版本组合是这样Flutter SDK用3.7.x的OpenHarmony适配分支DevEco Studio 4.0OpenHarmony SDK API 10。装了之后第一步先在命令行跑flutter doctor确认flutter和dart都识别到了再创建一个测试工程把默认的counter demo跑到OpenHarmony模拟器上。这个验证步骤千万别省我就是因为省了这步后面新建项目编译时才发现engine路径没配好白白折腾了两三个小时。3.2 创建项目并接入OpenHarmony工程结构Flutter for OpenHarmony的工程结构和Android/iOS都不太一样它没有android/和ios/目录取而代之的是一个ohos/目录里面是标准的OpenHarmony工程结构。创建项目时我推荐用官方提供的模板命令flutter create --platformsohos cat_manager_app执行完以后你会看到ohos目录下有个entry模块Flutter的入口Activity/Ability就在这里面相当于Android的MainActivity。Flutter层代码还是在lib/目录下通过一套类似Android的platform channel机制跟ohos层通信。这里有一个容易搞混的点Flutter for OpenHarmony里平台通道的宿主不是Activity而是Ability。注册MethodChannel的时机要在Ability的onCreate或onWindowStageCreate里不要在Flutter页面加载完成之后才注册不然会出现通道已建立但平台端没响应的诡异问题。我踩过这个坑后面排查篇会细说。4. 喂食功能的核心实现UI层与业务层4.1 喂食页面的UI结构喂食页面我采用了大按钮 状态卡片 定时列表的布局。顶部是一张猫咪状态卡根据FeedState显示不同颜色空闲是绿色、投喂中是橙色呼吸动画、异常是红色。中间是投喂主按钮按下后不是直接触发而是弹出一个底部菜单让你选档位小份舵机转1.5秒、标准3秒、加餐5秒。为什么要做档位而不是直接给一个投喂量数字因为档位对应的是舵机转动时长底层舵机是开环控制没有闭环反馈的情况下用时长控制出粮量最简单可靠用户层面理解也直观。UI层代码我大致这样写的class FeedPage extends StatelessWidget { override Widget build(BuildContext context) { final feedModel context.watchFeedModel(); return Scaffold( body: Column( children: [ _buildStatusCard(feedModel.state), _buildActionButton(), _buildScheduleList(feedModel.schedules), ], ), ); } }按钮点击之后的业务逻辑放在FeedModel里因为要处理异步调用和状态流转。每次点击投喂FeedModel先检查当前状态是不是idle如果不是就提示用户设备正在运行中防止连点导致重复指令。4.2 状态管理与事件回传状态管理我用了provider ChangeNotifier的组合。FeedModel负责维护FeedState、持有FeedService的引用、暴露startFeed()、stopFeed()、resetError()这些方法。FeedService则是平台通道的封装层对外提供Future类型的方法给Model调用。这里要特别说下EventChannel的角色。MethodChannel适合请求-响应模式的调用但喂食过程中会产生一系列异步事件开始出粮、电机转动中、出粮完成、堵转异常这些都是设备主动推送的用EventChannel来监听最合适。我建了一个FeedEventChannelOpenHarmony平台侧在不同节点主动向Flutter层发送事件Flutter层收到后更新状态机。整个通信架构用一句话概括下行指令走MethodChannel上行事件走EventChannel。5. 平台通道的完整链路实现5.1 Flutter端的通道封装Flutter端的通道封装是FeedService核心代码不算复杂但要注意的地方不少。class FeedService { static const MethodChannel _methodChannel MethodChannel(cat_manager/feed/method); static const EventChannel _eventChannel EventChannel(cat_manager/feed/event); Futurebool startFeed(int amount) async { try { final result await _methodChannel.invokeMethod(startFeed, { amount: amount, }); return result true; } on PlatformException catch (e) { // 平台层主动抛出的异常比如电机忙 return false; } } StreamFeedEvent get feedEvents { return _eventChannel.receiveBroadcastStream() .map((event) FeedEvent.fromMap(event as Map)); } }需要特别提醒的是MethodChannel的invokeMethod在OpenHarmony平台层的响应是异步的千万不要在主线程里做耗时操作。另外就是事件流一定要在页面初始化的时候就开始订阅而不是等用户点击投喂才订阅不然设备早期推送的事件会丢掉。5.2 OpenHarmony侧的平台实现OpenHarmony侧是整条链路里最容易出问题的一段。我用的是ArkTS语言写平台通道的宿主代码在Ability的onCreate里注册import { Ability, MethodChannel, EventChannel } from ohos/flutter_ohos; export default class MainAbility extends Ability { onCreate(want, launchParam) { this.initFeedChannels(); super.onCreate(want, launchParam); } private initFeedChannels() { const methodChannel new MethodChannel(cat_manager/feed/method); methodChannel.setMethodCallHandler((call) { if (call.method startFeed) { const amount call.arguments[amount]; return this.handleStartFeed(amount); } return Promise.resolve(false); }); const eventChannel new EventChannel(cat_manager/feed/event); this.feedEventSink eventChannel.setStreamHandler(); } }这里的handleStartFeed最终调用的是硬件控制模块也就是真正驱动舵机转动的部分。在OpenHarmony设备上我遇到的实际情况是舵机通过GPIO口连接开发板需要用HDI接口或底层驱动库来控制PWM输出。如果你的设备支持HDI可以直接调驱动服务如果只是普通的GPIO可以通过OpenHarmony的GPIO外设接口去操作。舵机控制的时序是上电后给舵机一个特定脉宽的PWM信号舵机就会转到对应角度。投喂器的出粮结构我设计成舵机带动挡板挡板打开后粮食靠重力落下。出粮量靠转动时长控制舵机从0度转到180度再回到0度算一个循环一个循环大约出粮10克。标准档位就是三个循环大约30克。实测下来这个量对成年猫比较合适但建议根据自己家猫的食量微调循环次数。5.3 状态回传与异常上报平台层一旦开始转动舵机就通过EventChannel推送状态private handleStartFeed(amount: number): Promiseboolean { return new Promise((resolve) { this.feedEventSink.success({ type: feeding, message: start }); this.motor.start(amount * 3) .then(() { this.feedEventSink.success({ type: done, message: success }); resolve(true); }) .catch((err) { this.feedEventSink.success({ type: error, message: blocked }); resolve(false); }); }); }这里有个细节EventChannel的success方法在事件发送完成后通道仍然是开启状态所以可以连续推送。如果你用的是旧版本的流式API可能要求你每次重新打开stream这个差异要注意看SDK版本。6. 定时喂食与远程控制的实现细节6.1 定时任务如何做到设备本地执行我把定时任务放在了OpenHarmony平台层执行这一点是权衡之后的选择。如果放在Flutter层用TimerApp进程一旦被系统杀死或者Flutter isolate异常定时器就没了。平台层用AlarmManager能做到设备待机状态下到点自动唤醒执行。平台层维护一个定时任务的数据库表字段包括scheduleId、hour、minute、amount、enabled。Flutter层调用setSchedule时平台层先更新数据库再重新注册AlarmManager的定时任务。到点触发后会检查启用状态然后走跟手动喂食一样的电机控制流程执行完推送一个事件通知Flutter层定时投喂已执行。6.2 远程喂食的容错设计远程投喂比本地触发多了一层网络的不确定性。我的方案是手机端先把投喂请求发给设备端部署的本地服务或者通过云透传设备收到后先回一个已收到指令的ack然后执行执行完再回传结果。如果超过10秒没有ackApp端提示用户设备可能不在线。千万不要让用户在远程按钮上一直等同步结果体验太差。7. 常见问题与排查技巧实录7.1 编译期问题我在这个项目里遇到的最典型编译报错是热词里也提到的You are applying Flutters main Gradle plugin imperatively using the apply script。这个报错通常出现在把Flutter项目迁移到新版本SDK之后因为新版Flutter Gradle插件要求用声明式方式引入插件而不是老的apply script。解决办法是找到ohos或者android目录下的build.gradle把老式的apply改成插件声明式引入然后同步重建。还有一个很经典的编译报错是could not close i...这类IO异常通常是Flutter engine产物不完整或者构建目录权限不对。我那次是项目里混入了不同版本的Flutter构建产物把build目录整个删掉重新编译就解决了。7.2 平台通道通信失败MethodChannel调用超时的排查我的经验是先分三段Flutter层有没有发出调用、平台层有没有收到调用、平台层有没有返回响应。三段分别打日志。最常见的问题是通道名称不一致Flutter层写的cat_manager/feed/method平台层注册时少写了一个字母这种错误肉眼很难发现最好把通道名抽成常量统一维护。另一个常见坑是Ability生命周期问题。如果MethodChannel在Ability的onDestroy之后被调用平台侧会静默丢弃请求。所以Flutter层在做通道调用前最好先检查设备连接状态。7.3 电机控制的时序问题舵机控制最怕的是PWM频率不对。我一开始用的20ms周期、1ms高电平结果舵机抖得厉害后来查资料发现这款舵机需要50Hz的频率脉宽0.5ms到2.5ms对应0到180度。调了之后稳稳当当。另外就是舵机供电一定要独立不要和开发板共用同一路电源否则转动瞬间的电流尖峰容易把系统搞重启我因为这个现象查了好几天驱动代码最后换了个电源就好了。7.4 状态不同步问题UI显示正在投喂但设备实际已经停了这种状态不同步主要是EventChannel断连导致的。排查时我先把EventChannel的onListen回调打上日志确认Flutter层是否真的监听成功然后确认平台层推送事件时sink是否为空。另外在弱网或设备休眠场景建议加一个心跳机制Flutter层每30秒问一次平台层当前状态超过3次没响应就标记为离线。8. 待到项目跑起来的感受喂食功能上线后我实际用了大概三周。最大的感受是Flutter for OpenHarmony这个组合已经完全能支撑真实场景了不是玩具。中间也踩了不少坑但大多集中在环境配置和平台通道通信上一旦把这些基础设施捋顺业务代码的开发和调试效率和常规Flutter项目没有太大区别。最后分享一个小技巧如果你也打算做类似的硬件交互App在开发阶段一定要把平台通道的日志加上开关而且默认打开。Flutter端打一条OpenHarmony端打一条两边的时间戳一对问题定位就是看日志的事。等稳定后再把日志关掉性能也就不会受到影响。
返回列表