Flutter鸿蒙开发实战:习惯养成工具从环境搭建到真机打包

发布时间:2026/10/13 13:17:01
Flutter鸿蒙开发实战:习惯养成工具从环境搭建到真机打包 前阵子朋友找过来说他们内部想上一个习惯养成的小工具手机端要同时覆盖 Android 和鸿蒙时间卡得紧原生双端各写一套明显不现实问我 Flutter 能不能直接顶上去。我当时给的结论是能但这不是把普通 Flutter 项目拎过来就能跑的事鸿蒙侧从 SDK 选用、权限声明到插件兼容都有一堆差异要处理。这篇就把我从零初始化工程、设计打卡数据、实现列表和热力图再到最后打包出真机可安装产物的完整过程拆开讲一遍重点放在文档里不会写、只有实际踩过才知道的细节上。1. 为什么偏偏用 Flutter 去碰鸿蒙这块硬骨头1.1 需求本身不复杂平台数量才是难点习惯养成应用的核心功能掰开手指头数一下一个习惯列表每天点一下打卡记录连续完成的天数配一个日历热力图再按用户设定的时间发一条本地提醒通知。这个规模放在单一平台上哪怕用原生写两到三周顶天了。问题在于你要同时交付 Android 和鸿蒙两个平台如果走双原生路线就得维护两套代码、两个开发环境、两拨提测节奏而业务逻辑本身又是高度相似的。这种场景天然适合跨平台方案。团队内部工具追求的是“能稳定跑、好维护”而不是极限性能。习惯打卡这种列表为主、交互简单的应用对渲染性能的要求远没有达到需要原生控件撑场面的程度一套逻辑能覆盖多个端光是少维护一套代码长期看就已经值回票价了。1.2 跨端框架摆在一起我更愿意压在 Flutter 上市面上跨端方案不少一类是 JavaScript 生态的一类是类 React 的桥接渲染还有一类就是 Flutter 这种自绘渲染。我最后选 Flutter核心原因是它的 UI 一致性最有保证。自绘渲染意味着风格统一的控件库里画出来的按钮、卡片、列表在 Android 和鸿蒙上的观感基本一致很少出现“这个间距在这边正常、在那边塌了”的情况。另一个现实因素是团队里本来就有 Dart 基础上手成本低。Flutter 的状态管理和组件化思路很直观只要不写太复杂的自定义绘制开发效率明显比双原生高。当然我也清楚它的软肋第三方插件生态里很多包只做了 Android/iOS 适配鸿蒙平台可能需要单独找兼容分支或者自己封装一层。这个点在整个开发周期里会反复出现后面我会单独开一节详说。1.3 一个容易被忽略的前提鸿蒙侧的 Flutter 不是标准版这是最容易被“跨平台”三个字迷惑的地方。很多人以为 Flutter 天然就有三端、四端支持拿标准的 Flutter SDK 创建工程之后想当然地以为跑鸿蒙设备没问题结果设备列表里压根识别不到鸿蒙机器。实际上要正经做鸿蒙 NEXT 平台的 Flutter 应用用的是鸿蒙生态适配过的 Flutter SDK 分支而不是官方默认分发的那份。安装好适配版之后flutter doctor 才能识别出鸿蒙设备新建工程才会带出 ohos 相关的目录。这一步看着不起眼但版本没切开的话后面每一步都会出鬼。所以我强烈建议动手以前先把“到底用哪份 Flutter SDK、对应哪个鸿蒙 API 版本”写进项目 README不然换台电脑就得重新折腾一遍。2. 收拾开发环境版本对不齐才是最大的坑2.1 版本搭配清单开发环境这部分我个人的体会是安装不是难点版本对不齐才是。下面是我跑通这轮开发用的搭配策略不敢说是标准答案但照着做至少能少踩一半坑。组件我采用的版本策略备注Flutter鸿蒙适配版选与目标鸿蒙 API 版本相匹配的 release确认 flutter doctor 能识别 ohosDart跟随 Flutter SDK 自带版本不要手动单独升 Dart容易把 SDK 内部依赖搞乱JDK17部分老版本工具链在更高 JDK 上会出现编译期诡异报错鸿蒙开发工具链与设备系统版本对应的正式版IDE 和命令行工具版本尽量保持一致设备连接工具对应版本的真机调试工具用来安装、日志、截图必不可少这套搭配不是我一次性就摸准的。第一版工程我图省事把 Flutter 升到了最新版结果设备连接工具版本跟不上真机安装时一直报签名不匹配查了半天才发现是工具链版本和系统版本有代差。后来老老实实按“设备 API 版本 → 工具链版本 → Flutter 适配版”的顺序倒推锁版本才稳定下来。2.2 第一次 Create 项目确认多出来的 ohos 目录环境切换好之后创建工程的方式和普通 Flutter 项目差不太多。先确认支持项里已经打开鸿蒙平台开关然后再执行 flutter create。工程生成后目录里会比普通项目多出一个 ohos 目录这个目录相当于鸿蒙侧的原生壳子和 android、ios 目录的角色一样。我当时第一次生成完发现没有这个目录第一反应是命令没执行对反复试了好几次才发现是 SDK 没切换干净。这里有个经验命令行工具分别输出版本号确实指向同一个适配版安装目录之后再重新执行 flutter create或者给已有工程补一个 flutter create --platformsohos 让生成器补齐鸿蒙壳。一旦 ohos 目录正常出现说明工程骨架已经搭起来了后面无非是往这个壳子里填应用名称、包名、图标这些常规信息。2.3 我在这步卡最久的三个问题第一个是依赖下载速度。工程一创建就要拉一堆包中间有任何一个包校验失败整个构建就卡住。建议先把公共依赖镜像配好失败后也不要反复原地重试先把下载缓存清掉再继续否则容易一直卡在同一个包的坏缓存上。第二个是真机连接识别。用设备连接工具看设备列表有时候明明插着线却看不到设备多数不是驱动问题而是手机上的开发者调试开关没有针对当前电脑授权。在设备上重新确认授权再执行连接命令基本都能解决。第三个是 IDE 里的 Flutter 插件和命令行工具版本不一致。IDE 内置的插件会悄悄用自己的 Flutter 路径和你终端里配置的适配版不是同一个导致 IDE 构建时莫名其妙报错。解决办法也简单在 IDE 的设置里把 Flutter SDK 路径显式指向适配版目录不要让 IDE 自动探测。3. 习惯数据模型和打卡逻辑先想清楚再写页面3.1 最小可用模型字段越少越好但有两个不能省写页面以前我先把数据模型定了。习惯养成应用第一版不需要特别复杂的字段少而精才是关键。我定义的最小模型长这样class Habit { String id; // 唯一标识 String name; // 习惯名称 String? icon; // 展示图标第一版用 emoji 字符串 DateTime createdAt; // 创建时间 String reminderTime; // 提醒时间格式 HH:mm ListString completedDates; // 完成日期列表元素是 yyyy-MM-dd }这里有两个字段不能省一个是 id后续做删除、编辑、排序都靠它定位另一个是 completedDates它是整个应用最核心的数据来源。有人会把“今天是否完成”存成一个布尔值字段这样设计在需求只有“今天打一次卡”时看着够用但一旦要算连续天数、画热力图就完全不够了。3.2 “连续打卡数”不该用布尔值硬算连续打卡天数这个功能几乎每个习惯养成应用都会做。最稳的计算方式是从今天往前倒推去 completedDates 这个日期集合里逐个核对有没有当天的记录。一笔笔数直到断档为止。关键点在“今天是否已经打卡”的判断上如果今天还没打计算起点应该从昨天开始。int calcStreak(ListString completedDates, DateTime today) { final doneSet completedDates.toSet(); var streak 0; var cursor today; // 今天没打卡就从昨天开始数 if (!doneSet.contains(formatDate(cursor))) { cursor cursor.subtract(const Duration(days: 1)); } while (doneSet.contains(formatDate(cursor))) { streak; cursor cursor.subtract(const Duration(days: 1)); } return streak; }这段逻辑不复杂但有两个细节实测很坑。第一是日期格式化必须统一用本地时区的 yyyy-MM-dd存的时候是什么规则算的时候也必须是什么规则否则跨天瞬间会出现“今天记录被算成昨天”的问题。第二是 completedDates 里可能混进未来日期比如测试时手动加了几天后的记录倒推时能算出来但界面上会显得很怪所以在写入环节就应该拦截掉未来日期。3.3 存储先上轻方案别被“正规架构”绑架习惯条目量很小一个用户撑死几十条每条带几个日期字符串第一版根本用不着上完整的 SQLite 数据库。我直接用 shared_preferences 存 JSON 字符串整份列表序列化后一次性写进去读出来再反序列化成内存里的对象列表。class HabitStore { static const _key habit_list_v1; static ListHabit load() { final prefs await SharedPreferences.getInstance(); final raw prefs.getString(_key); if (raw null) return []; final list jsonDecode(raw) as List; return list.map((e) Habit.fromJson(e as MapString, dynamic)).toList(); } static Futurevoid save(ListHabit habits) async { final prefs await SharedPreferences.getInstance(); final raw jsonEncode(habits.map((e) e.toJson()).toList()); await prefs.setString(_key, raw); } }为什么第一版不直接上数据库一是 insert、update、delete 的样板代码量很大二是鸿蒙平台上一些数据库插件的兼容性还不稳定初期没必要把这个变量引进来。我建议先把读写收敛到一个 store 接口后面等用户量、数据量真上来了再换成更重型的数据方案页面层完全不用动。这属于“先跑起来再优化”的务实做法。4. 三个核心交互的落地实现4.1 首页列表骨架要稳打卡按钮要防抖首页就是一个习惯列表我相信绝大多数人第一反应是用 ListView Card。这里我踩过一个体验上的坑一开始打卡按钮直接调 setState 刷新整个页面几十条习惯时肉眼能感到卡顿。后来改成把数据封装成可监听的列表模型只有当前打卡项变化时局部刷新流畅度立刻上来了。打卡按钮本身还有个防重复问题。用户连续快速点两下很容易产生两次状态写入如果不做保护completedDates 里会出现重复日期。我在按钮点击里加了一个 pending 标记Futurevoid _toggleHabit(Habit habit, String date) async { if (_pendingIds.contains(habit.id)) return; _pendingIds.add(habit.id); setState(() { if (!habit.completedDates.contains(date)) { habit.completedDates.add(date); } else { habit.completedDates.remove(date); } }); await HabitStore.save(_habits); _pendingIds.remove(habit.id); }这个写法简单粗暴但足够解决连点导致的重复写入。存储写完后才解除标记即使存储慢一点也不会在产品上留下脏数据。4.2 热力图不引第三方图表库GridView 也能画日历热力图是习惯类应用里仪式感最强的部分。第一版我没引任何图表库直接用 GridView 画了一个 7 列 × 若干行的色块网格。每一列代表一周每一行代表星期几一个格子代表一天颜色深浅由当天附近一段窗口内完成的次数决定。GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 7, mainAxisSpacing: 4, crossAxisSpacing: 4, ), itemBuilder: (context, index) { final day startOfWeek.add(Duration(days: index)); final count getCompletionCountAround(day); return _buildDayCell(day, count); }, )画的时候要注意两个点。一个是起始日期必须对齐到某个周的开始否则格子错位另一个是格子的颜色策略我用的是“当天是否完成 最近 90 天完成次数”组合映射完成次数多颜色深完成次数少颜色浅一眼能看出整体坚持的节奏。自绘的好处是样式完全可控不被第三方库的 API 绑住。4.3 本地提醒别写后台计时器交给系统通知调度提醒功能第一版很容易写成“我开个 timer 到点就弹”。这在应用前台运行时确实有效但应用一切到后台或者被杀掉计时器就没了。正确思路是注册一个每日重复的本地通知把调度交给系统。系统本地通知的接入方式在不同端上有差异Android 侧要申请通知权限鸿蒙侧同样有动态申请流程而且 API 签名不完全一样。我当时的做法是在用户设置提醒时间的界面点“开启”时才请求权限而不是在应用启动时就请求这样用户更容易理解为什么需要这个权限。正式发布前一定要在真机上验证一遍“设置提醒后杀掉应用到了点仍然能收到通知”这个完整链路这是模拟器最容易骗人的地方。5. 鸿蒙平台特有的坑与上架前的检查5.1 权限不是在配置文件里写完就完事普通 Android 应用很多人习惯在清单文件里把权限声明好默认会弹授权。到了鸿蒙平台上这个习惯得改。鸿蒙的权限体系里有一部分权限是受控权限需要在项目配置文件里声明同时还要在代码里走动态申请流程两者缺了都会在真机上出现“功能失效但不报错”的现象。最常见的是本地通知权限和网络权限。网络权限属于应用基础能力配置好了基本不会再弹窗而通知权限一定要在用户操作的具体场景里触发申请并且要处理用户拒绝的情况。拒绝之后界面上要给一句明确的引导文案而不是静默失败否则用户会以为应用坏了。5.2 插件清单要逐个过一遍别让 build 成功骗了你Build 能过不代表功能在鸿蒙真机上正常。Flutter 生态里的插件默认适配的是 Android/iOS鸿蒙侧能用多少、怎么改必须逐个验证。我把第一版用到的插件列了个表每项都标注了实测结论插件/能力实测情况处理办法shared_preferences兼容性较好基础读写正常用 JSON 序列化时注意键名规范和异常兜底本地通知插件原生 API 与 Android 不完全一致封装一层鸿蒙侧换成本地通知模块的调用path_provider问题不大路径获取正确多测试几次应用重启后的路径稳定性图表库热力图场景没必要用 GridView 自绘少一个依赖少一个坑sqflite兼容性不确定第一版用 shared_preferences 规避这一步最花时间的不是写代码而是“确认某个能力能不能用”。我的经验是开发第一天就把核心插件在鸿蒙真机上跑一个最小 Demo不要等页面写了一半才发现底层能力不通返工成本会大到你不想重构。5.3 真机调试、安装包与签名信息鸿蒙平台的真机调试我习惯用命令行工具操作。设备连上后先用命令看一眼设备列表再执行安装命令把构建出来的产物装到手机上日志统一看控制台输出。如果同一个包反复安装失败优先检查签名文件签名不匹配是安装失败最常见的原因。发布前还要理清楚产物类型和签名配置的关系。构建出来的安装包分为可安装的单模块产物和完整的应用包两者用途不同签名环节也有差异。我的建议是内测阶段直接用可安装的单模块产物装真机验证完核心链路后再打完整应用包走上线流程。发布到应用市场之前隐私说明、权限用途说明都要在后台对应位置填清楚这一项看着不起眼却是很常见的驳回原因。6. 第一版跑通之后的复盘与扩展6.1 我重写了三个地方第一版跑通后我趁着问题记忆还新鲜把几个明显不合理的地方重写了。第一是日期存储格式早期我混用过带时间的完整时间戳导致热力图跨天定位出错后来统一改成纯日期字符串一劳永逸。第二是列表刷新粒度从整页 setState 改成局部刷新虽然改动不大但体验提升非常明显。第三是通知权限的申请时机从启动时申请改成在设置页触发有效避免了用户一打开应用就被系统弹窗劝退的情况。6.2 给后续扩展留下的接口位代码里我刻意留了几个扩展点。存储层是接口将来要迁到数据库或者云端同步只改实现不改页面。统计逻辑单独抽了一个模块以后除了热力图还能轻松加月报、趋势曲线。习惯模型里目前没有目标天数、分类这些字段但数据结构用的是 JSON向后兼容比较容易新增字段时给个默认值就行。6.3 一点私人心得这轮开发如果让我重新排优先级我会把“第一天就在真机上跑通最小 Demo”放在环境搭建的前面因为很多环境问题在最小的项目里更容易暴露代码多了以后错误日志混在一起排查起来完全不是一回事。另外一个小技巧热力图的格子颜色不要用单一色相的透明度递减用“低完成率偏灰绿、高完成率偏深蓝”的双段色标视觉上更容易引发连续打卡的欲望这一点是我给一个习惯减肥的朋友看 demo 时他无意间提到的改完之后团队里不少人主动多打了一周卡。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询