多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用Flutter开发跨平台鸿蒙养花APP,浇水提醒与植物识别实现指南

用Flutter开发跨平台鸿蒙养花APP,浇水提醒与植物识别实现指南 第一次听到“用 Flutter 养花”这个需求时我以为是句玩笑。直到朋友在阳台摆了几排多肉出差一周回来死了大半他说想做个 APP拍照能识别植物品种该浇水的时候能提醒一句。恰好那阵子我们团队正在把一套业务代码同时编译到 Android、iOS 和鸿蒙设备上就顺手用 Flutter 开启了养花 APP 的开发流程。这个项目不大但麻雀虽小五脏俱全UI 框架、状态管理、本地数据库、系统通知、相机调用、跨平台通道一个都没落下。如果你也想入门 Flutter 跨平台开发或者正好需要给鸿蒙设备做应用这篇文章能帮你少踩不少坑。1. 为什么用 Flutter 做跨平台鸿蒙养花 APP1.1 养花用户真正需要什么做养花 APP 之前先别急着写代码。我在项目里犯了第一个错误一上来就画界面结果功能堆得越来越像“植物百科全书”用户却卡在最基本的“明天要不要浇水”上。后来我重新观察了目标用户新手养花党、经常出差的人、阳台党他们不是要研究植物学而是想要一个顺手的小助手。最终需求被拆成三个高频点浇水提醒不同的花浇水的周期完全不同多肉可能 15 天一次薄荷两天一次。用户要的不是固定闹钟而是“上次浇水后自动计算下一天”的规则。植物识别买回来一盆不知名的绿植拍张照就能知道品种顺便显示它的光照、湿度、温度偏好。养护记录花了几次水、施过几次肥、有没有换过盆历史记录一目了然帮用户逐步摸清植物的脾气。低频率的“百科查阅”可以作为辅助模块不值得花太多精力做内容库。这个功能定位直接影响了我后面的技术选型不需要自建复杂服务端也不要用庞大的原生 SDK客户端为主、轻量网络请求为辅这就把 Flutter 推到了前台。1.2 从决策到落地的技术路线技术选型永远是权衡的结果。理论上养花 APP 可以用完整的鸿蒙原生 ArkTS/ArkUI 实现一套再分别实现 Android 和 iOS 版本。但一个小团队要维护三套代码光是“多肉修剪提醒”这种细节都能改到怀疑人生。更现实的是UI 在 Android 上刚调好iOS 上圆角像素又差了 1鸿蒙的字体渲染再改一遍。这种三端联调的沟通成本极高。Flutter 的价值在于自绘引擎和统一的逻辑层界面不是调用系统原生的控件而是用 Skia 或 Impeller 引擎把每一帧画出来所以同一套界面代码在 Android、iOS、鸿蒙上能保持很高的一致性。开发者只需要维护一份 Dart 代码再针对不同平台做少量适配比如通知权限、相机权限和后台任务限制。养花 APP 这种以信息展示和提醒为核心的强交互工具天然适合 Flutter。我当时定下的技术路线是这样的UI 与业务逻辑全部由 Flutter/Dart 实现跨 Android、iOS、鸿蒙复用。状态管理选择轻量级 Provider避免引入过重框架。本地数据库用 Hive养花记录这种弱关联数据不需要上 SQLite 的完整能力但需要极快的读写速度。系统提醒和相机调用通过 flutter_local_notifications 和 image_picker 这类社区插件实现鸿蒙端再针对 platform channel 做补充适配。这个路线的核心逻辑是把能够跨平台的代码量最大化把必须调用系统能力的部分收敛到一个薄薄的适配层后面每一步都围绕这个原则展开。2. 开发环境搭建与鸿蒙适配2.1 环境准备清单先解决最难啃的骨头Flutter 和鸿蒙开发环境共存。现在 Flutter 官方和开源鸿蒙社区都在推进跨端支持如果你拿到的是带鸿蒙能力的 Flutter SDK通常会在flutter doctor里看到ohos相关的工具链。我建议按这个顺序准备安装 Flutter SDK建议用 FVM 管理版本避免不同项目之间因为 Flutter 版本不一致导致莫名其妙的编译错误。安装 DevEco Studio里面会带鸿蒙 SDK、模拟器和签名调试工具。如果只是用 Flutter 写界面你依然需要一个鸿蒙工程壳来承接引擎的加载。配置ohos命令行工具确认hdc鸿蒙设备调试工具能在终端里直接执行否则后面打包上传设备会很痛苦。在 Android Studio 里装好 Flutter 插件日常写 Dart 代码的效率靠它保证DevEco Studio 则用来改鸿蒙原生壳和运行鸿蒙调试。这里有一个细节点不要一上来就安装最新的 Flutter 版本而是先确认你手上的项目需要哪个 SDK 分支。网络上关于“当前配置的 Flutter SDK 不被完全支持”的报错绝大多数是版本匹配问题。养花 APP 当时用的是支持鸿蒙的稳定分支版本号虽然是日级迭代但只锁定一个大版本范围能减少很多环境类噪音。2.2 创建 Flutter 项目和鸿蒙工程在一套完整的 Flutter 鸿蒙生态里创建项目的流程是先用 Flutter 创建通用工程再通过命令添加鸿蒙平台支持。基本命令如下flutter create --org com.example --project-name plant_app .如果你的 Flutter SDK 已经接入鸿蒙能力项目目录下会出现ohos文件夹里面是一个标准的鸿蒙工程壳。如果没有可以用支持鸿蒙的分支或者官方后续版本提供的迁移命令这个概念和当年 Flutter 支持 Web、Windows 是一模一样的先有平台壳以后统一由 Flutter 生成。flutter build ohos --debug这条命令会把 Dart 代码编译成鸿蒙设备可以识别的格式同时把原生壳打包成 HAP。首次编译通常需要几分钟主要是下载 Gradle 依赖和鸿蒙 SDK 组件。在你看到build目录下出现 HAP 文件之前都别急着连设备。2.3 目录结构规划项目跑通后我重点规划了目录结构。养花 APP 虽然不算复杂但如果代码全堆在main.dart里后面加两三个页面就会乱成一团。我的划分方式lib/ main.dart app.dart models/ plant.dart record.dart pages/ home_page.dart camera_page.dart calendar_page.dart mine_page.dart provider/ plant_provider.dart services/ notification_service.dart recognition_service.dart database_service.dart utils/ date_helper.dart platform/ event_channel_handler.dartplatform目录单独放所有调用鸿蒙原生能力的代码比如回到后台时系统推送过来的浇水提醒数据、原生相机返回的图片路径。这样分离的好处是当你在鸿蒙端遇到兼容性问题时只需要排查这个目录不用在几百个页面里找方法通道。3. 功能模块开发与核心代码设计3.1 底部导航与页面切换的三个坑养花 APP 的主框架我选了底部导航植物列表、识别拍照、提醒日历、我的设置。Flutter 自带的BottomNavigationBar能快速搭起来但实际开发中有三个坑必须注意。第一个坑是页面状态丢失。底部导航切换时如果直接使用IndexedStack所有页面会一直保持活动状态不会因为切走而重建。这是最稳妥的方案代价是内存占用稍高但对于养花 APP 这种轻量页面完全能接受。class MainShell extends StatelessWidget { const MainShell({super.key}); override Widget build(BuildContext context) { return IndexedStack( index: _currentIndex, children: const [ PlantHomePage(), CameraPage(), CalendarPage(), MinePage(), ], ); } }第二个坑是点击当前 Tab 的视觉反馈。Flutter 的BottomNavigationBar在点击时默认会播放一个缩放或淡入淡出的动画如果用户高频点击这个动画会显得拖沓。网上很多教程让你重写整个BottomNavigationBar其实可以简单地把动画时长归零。不过要注意不同 Flutter 版本处理方式不一样不要盲目抄代码先看 Flutter 源码里的动画实现。第三个坑是页面标题栏和底部导航之间的状态同步。App 内切换 Tab 时AppBar标题要跟着变但AppBar是每个页面自己定义的所以后来我把AppBar抽成了统一组件传入当前 Tab 的标题避免三处代码维护三种状态。3.2 浇水提醒定时任务和本地通知浇水提醒是整个 APP 的核心也是最容易做砸的部分。一开始我打算让服务端定时推送后来发现养花用户根本不登录一台离线设备也要能提醒所以改为“本地通知 本地计算”。基本逻辑是用户给某盆花浇水后自动按下一次浇水时间然后查询接下来 24 小时内需要提醒的植物在对应时间点触发本地通知。Flutter 端我选flutter_local_notifications插件它封装了 Android、iOS 的通知能力鸿蒙端则需要通过平台通道做适配。这里有三个细节值得记录时间计算不能只做“每 N 天提醒”因为花的状态会变。比如多肉夏季可能休眠需要人为推迟浇水。我做了一个nextWateringDate()方法每次浇水时基于当前日期和用户设置的间隔天数按日历日计算而不是简单累加 24 小时。通知权限不是默认开启的。Android 13 以后要申请POST_NOTIFICATIONS权限鸿蒙系统同样有自己的通知权限管理。务必在首次进入提醒模块时主动引导用户开启通知而不是等到通知发不出来时才被用户投诉。通知点击后要跳转到对应植物详情页。这需要配置onDidReceiveNotificationResponse回调把植物 ID 传进来。如果漏了这一步用户收到通知毫无上下文提醒功能的价值就砍了一半。Futurevoid scheduleReminder(Plant plant) async { final nextTime plant.nextWateringDate; const androidDetails AndroidNotificationDetails( plant_watering, 浇水提醒, channelDescription: 到了该浇水的日子, importance: Importance.high, priority: Priority.high, ); await notifications.zonedSchedule( plant.id, 该给 ${plant.name} 浇水啦, 上次浇水是 ${plant.lastWatered.toString()}今天该浇了, nextTime, NotificationDetails(android: androidDetails), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, ); }这里埋着一个坑zonedSchedule需要传入时区如果你传了绝对时间但用户换时区或系统自动调整时间提醒就会错乱。保险做法是每次进入页面时重新计算最近 24 小时的提醒列表而不是完全依赖系统定时器。3.3 植物识别拍照调用和模型选择植物识别模块我起初想直接调用在线图像识别 API后来发现两个问题一是花卉品种地域性很强通用模型容易把绿萝当成吊兰二是在线网络请求在信号差的阳台场景下体验很糟用户拍完照转圈十几秒耐心直接归零。最终方案分两级第一级是本地的轻量分类模型先把图片粗略归到常见的 20 类植物比如绿萝、吊兰、多肉、龟背竹、薄荷、月季第二级是用户对结果不满意时可以发起在线细识别但这不是核心路径。Flutter 端调用相机用image_picker插件用户拍照后把图片传给识别服务。离线模型的接入方式可以用 TFLite 或者 ONNX Runtime。养花 APP 里我选了简单方式把一张图片压缩到 256×256然后传给预训练模型返回置信度最高的几个品种。这个模型不需要我们从头训练网上有不少花卉分类模型可以直接迁移但要注意训练数据里是否覆盖了你目标用户常见的植物。FutureListRecognitionResult recognizePlant(File image) async { final bytes await image.readAsBytes(); final result await _classifier.classifyFromBytes(bytes); return result; }识别完成后页面展示植物名称、简介和养护建议。这里要注意识别结果置信度低于阈值时不要强行给用户一个答案我一般低于 60% 就显示“识别不太确认请换个角度拍摄”避免误导用户。3.4 数据存储设计植物、记录、提醒养花 APP 的数据有一个特点单机小数据、弱关联、更新频繁。用户只有几盆花但每一棵都有几十条浇水记录。最开始我用 SQLite后来觉得太重了。改成 Hive 后读写快了一个数量级而且不用写原生代码维护数据库升级。我定义了三个核心模型模型关键字段用途Plantid、name、category、photoPath、wateringIntervalDays保存植物基础信息和浇水周期WaterRecordid、plantId、wateredAt、note记录每次浇水时间形成历史记录ReminderRuleid、plantId、nextTime、enabled存储系统通知调度状态避免重复创建通知Plant和WaterRecord是一对多关系但 Hive 不支持关系查询所以我直接在Plant里保存一个ListString存 recordId读取时把对应记录丢给页面去展示。这个做法听起来不够“关系型”但在单机 App 里非常实用省去了连表查询的开销。最关键的一点是避免“保存失败”造成的用户信任崩塌。我每次写入记录后会立即刷新 UI如果写入失败则给出一个 SnackBar 提示而不是默默吞掉错误。这一点在 Flutter 的异步模型里尤其容易被忽略很多人喜欢用async方法却忘了等待结果。4. 鸿蒙平台适配MethodChannel、EventChannel 与打包4.1 鸿蒙生命周期和 Flutter 引擎接入当你第一次把一个 Flutter 工程跑上鸿蒙设备时最明显的感觉是硌手Flutter 的标准生命周期在鸿蒙上并不能完全照搬。比如 Flutter 的AppLifecycleState在 Android 上有onPause、onStop在鸿蒙上则有自己的一套前后台切换概念。我一开始写的“从后台恢复刷新植物状态”逻辑在鸿蒙端一直不触发后来才发现要监听鸿蒙的onForeground再通过通道通知 Flutter 端。本质上鸿蒙原生壳是 Flutter 引擎的宿主。养花 APP 的鸿蒙工程入口一般是一个UIAbility它负责创建 FlutterAbility 实例并加载页面。如果你拿到的是社区适配版本不要随意改动原生壳里的默认参数尤其是初始化时传入的引擎参数稍有不慎就会出现 Flutter 引擎加载失败或者黑屏。我建议的接入步骤是这样的在 DevEco Studio 中打开ohos目录确认entry模块配置正确包名、版本号和应用图标都对应项目。确保 Flutter 引擎在onCreate阶段初始化并且设置好路由入口。所有与原生能力相关的调用都用统一的 MethodChannel 通道处理避免直接在前端页面里写PlatformException。4.2 用 EventChannel 做原生端消息推送Flutter 与鸿蒙原生通信有两种常用通道MethodChannel 用于调用原生方法并等待结果EventChannel 用于原生向上主动推送事件。养花 APP 里一个典型场景是当用户在系统设置里关闭通知权限后原生系统会回调给 Flutter页面立刻提示“通知已关闭请重新开启”。这个回调如果用轮询就太蠢了直接用 EventChannel 监听最合适。Flutter 端示例final _eventChannel EventChannel(com.example.plant_app/notification_status); void listenNativeStatus() { _eventChannel.receiveBroadcastStream().listen((event) { if (event notification_disabled) { _statusNotifier.add(NotificationStatus.disabled); } }); }鸿蒙原生端则需要把对应事件通过注册好的 EventChannel 的sendEvent发出来。这个通道的命名必须和 Flutter 端严格一致否则会静默失败。我在调试这类问题时第一件事永远是确认通道名是不是多打了一个下划线。4.3 签名打包和上架前准备养花 APP 做到可以安装到真机时更麻烦的是签名和打包。鸿蒙应用有自己的一套签名体系与 Android 的 keystore 不是一码事。你需要先在 DevEco Studio 里生成密钥然后用hdc app install往设备上装调试包。如果只做调试打开 DevEco 的自动签名即可如果要上架应用市场还要进入正式的签名流程引入版权信息、开发证书和发布证书一步都不能漏。这里有一个真实经验不要把调试包直接发给用户安装。很多用户拿到的手机不一定开了“允许安装外部来源应用”调试包也无法使用正式推送服务。交付测试时要么引导用户打开对应的安装入口要么直接打 HAP 包并做好降级测试。还有一个容易被忽略的点养花 APP 如果需要访问网络识别在线花卉或加载百科图片必须在module.json5里声明ohos.permission.INTERNET权限。缺少这个权限时Flutter 里发出的 HTTP 请求会全部超时而且不会在控制台出现特别明显的异常提示。5. 常见问题与排错实录5.1 构建失败类问题跨平台项目里构建报错是每天的家常便饭。我整理了几条高频问题附上排查思路报错信息原因解法Could not determine the dependencies of task :app:compileDebugJavaWithJavacGradle 依赖下载失败或缓存不完整清理~/.gradle/caches后重新同步也可以检查是否需要配置镜像源The current configured Flutter SDK is not known to be fully supportedFlutter 版本和工程版本不匹配用flutter downgrade切换到工程对应版本或升级项目 SDK不要强行绕过Execution failed for task :ohos:packageHap鸿蒙工程签名配置缺失或资源文件重复检查 DevEco 里的签名配置确认没有重复的 so 文件和资源引用Error: The ohos platform is not enabled当前 Flutter SDK 未启用鸿蒙支持使用指定鸿蒙支持分支重新执行flutter config --enable-ohos或等价命令很多新手遇到第一个报错就直接重装 Flutter其实只要打开 Gradle 控制台看具体是哪一个依赖下载失败大概率能定位到是 network 问题。不要迷信“重装大法”。5.2 运行时报错类问题运行阶段的坑比构建阶段更隐蔽。我印象最深的一个问题是在鸿蒙上点击拍照按钮APP 直接闪退。排查半天发现是相机权限没配置而 Flutter 的image_picker在 Android 上会自动处理权限申请在鸿蒙上却不会。所以凡是涉及系统权限的插件必须在鸿蒙壳里手动声明权限不要默认插件会自动帮你搞定。另一类问题是通知不触发。如果用户把设备连接了省电模式定时任务被系统后台策略限制zonedSchedule可能毫无反馈。我的做法是在页面加载时主动用getPendingNotificationRequests()检查所有待触发的通知发现丢失就重新创建一遍。用户不一定会注意到后台悄悄发生的事但提醒如果没发出去他只会觉得是 App 不行。5.3 性能优化与包体瘦身养花 APP 的安装包体积在鸿蒙端上很容易做到 40MB 以上因为 Flutter 的引擎资源本身就占体积。这里有几个优化点开启 Impeller 渲染引擎新版本 Flutter 默认开启如果还在用 Skia可以尝试打开。Impeller 在复杂界面下的首帧渲染更快动画更平滑。压缩图片资源不要在assets里放原图一张 1920×1080 的花卉背景图足够让包体涨 3MB。统一处理成 WebP 或者 JPEG 质量 85% 以下。移除不用的插件很多人把 image_picker、shared_preferences、permission_handler 一股脑引入结果只用到其中部分功能。可以用flutter pub deps检查依赖树把不需要的插件拆出去。启用树的摇树优化Release 包构建时确保开启了--tree-shake-icons能把未使用的 FontAwesome 图标从包体里去掉。我实测过不优化前 HAP 体积 32MB优化后 24MB内存峰值从 380MB 降到 310MB。这个差距在用户手机上感受非常明显。5.4 我的调试习惯最后分享一套我自己的调试流程。写 Flutter 鸿蒙应用时我不喜欢只用一种 IDE 从头看到尾。平时写 Dart 代码默认在 Android Studio 里完成热重载能极大提升调整 UI 的效率当涉及鸿蒙原生能力时再用 DevEco Studio 打开ohos目录把日志通过hdc log捞出来对比。日志系统这边必须分清层级。Flutter 里用debugPrint打日志鸿蒙原生侧用hilog打日志如果两侧都打印建议统一加一个请求 ID 的前缀比如plant_123_request_start这样在混排日志里一眼能看到一次调用的完整链路。真的遇到平台通道不通时先跑一个最小 Bug 复现在 Flutter 端写一个按钮点击后直接调用原生方法返回当前系统电量如果这个都能失败那就是通道配置问题和业务逻辑无关。回到养花 APP 本身这个项目最终跑起来后朋友反馈最好的不是识别准确率而是提醒功能里的“上次浇水时间”展示。用户需要的不只是“哪天浇水”更是“我上一次做事是不是太晚了”。这种心理体验只有在真实使用中才会被发现。做这类工具型 App与其在技术上炫技不如把用户最容易感知的那几个节点打磨到极致。我个人体会最深的一点是跨平台不是“一次编写处处运行”那么理想化。每一层兼容都要花真金白银去调试Flutter 只是把复杂度压缩到了一个可管理的薄层里。如果你已经在路上记得把平台差异当成功能特性而非 bug 来对待少一点抱怨多一点日志路会顺很多。
返回列表