如果你最近也开始盯着鸿蒙设备上的应用缺口,大概率会和我遇到同一个问题:团队就那几个人,Android 和 iOS 版已经够折腾了,再为鸿蒙单独养一套原生开发队伍,排期和成本都扛不住。所以当客户把“老年健康指南应用”这个需求丢过来时,我第一反应就是:这活儿必须走跨平台方案,而且最好能一套代码通吃安卓、PC 和鸿蒙。调研了一圈,最后落在 Flutter 框架身上。这篇内容就是基于实际项目“Flutter 框架跨平台鸿蒙开发 - 老年健康指南应用开发教程”整理出来的完整复盘。我会从技术选型、需求拆解、环境配置、界面实现、数据存储、鸿蒙适配到打包发布,按我做这个项目的真实推进顺序来讲。适合团队里有 Flutter 基础、又想低成本覆盖鸿蒙生态的开发同学参考,也适合第一次接触 Flutter 跨平台鸿蒙开发的新手照着走一遍。1. 为什么是 Flutter:跨平台鸿蒙开发的现实选择先别急着敲命令,把选型理由想清楚,后面遇到问题才知道怎么取舍。1.1 项目驱动的真实痛点客户给的原始需求其实很朴素:一个给长辈用的健康指南应用,要能看健康提醒、记录每日血压和吃药情况、给一些简单的饮食运动建议。但客户后面补了一句“最好市面上主流的大屏设备都能跑”。这里说的“大屏设备”,在 2024 年之后的语境里基本等于:安卓平板、Windows 桌面、还有越来越多的鸿蒙设备。国内很多家庭给长辈买的智能设备都是鸿蒙系统的,这块市场你不能假装看不见。可是如果按老思路,安卓一套、Windows 一套、鸿蒙再一套,三个人干半年都未必收工。我们需要的是“一套 Flutter 代码,多端编译”的路线。1.2 Flutter 在鸿蒙生态里的特殊地位Flutter 能跑鸿蒙,靠的不是官方单方面的施舍,而是开源社区的适配层—— Flutter 的 OpenHarmony 版本 SDK 提供了flutter build hap等命令,让 Flutter 工程可以产出鸿蒙应用包(hap 包)。和 React Native 等其他跨平台方案相比,Flutter 的优势在于底层渲染引擎是自绘的,不依赖系统 WebView,所以在鸿蒙上的表现一致性要好很多。对于老年健康指南这类应用,界面控件多、点击交互频繁、字体又大,如果用 Web 套壳方案,滚动流畅度和点击反馈在大字体模式下容易出现各种小毛病。Flutter 的 Skia 自绘引擎反而省心:字体渲染、圆角卡片、阴影效果各端一致,测试工作量小一截。1.3 什么时候不要选 Flutter + 鸿蒙选型不能只听好话,我也列一下不适合的情况,免得你被坑:应用强依赖系统级原生能力,比如调用非常冷门的鸿蒙硬件接口,而插件还没有适配。健康类应用如果只做记录和提醒问题不大,但如果你要对接某些特定的健康监测硬件,要先查插件支持情况。团队只会原生开发,没有任何 Dart 经验,并且项目周期短到没时间学新语言。应用对包体积极度敏感,Flutter 产物确实比原生大一些,一般健康应用无所谓,但如果你要做极轻量工具,要注意。提示:选 Flutter 做鸿蒙开发,本质是用“学习成本和时间成本”换“多端覆盖能力”,对中小团队和外包项目非常划算,但不要指望零成本迁移到鸿蒙,还是需要配一次鸿蒙 SDK 环境、处理一些权限差异。2. 老年健康指南App:需求梳理与信息架构很多开发者一上来就写界面,结果做出来的东西年轻同事看着挺酷,长辈拿着不知道怎么点。做老年健康方向的应用,产品逻辑比技术更重要。2.1 老年用户的产品诉求,和你想的不一样我实际调研过几位长辈的使用习惯,发现一些反直觉的现象:他们不追求功能多,只求“打开就知道今天要干嘛”。你给一堆图表和智能分析,他们反而焦虑。字体要大,不是大一倍那种,是能贴着脸看那种。很多长辈手机系统字体已经调到了最大,如果应用不支持系统字体缩放,整个布局就乱套了。误触非常常见。手指干、屏幕灵敏度差异大、点按时间偏长,按钮太小或太近就容易点错。他们不太会看“帮助文档”,所有的引导必须做在界面本身,比如按钮上直接写“记血压”,而不是画个 icon 让人猜。所以这个应用的信息架构必须极简:主界面四个大字入口,每个入口进去只有一个核心操作,辅助信息藏在二级页面。2.2 功能清单拆解:只做四件事我把需求收敛成下面的四个核心模块,每个模块都是一个独立页面:模块核心功能设计要点健康打卡记录每日血压、血糖、体重数字输入用大数字键盘,结果用颜色分级提示用药提醒设置早中晚用药时间,到点提醒声音大、文字大、重复提醒直到确认健康指南按分类展示饮食、运动建议图文卡片,每张卡只讲一件事我的档案查看历史记录、修改个人信息简单列表,支持导出和分享这四件事对应的页面层级很清楚:首页四个大卡片 → 二级功能页 → 三级详情页。全应用最多三层,不能让长辈迷路。2.3 为什么必须避免“设计感过强”的交互我见过不少同类产品喜欢用滑动手势、长按操作、下沉式菜单这些现代交互。但在老年场景里,这些全是坑:滑动切换 Tab,长辈很容易误触,一下就滑走了。长按呼出菜单,他们根本不知道还能这样操作。隐藏式导航,他们找不到返回按钮。所以我把所有交互都设为“点击可见按钮”,按钮高度至少 56dp,间距至少 12dp。这不是设计保守,是实打实的用户需求。后面实现的时候你会发现,Flutter 做这些大按钮和固定布局非常顺手。3. 环境准备:Flutter 跨平台鸿蒙开发的第一道坎这可能是整个项目里新人最容易卡住的地方。不是 Flutter 本身难装,而是“Flutter 跑鸿蒙”需要额外配置一层鸿蒙 SDK,很多人没搞清楚这个链条,装到一半就放弃了。3.1 工具链清单:四样东西缺一不可要做 Flutter 跨平台鸿蒙开发,你本机需要准备:Flutter SDK(3.x 版本,建议 3.19 及以上,太老的版本对鸿蒙适配不友好)鸿蒙 SDK(通过 DevEco Studio 里的 SDK Manager 下载,或者命令行方式下载)DevEco Studio(鸿蒙官方 IDE,主要用来管理 SDK、签名、跑模拟器)一个可用的鸿蒙模拟器或真机链条是:Flutter 工程 → Flutter 的 ohos 适配层 → 鸿蒙 SDK → 生成 hap 包。任何一个环节版本不对,命令就会报一些很奇怪的错误。3.2 配置细节:环境变量和命令行启用拿到工具后,先把鸿蒙 SDK 路径配置好。我用的命令大致是这样的,注意 SDK 路径不要带空格,否则后面构建容易出问题:# 设置鸿蒙 SDK 路径 export LOCAL_HOS_SDK_PATH=/你的路径/ohos-sdk # 启用 Flutter 的鸿蒙通道 flutter config --enable-ohos # 查看是否生效 flutter doctor如果flutter doctor里出现OHOS相关条目,说明适配层认到了。这一步做完,新建项目时就可以指定平台:flutter create --platforms ohos,android,ios health_guide_app这里我会同时保留 android 和 ios 平台,方便以后多端打包。项目创建之后,目录里会出现一个ohos文件夹,里面是鸿蒙平台的壳工程,Flutter 代码依然写在lib目录里,不用动壳工程。3.3 我踩过的环境坑:版本不匹配和签名缺失第一条:Flutter SDK 和鸿蒙 SDK 版本要匹配,不是说“越新越好”。我用最新版 Flutter 配上一个较老的鸿蒙 SDK,结果构建时报了一堆 C++ 符号找不到。后来把鸿蒙 SDK 升到和 Flutter 适配层要求的版本,问题才消失。第二条:直接命令行flutter run -d ohos跑真机时,会提示签名问题。鸿蒙真机安装应用需要签名配置,不像安卓 debug 模式自动帮你签。需要先去 DevEco Studio 里配置好自动签名,再回命令行运行。第三条:模拟器启动很慢,而且某些版本的模拟器对 Flutter 渲染支持有问题,界面会花屏。我的解决办法是:日常逻辑调试用安卓模拟器跑同一个工程,鸿蒙真机只做集成验证和性能测试。反正核心 UI 是同一套代码,这样省很多时间。4. 落地核心界面:大字、高对比、大点击区环境通了以后,别急着写业务逻辑,先把“老年友好”这个主题的界面基座打好。这一部分就是你交作业时最能拿出来展示的东西。4.1 全局文字缩放:TextScaler 的正确打开方式老年应用的字体不能只靠“加大字号”这种物理操作,还要跟随系统字体设置一起缩放。Flutter 里控制全局尺寸缩放的关键是MediaQuery.textScaler。但有个坑:如果全局自定义了超大textScaler,列表页的卡片高度、文字换行都会乱套。我采用的方法是,设置一个“温和的最大缩放值”,并且对关键文本用maxLines和overflow做保护:MaterialApp( title: '长辈健康指南', builder: (context, child) { final mediaQueryData = MediaQuery.of(context); final scale = mediaQueryData.textScaler.scale(1.0).clamp(1.0, 1.8); return MediaQuery( data: mediaQueryData.copyWith( textScaler: TextScaler.linear(scale), ), child: child!, ); }, theme: _buildTheme(), home: const HomePage(), );注意:clamp到 1.8 不是说只允许放大到 1.8 倍,而是提供一个上限,防止用户在系统层面把字体调到离谱时,界面直接变得不可用。我的实测经验是 1.6~2.0 之间最安全,既能“跟随系统”,又不至于把布局撑爆。4.2 高对比度主题:不要纯白背景也不要纯黑文字长辈的视力普遍下降,对低对比度内容的识别很差。但高对比不等于“纯黑底白字”,那个看久了眼睛累。我最后用的是暖白色背景配深褐色文字,重点按钮用深橙色,整体类似报纸的阅读质感。主题定义我单独抽了一个函数,下面是一个可以照抄的简化版:ThemeData _buildTheme() { final base = ThemeData.light(); return base.copyWith( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFFE8590C), // 深橙 brightness: Brightness.light, ), scaffoldBackgroundColor: const Color(0xFFFDF6EC), textTheme: base.textTheme.apply( bodyColor: const Color(0xFF3E2723), displayColor: const Color(0xFF3E2723), ), ); }实际运行时我发现:按钮上的文字必须比背景更重,比如深橙底上用白色文字没问题,但如果按钮变成浅橙底,就一定要配深褐文字,否则长辈会看不清按钮上的字。这条规则我写进了项目的 UI 检查清单里。4.3 大按钮与大卡片组件:顺手解决误触和点击反馈这个项目里我基本不用原生ElevatedButton,而是封装了一个BigActionCard,统一处理大尺寸、圆角、阴影和点击波纹:class BigActionCard extends StatelessWidget { final String title; final String subtitle; final IconData icon; final VoidCallback onTap; const BigActionCard({ super.key, required this.title, required this.subtitle, required this.icon, required this.onTap, }); @override Widget build(BuildContext context) { return Semantics( button: true, label: '$title,$subtitle', child: Material( color: Colors.white, borderRadius: BorderRadius.circular(20), elevation: 2, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Container( height: 120, padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Icon(icon, size: 44, color: const Color(0xFFE8590C)), const SizedBox(width: 16), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(subtitle, style: const TextStyle(fontSize: 16, color: Color(0xFF6D5A50)), maxLines: 1, overflow: TextOverflow.ellipsis), ], ), ), const Icon(Icons.chevron_right, size: 32, color: Color(0xFFB8A79C)), ], ), ), ), ), ); } }封装这个组件的经验之谈:一定要加上Semantics。长辈用系统自带的“屏幕朗读”功能时,没有语义标签的卡片读出来就是一串乱码。顺便说一句,很多团队做老年应用完全忽略无障碍,这是大忌。首页四个大卡片排列方式也不用花哨,两行两列,每张卡片高度统一 120dp,间距 16dp。长辈的手指一般比较粗,120dp 的高度加全屏宽度,误触概率能压到最后。5. 本地数据与业务逻辑:健康记录和用药提醒界面骨架搭好后,开始处理实际数据。老年健康应用有个特殊要求:长辈不一定常联网,而且对“数据传到网上”有天然顾虑。所以我的方案是默认本地存储,如果需要同步,用户手动操作。5.1 为什么选 shared_preferences + sqflite 双保险项目里我同时用了两个存储方案:shared_preferences存用户设置、最后一次打卡日期、用药提醒的开关状态。sqflite存历史健康记录,因为记录会越来越多,需要结构化查询。为什么不直接全用数据库?因为提醒状态和“今天是否打卡”这类键值对用数据库反而是杀鸡用牛刀,而且 shared_preferences 读取极快,应用启动时能立刻显示今天的状态。历史记录才值得进数据库。5.2 健康打卡的存储结构与读写我建了一张非常简单的表:CREATE TABLE health_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, type TEXT NOT NULL, value REAL NOT NULL, note TEXT, created_at TEXT NOT NULL );type字段存blood_pressure、blood_sugar、weight这类枚举值。date用yyyy-MM-dd字符串,好排序也好查重。同一天同类型只允许记录一次,如果用户再点打卡,就提示“今天已经记录过了,可以去档案里修改”。写记录的 Dart 代码逻辑不复杂,核心是异步操作数据库,注意不要阻塞 UI:Future<void> upsertRecord(HealthRecord record) async { final db = await _db; await db.insert( 'health_records', record.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); }做这个模块时最容易翻车的地方是date的时区。如果你直接用DateTime.now()存,东八区和 UTC 混在一起,第二天打开应用会出现“昨天记录没了”的灵异事件。我最后统一用本地时区格式化后的字符串,不存时间戳。5.3 用药提醒的本地通知方案用药提醒这个功能,理想情况是接入各厂商的推送通道,但本地应用没有那么强的后端支撑,而且长辈完全离线也要能用。所以我选了本地通知方案。Flutter 侧我用的是flutter_local_notifications,这个插件的跨平台支持已经比较成熟。鸿蒙端我实测发现,基础的通知弹窗是可以用的,但要记得在鸿蒙壳工程里申请通知权限,否则没有弹窗。初始化通知的代码我提炼成了一个 service:class ReminderService { static final ReminderService instance = ReminderService._(); Future<void> init() async { final plugin = FlutterLocalNotificationsPlugin(); await plugin.initialize( const InitializationSettings( android: AndroidInitializationSettings('app_icon'), iOS: DarwinInitializationSettings(), ), ); // 请求通知权限 await plugin .resolvePlatformSpecificImplementation <IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions(alert: true, badge: true, sound: true); await plugin .resolvePlatformSpecificImplementation <AndroidFlutterLocalNotificationsPlugin>() ?.requestNotificationsPermission(); } Future<void> scheduleDailyReminder({ required int id, required TimeOfDay time, required String title, required String body, }) async { final plugin = FlutterLocalNotificationsPlugin(); await plugin.zonedSchedule( id, title, body, _nextInstanceOfTime(time), const NotificationDetails( android: AndroidNotificationDetails( 'medication_reminder', '用药提醒', channelDescription: '每日用药时间提醒', importance: Importance.high, priority: Priority.high, ), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, ); } }用药提醒的逻辑一定要加“重复提醒”。很多长辈吃了药就忘,单纯弹一次根本不够。我把同一条提醒设为每 10 分钟重复一次,直到用户点击“我已吃药”。这一点是在和客户反复确认后才定的,也是整个应用里长辈反馈最好的功能。5.4 周历和饮食建议这些容易忽略的细节历史记录列表里我加了一个简单的“周视图”小模块,按周显示每天的打卡状态。这里有个常见的坑:中国习惯周一作为一周开始,而很多库默认周日开始。我在处理周计算时显式指定了周起始日,否则长辈看到的周历和你以为的完全对不上。健康指南的饮食建议我做得更“笨”一些:每条建议就是一个卡片,包含一个标题、一段话、一张插图。不搞信息流、不做推荐算法。因为内容多了长辈根本看不过来,而且医疗健康类内容一旦“智能推荐”出错,责任很大。所有内容都声明“仅供参考,具体请遵医嘱”。这一点大家做健康类应用时一定要守住底线。6. 鸿蒙适配与权限:从 Android 思维里跳出来很多从安卓转过来的开发者,到了鸿蒙上第一反应是“这不就是安卓吗”。实际上鸿蒙的权限模型、生命周期和应用沙箱都有自己的脾气,尤其是从 Flutter 层调用原生的部分,需要单独适配。6.1 权限声明:不要只改 AndroidManifest如果你打算让同一个 Flutter 工程同时支持安卓和鸿蒙,那么安卓的权限写在android/app/src/main/AndroidManifest.xml里,鸿蒙的权限则要写在ohos壳工程的module.json5里。两个文件互不相干,少写一个就等着运行时静默失败。以相机拍照记录药品或食物为例,鸿蒙端要声明类似下面的权限(不同的鸿蒙 SDK 版本,权限字符串会有差异,以实际版本为准):{ module: { requestPermissions: [ { name: "ohos.permission.CAMERA", reason: "用于拍摄药品包装和食物照片", usedScene: { abilities: ["EntryAbility"], when: "inuse" } } ] } }这里最坑的地方是reason必须写清楚用途,否则上架审核时会被打回。我在第一次提交时就因为 reason 写得模糊被驳回过一次。6.2 返回键、手势和系统字体:体验差异比你想的大安卓的返回键逻辑大家已经很熟了,但鸿蒙设备的交互有差异,尤其是面向长辈的场景:有些鸿蒙设备没有三大金刚键,靠的是侧滑返回手势。如果应用内用了自定义手势,很容易和系统手势冲突。鸿蒙的“大字体”设置档位比安卓更激进,系统最大字体下,小屏手机的 Flutter 布局可能溢出。我的应对办法前面已经说过,用textScaler限制最大缩放,同时所有文本组件都加maxLines兜底。鸿蒙的部分平板设备默认开启了“长辈模式”,系统字体、图标、触摸响应区域都会变大。你的应用如果能跟随系统字体缩放,在这类设备上的表现会非常自然。6.3 真机测试:至少准备三台不同形态的设备我测试时用了三种设备:一台小屏直板手机、一台大屏平板、一台折叠屏设备。为什么一定要这么测?小屏手机主要看布局会不会溢出、按钮能不能点到。大屏平板主要看卡片要不要跟着铺满,还要不要限制最大宽度。折叠屏的坑在于展开和折叠的尺寸变化,虽然 Flutter 会触发重建,但如果你在初始化时缓存了尺寸,界面就会错乱。真机测试中我发现 Flutter 在鸿蒙平板上有时候状态栏会顶到界面,解决方法是获取系统安全区域并设置 padding,不要用一堆写死的间距:final SafeArea( child: Scaffold(...) );这个看起来不起眼,但在不同屏幕形态的鸿蒙设备上特别管用。6.4 插件兼容性的排查方法鸿蒙上的 Flutter 插件生态还没有安卓那么完善。我遇到过一个很实际的问题:某个图像处理插件在安卓上正常,在鸿蒙上却直接报“MissingPluginException”。排查思路是:查看插件是否声明了 ohos 平台的实现。到 pub.dev 看插件的支持平台列表。如果插件不支持,就去插件仓库找是否有 OpenHarmony 分支,或者用我们设计的“健康打卡”里最简单的方案:只使用camera插件的基础拍照能力,不碰复杂的滤镜和图像分析。实操心得:遇到插件不兼容,先别急着换插件,很多复杂插件的核心功能用系统已有能力就能实现,或者用 MethodChannel 自己写一个薄壳调用鸿蒙原生接口,也就百来行代码。这比强行适配一个不支持 ohos 的插件省事得多。7. 打包发布与上架自测清单最后一步是把应用从开发机带到用户的设备上。Flutter 跨平台鸿蒙开发的打包流程和安卓不完全一样,但熟悉之后也没什么玄学。7.1 生成 hap 包和签名配置本地调试用的是自动签名,但发布版需要正式的签名文件。流程如下:在 DevEco Studio 里生成签名证书,下载到本地。在鸿蒙壳工程里配置签名文件路径。命令行执行构建:flutter build hap --release构建完成后,产物在build/ohos/release/目录下,后缀是.hap。这个包就可以拿去应用市场做测试分发或上架了。第一次构建 hap 包通常会有几类报错:证书过期或路径不对,检查签名配置。包名和签名不一致,检查应用市场上的包名和本地签名是否匹配。代码里有插件找不到鸿蒙端实现,构建时不会报错,但运行时会崩,所以上架前最好做一次全面回归。7.2 上架前的自测清单:我每次都会跑一遍我把这个项目沉淀成了一份自测清单,不复杂,但很有效:[ ] 系统字体调到最大,界面上所有按钮是否可点、文字是否溢出。[ ] 开启屏幕朗读模式,操作一遍核心流程,看看所有控件是否有语义标签。[ ] 断网状态下,是否还能完成健康打卡、查看历史记录。[ ] 用药提醒设置后,锁屏状态下是否准时弹出。[ ] 平板、折叠屏、小屏手机三台设备各跑一遍,看布局是否错乱。[ ] 连续打卡 7 天,检查周历和统计是否正常。[ ] 拍照权限、通知权限都拒绝一次再进入应用,不会闪退。这套清单不依赖任何自动化测试脚本,纯手工跑,但在老年健康这个场景里,手工实测比自动化用例更能发现问题。因为用户不是标准的“测试用户”,他们会做出各种意想不到的操作。7.3 后续迭代:心率插件、多语言、云同步这个项目上线后,客户又提了三个新方向:对接心率手环,这需要原生插件配合,鸿蒙端的设备协议不统一,是个硬骨头。增加语音提示,长辈视力不好时点击按钮能朗读内容。多端云同步,让子女能看到父母的健康记录。这些方向我都会优先在 Flutter 层做能力预留,比如日程数据结构里预留server_id字段,后续接云同步时不用改表结构。语音提示用 Flutter 自带的flutter_tts做原型,鸿蒙端如果不行,就通过 MethodChannel 调系统语音,整体方案已经有眉目了。最后分享一个小技巧这个项目做下来,我最想留给大家的一条经验是:做老年健康类应用,永远不要用年轻开发者的视觉标准去验收界面。每次改完一个页面,把手机字体调到最大,戴上老花镜模拟一下视力模糊的状态,再去点那些按钮。你会发现自己觉得挺好看的界面,在真实长辈眼里可能全是灾难。我在测试时养成了一个习惯:把设计稿里的初级灰文字全部换成深褐或深灰,把低于 48dp 高度的可点击元素全部放大,把一切的“双击”“长按”“滑动”全部改回单击。这些改动在代码里可能就是一个属性、一个变量的事,但对使用这个应用的长辈来说,体验差别是天壤之别。Flutter 的好处是这些调整写一次,所有平台同步生效,这也是我敢把这个项目定成跨平台方案并坚持到最后的底气。
如果你最近也开始盯着鸿蒙设备上的应用缺口,大概率会和我遇到同一个问题:团队就那几个人,Android 和 iOS 版已经够折腾了,再为鸿蒙单独养一套原生开发队伍,排期和成本都扛不住。所以当客户把“老年健康指南应用”这个需求丢过来时,我第一反应就是:这活儿必须走跨平台方案,而且最好能一套代码通吃安卓、PC 和鸿蒙。调研了一圈,最后落在 Flutter 框架身上。这篇内容就是基于实际项目“Flutter 框架跨平台鸿蒙开发 - 老年健康指南应用开发教程”整理出来的完整复盘。我会从技术选型、需求拆解、环境配置、界面实现、数据存储、鸿蒙适配到打包发布,按我做这个项目的真实推进顺序来讲。适合团队里有 Flutter 基础、又想低成本覆盖鸿蒙生态的开发同学参考,也适合第一次接触 Flutter 跨平台鸿蒙开发的新手照着走一遍。1. 为什么是 Flutter:跨平台鸿蒙开发的现实选择先别急着敲命令,把选型理由想清楚,后面遇到问题才知道怎么取舍。1.1 项目驱动的真实痛点客户给的原始需求其实很朴素:一个给长辈用的健康指南应用,要能看健康提醒、记录每日血压和吃药情况、给一些简单的饮食运动建议。但客户后面补了一句“最好市面上主流的大屏设备都能跑”。这里说的“大屏设备”,在 2024 年之后的语境里基本等于:安卓平板、Windows 桌面、还有越来越多的鸿蒙设备。国内很多家庭给长辈买的智能设备都是鸿蒙系统的,这块市场你不能假装看不见。可是如果按老思路,安卓一套、Windows 一套、鸿蒙再一套,三个人干半年都未必收工。我们需要的是“一套 Flutter 代码,多端编译”的路线。1.2 Flutter 在鸿蒙生态里的特殊地位Flutter 能跑鸿蒙,靠的不是官方单方面的施舍,而是开源社区的适配层—— Flutter 的 OpenHarmony 版本 SDK 提供了flutter build hap等命令,让 Flutter 工程可以产出鸿蒙应用包(hap 包)。和 React Native 等其他跨平台方案相比,Flutter 的优势在于底层渲染引擎是自绘的,不依赖系统 WebView,所以在鸿蒙上的表现一致性要好很多。对于老年健康指南这类应用,界面控件多、点击交互频繁、字体又大,如果用 Web 套壳方案,滚动流畅度和点击反馈在大字体模式下容易出现各种小毛病。Flutter 的 Skia 自绘引擎反而省心:字体渲染、圆角卡片、阴影效果各端一致,测试工作量小一截。1.3 什么时候不要选 Flutter + 鸿蒙选型不能只听好话,我也列一下不适合的情况,免得你被坑:应用强依赖系统级原生能力,比如调用非常冷门的鸿蒙硬件接口,而插件还没有适配。健康类应用如果只做记录和提醒问题不大,但如果你要对接某些特定的健康监测硬件,要先查插件支持情况。团队只会原生开发,没有任何 Dart 经验,并且项目周期短到没时间学新语言。应用对包体积极度敏感,Flutter 产物确实比原生大一些,一般健康应用无所谓,但如果你要做极轻量工具,要注意。提示:选 Flutter 做鸿蒙开发,本质是用“学习成本和时间成本”换“多端覆盖能力”,对中小团队和外包项目非常划算,但不要指望零成本迁移到鸿蒙,还是需要配一次鸿蒙 SDK 环境、处理一些权限差异。2. 老年健康指南App:需求梳理与信息架构很多开发者一上来就写界面,结果做出来的东西年轻同事看着挺酷,长辈拿着不知道怎么点。做老年健康方向的应用,产品逻辑比技术更重要。2.1 老年用户的产品诉求,和你想的不一样我实际调研过几位长辈的使用习惯,发现一些反直觉的现象:他们不追求功能多,只求“打开就知道今天要干嘛”。你给一堆图表和智能分析,他们反而焦虑。字体要大,不是大一倍那种,是能贴着脸看那种。很多长辈手机系统字体已经调到了最大,如果应用不支持系统字体缩放,整个布局就乱套了。误触非常常见。手指干、屏幕灵敏度差异大、点按时间偏长,按钮太小或太近就容易点错。他们不太会看“帮助文档”,所有的引导必须做在界面本身,比如按钮上直接写“记血压”,而不是画个 icon 让人猜。所以这个应用的信息架构必须极简:主界面四个大字入口,每个入口进去只有一个核心操作,辅助信息藏在二级页面。2.2 功能清单拆解:只做四件事我把需求收敛成下面的四个核心模块,每个模块都是一个独立页面:模块核心功能设计要点健康打卡记录每日血压、血糖、体重数字输入用大数字键盘,结果用颜色分级提示用药提醒设置早中晚用药时间,到点提醒声音大、文字大、重复提醒直到确认健康指南按分类展示饮食、运动建议图文卡片,每张卡只讲一件事我的档案查看历史记录、修改个人信息简单列表,支持导出和分享这四件事对应的页面层级很清楚:首页四个大卡片 → 二级功能页 → 三级详情页。全应用最多三层,不能让长辈迷路。2.3 为什么必须避免“设计感过强”的交互我见过不少同类产品喜欢用滑动手势、长按操作、下沉式菜单这些现代交互。但在老年场景里,这些全是坑:滑动切换 Tab,长辈很容易误触,一下就滑走了。长按呼出菜单,他们根本不知道还能这样操作。隐藏式导航,他们找不到返回按钮。所以我把所有交互都设为“点击可见按钮”,按钮高度至少 56dp,间距至少 12dp。这不是设计保守,是实打实的用户需求。后面实现的时候你会发现,Flutter 做这些大按钮和固定布局非常顺手。3. 环境准备:Flutter 跨平台鸿蒙开发的第一道坎这可能是整个项目里新人最容易卡住的地方。不是 Flutter 本身难装,而是“Flutter 跑鸿蒙”需要额外配置一层鸿蒙 SDK,很多人没搞清楚这个链条,装到一半就放弃了。3.1 工具链清单:四样东西缺一不可要做 Flutter 跨平台鸿蒙开发,你本机需要准备:Flutter SDK(3.x 版本,建议 3.19 及以上,太老的版本对鸿蒙适配不友好)鸿蒙 SDK(通过 DevEco Studio 里的 SDK Manager 下载,或者命令行方式下载)DevEco Studio(鸿蒙官方 IDE,主要用来管理 SDK、签名、跑模拟器)一个可用的鸿蒙模拟器或真机链条是:Flutter 工程 → Flutter 的 ohos 适配层 → 鸿蒙 SDK → 生成 hap 包。任何一个环节版本不对,命令就会报一些很奇怪的错误。3.2 配置细节:环境变量和命令行启用拿到工具后,先把鸿蒙 SDK 路径配置好。我用的命令大致是这样的,注意 SDK 路径不要带空格,否则后面构建容易出问题:# 设置鸿蒙 SDK 路径 export LOCAL_HOS_SDK_PATH=/你的路径/ohos-sdk # 启用 Flutter 的鸿蒙通道 flutter config --enable-ohos # 查看是否生效 flutter doctor如果flutter doctor里出现OHOS相关条目,说明适配层认到了。这一步做完,新建项目时就可以指定平台:flutter create --platforms ohos,android,ios health_guide_app这里我会同时保留 android 和 ios 平台,方便以后多端打包。项目创建之后,目录里会出现一个ohos文件夹,里面是鸿蒙平台的壳工程,Flutter 代码依然写在lib目录里,不用动壳工程。3.3 我踩过的环境坑:版本不匹配和签名缺失第一条:Flutter SDK 和鸿蒙 SDK 版本要匹配,不是说“越新越好”。我用最新版 Flutter 配上一个较老的鸿蒙 SDK,结果构建时报了一堆 C++ 符号找不到。后来把鸿蒙 SDK 升到和 Flutter 适配层要求的版本,问题才消失。第二条:直接命令行flutter run -d ohos跑真机时,会提示签名问题。鸿蒙真机安装应用需要签名配置,不像安卓 debug 模式自动帮你签。需要先去 DevEco Studio 里配置好自动签名,再回命令行运行。第三条:模拟器启动很慢,而且某些版本的模拟器对 Flutter 渲染支持有问题,界面会花屏。我的解决办法是:日常逻辑调试用安卓模拟器跑同一个工程,鸿蒙真机只做集成验证和性能测试。反正核心 UI 是同一套代码,这样省很多时间。4. 落地核心界面:大字、高对比、大点击区环境通了以后,别急着写业务逻辑,先把“老年友好”这个主题的界面基座打好。这一部分就是你交作业时最能拿出来展示的东西。4.1 全局文字缩放:TextScaler 的正确打开方式老年应用的字体不能只靠“加大字号”这种物理操作,还要跟随系统字体设置一起缩放。Flutter 里控制全局尺寸缩放的关键是MediaQuery.textScaler。但有个坑:如果全局自定义了超大textScaler,列表页的卡片高度、文字换行都会乱套。我采用的方法是,设置一个“温和的最大缩放值”,并且对关键文本用maxLines和overflow做保护:MaterialApp( title: '长辈健康指南', builder: (context, child) { final mediaQueryData = MediaQuery.of(context); final scale = mediaQueryData.textScaler.scale(1.0).clamp(1.0, 1.8); return MediaQuery( data: mediaQueryData.copyWith( textScaler: TextScaler.linear(scale), ), child: child!, ); }, theme: _buildTheme(), home: const HomePage(), );注意:clamp到 1.8 不是说只允许放大到 1.8 倍,而是提供一个上限,防止用户在系统层面把字体调到离谱时,界面直接变得不可用。我的实测经验是 1.6~2.0 之间最安全,既能“跟随系统”,又不至于把布局撑爆。4.2 高对比度主题:不要纯白背景也不要纯黑文字长辈的视力普遍下降,对低对比度内容的识别很差。但高对比不等于“纯黑底白字”,那个看久了眼睛累。我最后用的是暖白色背景配深褐色文字,重点按钮用深橙色,整体类似报纸的阅读质感。主题定义我单独抽了一个函数,下面是一个可以照抄的简化版:ThemeData _buildTheme() { final base = ThemeData.light(); return base.copyWith( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFFE8590C), // 深橙 brightness: Brightness.light, ), scaffoldBackgroundColor: const Color(0xFFFDF6EC), textTheme: base.textTheme.apply( bodyColor: const Color(0xFF3E2723), displayColor: const Color(0xFF3E2723), ), ); }实际运行时我发现:按钮上的文字必须比背景更重,比如深橙底上用白色文字没问题,但如果按钮变成浅橙底,就一定要配深褐文字,否则长辈会看不清按钮上的字。这条规则我写进了项目的 UI 检查清单里。4.3 大按钮与大卡片组件:顺手解决误触和点击反馈这个项目里我基本不用原生ElevatedButton,而是封装了一个BigActionCard,统一处理大尺寸、圆角、阴影和点击波纹:class BigActionCard extends StatelessWidget { final String title; final String subtitle; final IconData icon; final VoidCallback onTap; const BigActionCard({ super.key, required this.title, required this.subtitle, required this.icon, required this.onTap, }); @override Widget build(BuildContext context) { return Semantics( button: true, label: '$title,$subtitle', child: Material( color: Colors.white, borderRadius: BorderRadius.circular(20), elevation: 2, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Container( height: 120, padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Icon(icon, size: 44, color: const Color(0xFFE8590C)), const SizedBox(width: 16), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(subtitle, style: const TextStyle(fontSize: 16, color: Color(0xFF6D5A50)), maxLines: 1, overflow: TextOverflow.ellipsis), ], ), ), const Icon(Icons.chevron_right, size: 32, color: Color(0xFFB8A79C)), ], ), ), ), ), ); } }封装这个组件的经验之谈:一定要加上Semantics。长辈用系统自带的“屏幕朗读”功能时,没有语义标签的卡片读出来就是一串乱码。顺便说一句,很多团队做老年应用完全忽略无障碍,这是大忌。首页四个大卡片排列方式也不用花哨,两行两列,每张卡片高度统一 120dp,间距 16dp。长辈的手指一般比较粗,120dp 的高度加全屏宽度,误触概率能压到最后。5. 本地数据与业务逻辑:健康记录和用药提醒界面骨架搭好后,开始处理实际数据。老年健康应用有个特殊要求:长辈不一定常联网,而且对“数据传到网上”有天然顾虑。所以我的方案是默认本地存储,如果需要同步,用户手动操作。5.1 为什么选 shared_preferences + sqflite 双保险项目里我同时用了两个存储方案:shared_preferences存用户设置、最后一次打卡日期、用药提醒的开关状态。sqflite存历史健康记录,因为记录会越来越多,需要结构化查询。为什么不直接全用数据库?因为提醒状态和“今天是否打卡”这类键值对用数据库反而是杀鸡用牛刀,而且 shared_preferences 读取极快,应用启动时能立刻显示今天的状态。历史记录才值得进数据库。5.2 健康打卡的存储结构与读写我建了一张非常简单的表:CREATE TABLE health_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, type TEXT NOT NULL, value REAL NOT NULL, note TEXT, created_at TEXT NOT NULL );type字段存blood_pressure、blood_sugar、weight这类枚举值。date用yyyy-MM-dd字符串,好排序也好查重。同一天同类型只允许记录一次,如果用户再点打卡,就提示“今天已经记录过了,可以去档案里修改”。写记录的 Dart 代码逻辑不复杂,核心是异步操作数据库,注意不要阻塞 UI:Future<void> upsertRecord(HealthRecord record) async { final db = await _db; await db.insert( 'health_records', record.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); }做这个模块时最容易翻车的地方是date的时区。如果你直接用DateTime.now()存,东八区和 UTC 混在一起,第二天打开应用会出现“昨天记录没了”的灵异事件。我最后统一用本地时区格式化后的字符串,不存时间戳。5.3 用药提醒的本地通知方案用药提醒这个功能,理想情况是接入各厂商的推送通道,但本地应用没有那么强的后端支撑,而且长辈完全离线也要能用。所以我选了本地通知方案。Flutter 侧我用的是flutter_local_notifications,这个插件的跨平台支持已经比较成熟。鸿蒙端我实测发现,基础的通知弹窗是可以用的,但要记得在鸿蒙壳工程里申请通知权限,否则没有弹窗。初始化通知的代码我提炼成了一个 service:class ReminderService { static final ReminderService instance = ReminderService._(); Future<void> init() async { final plugin = FlutterLocalNotificationsPlugin(); await plugin.initialize( const InitializationSettings( android: AndroidInitializationSettings('app_icon'), iOS: DarwinInitializationSettings(), ), ); // 请求通知权限 await plugin .resolvePlatformSpecificImplementation <IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions(alert: true, badge: true, sound: true); await plugin .resolvePlatformSpecificImplementation <AndroidFlutterLocalNotificationsPlugin>() ?.requestNotificationsPermission(); } Future<void> scheduleDailyReminder({ required int id, required TimeOfDay time, required String title, required String body, }) async { final plugin = FlutterLocalNotificationsPlugin(); await plugin.zonedSchedule( id, title, body, _nextInstanceOfTime(time), const NotificationDetails( android: AndroidNotificationDetails( 'medication_reminder', '用药提醒', channelDescription: '每日用药时间提醒', importance: Importance.high, priority: Priority.high, ), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, ); } }用药提醒的逻辑一定要加“重复提醒”。很多长辈吃了药就忘,单纯弹一次根本不够。我把同一条提醒设为每 10 分钟重复一次,直到用户点击“我已吃药”。这一点是在和客户反复确认后才定的,也是整个应用里长辈反馈最好的功能。5.4 周历和饮食建议这些容易忽略的细节历史记录列表里我加了一个简单的“周视图”小模块,按周显示每天的打卡状态。这里有个常见的坑:中国习惯周一作为一周开始,而很多库默认周日开始。我在处理周计算时显式指定了周起始日,否则长辈看到的周历和你以为的完全对不上。健康指南的饮食建议我做得更“笨”一些:每条建议就是一个卡片,包含一个标题、一段话、一张插图。不搞信息流、不做推荐算法。因为内容多了长辈根本看不过来,而且医疗健康类内容一旦“智能推荐”出错,责任很大。所有内容都声明“仅供参考,具体请遵医嘱”。这一点大家做健康类应用时一定要守住底线。6. 鸿蒙适配与权限:从 Android 思维里跳出来很多从安卓转过来的开发者,到了鸿蒙上第一反应是“这不就是安卓吗”。实际上鸿蒙的权限模型、生命周期和应用沙箱都有自己的脾气,尤其是从 Flutter 层调用原生的部分,需要单独适配。6.1 权限声明:不要只改 AndroidManifest如果你打算让同一个 Flutter 工程同时支持安卓和鸿蒙,那么安卓的权限写在android/app/src/main/AndroidManifest.xml里,鸿蒙的权限则要写在ohos壳工程的module.json5里。两个文件互不相干,少写一个就等着运行时静默失败。以相机拍照记录药品或食物为例,鸿蒙端要声明类似下面的权限(不同的鸿蒙 SDK 版本,权限字符串会有差异,以实际版本为准):{ module: { requestPermissions: [ { name: "ohos.permission.CAMERA", reason: "用于拍摄药品包装和食物照片", usedScene: { abilities: ["EntryAbility"], when: "inuse" } } ] } }这里最坑的地方是reason必须写清楚用途,否则上架审核时会被打回。我在第一次提交时就因为 reason 写得模糊被驳回过一次。6.2 返回键、手势和系统字体:体验差异比你想的大安卓的返回键逻辑大家已经很熟了,但鸿蒙设备的交互有差异,尤其是面向长辈的场景:有些鸿蒙设备没有三大金刚键,靠的是侧滑返回手势。如果应用内用了自定义手势,很容易和系统手势冲突。鸿蒙的“大字体”设置档位比安卓更激进,系统最大字体下,小屏手机的 Flutter 布局可能溢出。我的应对办法前面已经说过,用textScaler限制最大缩放,同时所有文本组件都加maxLines兜底。鸿蒙的部分平板设备默认开启了“长辈模式”,系统字体、图标、触摸响应区域都会变大。你的应用如果能跟随系统字体缩放,在这类设备上的表现会非常自然。6.3 真机测试:至少准备三台不同形态的设备我测试时用了三种设备:一台小屏直板手机、一台大屏平板、一台折叠屏设备。为什么一定要这么测?小屏手机主要看布局会不会溢出、按钮能不能点到。大屏平板主要看卡片要不要跟着铺满,还要不要限制最大宽度。折叠屏的坑在于展开和折叠的尺寸变化,虽然 Flutter 会触发重建,但如果你在初始化时缓存了尺寸,界面就会错乱。真机测试中我发现 Flutter 在鸿蒙平板上有时候状态栏会顶到界面,解决方法是获取系统安全区域并设置 padding,不要用一堆写死的间距:final SafeArea( child: Scaffold(...) );这个看起来不起眼,但在不同屏幕形态的鸿蒙设备上特别管用。6.4 插件兼容性的排查方法鸿蒙上的 Flutter 插件生态还没有安卓那么完善。我遇到过一个很实际的问题:某个图像处理插件在安卓上正常,在鸿蒙上却直接报“MissingPluginException”。排查思路是:查看插件是否声明了 ohos 平台的实现。到 pub.dev 看插件的支持平台列表。如果插件不支持,就去插件仓库找是否有 OpenHarmony 分支,或者用我们设计的“健康打卡”里最简单的方案:只使用camera插件的基础拍照能力,不碰复杂的滤镜和图像分析。实操心得:遇到插件不兼容,先别急着换插件,很多复杂插件的核心功能用系统已有能力就能实现,或者用 MethodChannel 自己写一个薄壳调用鸿蒙原生接口,也就百来行代码。这比强行适配一个不支持 ohos 的插件省事得多。7. 打包发布与上架自测清单最后一步是把应用从开发机带到用户的设备上。Flutter 跨平台鸿蒙开发的打包流程和安卓不完全一样,但熟悉之后也没什么玄学。7.1 生成 hap 包和签名配置本地调试用的是自动签名,但发布版需要正式的签名文件。流程如下:在 DevEco Studio 里生成签名证书,下载到本地。在鸿蒙壳工程里配置签名文件路径。命令行执行构建:flutter build hap --release构建完成后,产物在build/ohos/release/目录下,后缀是.hap。这个包就可以拿去应用市场做测试分发或上架了。第一次构建 hap 包通常会有几类报错:证书过期或路径不对,检查签名配置。包名和签名不一致,检查应用市场上的包名和本地签名是否匹配。代码里有插件找不到鸿蒙端实现,构建时不会报错,但运行时会崩,所以上架前最好做一次全面回归。7.2 上架前的自测清单:我每次都会跑一遍我把这个项目沉淀成了一份自测清单,不复杂,但很有效:[ ] 系统字体调到最大,界面上所有按钮是否可点、文字是否溢出。[ ] 开启屏幕朗读模式,操作一遍核心流程,看看所有控件是否有语义标签。[ ] 断网状态下,是否还能完成健康打卡、查看历史记录。[ ] 用药提醒设置后,锁屏状态下是否准时弹出。[ ] 平板、折叠屏、小屏手机三台设备各跑一遍,看布局是否错乱。[ ] 连续打卡 7 天,检查周历和统计是否正常。[ ] 拍照权限、通知权限都拒绝一次再进入应用,不会闪退。这套清单不依赖任何自动化测试脚本,纯手工跑,但在老年健康这个场景里,手工实测比自动化用例更能发现问题。因为用户不是标准的“测试用户”,他们会做出各种意想不到的操作。7.3 后续迭代:心率插件、多语言、云同步这个项目上线后,客户又提了三个新方向:对接心率手环,这需要原生插件配合,鸿蒙端的设备协议不统一,是个硬骨头。增加语音提示,长辈视力不好时点击按钮能朗读内容。多端云同步,让子女能看到父母的健康记录。这些方向我都会优先在 Flutter 层做能力预留,比如日程数据结构里预留server_id字段,后续接云同步时不用改表结构。语音提示用 Flutter 自带的flutter_tts做原型,鸿蒙端如果不行,就通过 MethodChannel 调系统语音,整体方案已经有眉目了。最后分享一个小技巧这个项目做下来,我最想留给大家的一条经验是:做老年健康类应用,永远不要用年轻开发者的视觉标准去验收界面。每次改完一个页面,把手机字体调到最大,戴上老花镜模拟一下视力模糊的状态,再去点那些按钮。你会发现自己觉得挺好看的界面,在真实长辈眼里可能全是灾难。我在测试时养成了一个习惯:把设计稿里的初级灰文字全部换成深褐或深灰,把低于 48dp 高度的可点击元素全部放大,把一切的“双击”“长按”“滑动”全部改回单击。这些改动在代码里可能就是一个属性、一个变量的事,但对使用这个应用的长辈来说,体验差别是天壤之别。Flutter 的好处是这些调整写一次,所有平台同步生效,这也是我敢把这个项目定成跨平台方案并坚持到最后的底气。