1. 项目缘起:从“落叶归根”这个意象聊起
1.1 这个App到底想解决什么问题
我做这个项目的最初动机,其实源于一次非常个人的体验。去年秋天,我坐在公园长椅上,看着树叶一片片落下,脑子里却还在想着工作群的未读消息、技术方案评审的争论、以及某个即将上线的版本Flutter编译报错。那一刻我意识到,我们的大脑已经被碎片化信息训练得像个永不停歇的弹幕机,连看落叶这种本该放空的事情,都变成了焦虑的背景音。
“落叶归根”这个App的名字,就是从那个场景里蹦出来的。我想做一个工具,它不追求功能堆砌,不强迫你社交打卡,也不搞什么冥想排行榜。它的核心目标只有一个:在你被思绪淹没的时候,用最简单优雅的方式,帮你把注意力拉回到当下这一刻。具体来说,就是提供一个轻量级的“正念日记”+“引导冥想”组合,让用户能随时记录当下的感受,或者跟着一段音频做几分钟的呼吸练习。
技术选型上,我一开始就锁定了Flutter。原因很简单:我需要同时覆盖Android、iOS和OpenHarmony三个平台,而Flutter的跨平台能力加上近年对OpenHarmony的官方支持,让我能用一个代码库搞定三端,不用为每个平台重复造轮子。而且Flutter的widget体系非常适合做这种需要精致动画和细腻交互的冥想类应用,它的渲染性能在移动端已经足够流畅。
1.2 为什么“落叶”这个意象贯穿了整个设计
很多人问我,为什么不直接叫“正念日记”或者“冥想助手”,而要起一个这么文艺的名字。其实“落叶归根”这四个字,非常精准地概括了这个App的核心交互逻辑。
- 落叶:代表你脑海中那些纷乱的思绪。每当你感到焦虑、烦躁、或者走神时,你可以在App里“落下”一片叶子。这片叶子可以是一段简短的文字记录,也可以是一个语音片段,或者只是一个简单的情绪标记(比如“烦躁”、“焦虑”、“平静”)。这个动作本身,就是一次“觉察”——你意识到自己走神了,这就是正念的第一步。
- 归根:代表你被引导着回到当下。App会根据你“落下”的叶子,提供针对性的回归练习。比如你标记了“焦虑”,App会推荐一个两分钟的呼吸练习;你标记了“烦躁”,可能会推荐一个身体扫描音频。这些练习的设计哲学是“轻量、不打扰、随时可停”,确保用户在任何碎片时间都能完成一次回归。
从技术实现角度看,这个设计让App的数据模型非常清晰:一个“叶子”就是一个记录对象,包含时间戳、情绪标签、内容(文本或音频路径)、以及完成状态(是否已进行回归练习)。数据库设计只有两张表:叶子表(Leaf)和练习记录表(Practice)。这种极简结构让后续的跨平台状态管理变得非常容易,后续我会详细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型决策:为什么是Flutter + OpenHarmony,以及三端架构如何统一
2.1 Flutter对OpenHarmony的支持现状
在2023年之前,Flutter官方对OpenHarmony的支持还处于实验阶段,社区里有一批开发者用定制的引擎在跑,但稳定性堪忧。不过从Flutter 3.16开始,官方正式将OpenHarmony列为受支持的平台之一,在flutter create时就能直接选择--platforms=ohos。这个变化对我来说是决定性的,意味着我不需要再维护一个分支版本的Flutter引擎,可以直接使用官方的工具链。
当前(基于Flutter 3.22+)的情况是:OpenHarmony支持已经比较完善,包括Dart语言的编译、Flutter引擎的渲染(使用Skia图形库)、以及插件系统的适配。不过有一个关键点:OpenHarmony的API与Android并不完全兼容,所以很多Android原生插件(比如高德地图、支付SDK)是无法直接使用的。我在做“落叶归根”时,有意识地避开了依赖第三方原生插件的功能,所有核心能力(UI、音频播放、本地存储、文件管理)都使用Flutter内置插件或纯Dart实现,这样在OpenHarmony上几乎零适配成本。
2.2 三端架构的统一设计
“三端”在我的定义里是:Android(手机/平板)、iOS(iPhone/iPad)、OpenHarmony(主要是华为平板和部分开发板,如RK3566)。这三者的屏幕尺寸、交互方式、系统特性都有差异,但核心诉求一致:用户要在任何设备上获得相同的“回归当下”体验。
我的架构采用了经典的分层设计:
- 数据层:使用
shared_preferences存储简单配置(如主题色、是否已引导),使用hive作为本地数据库存储叶子记录。hive是一个纯Dart的键值存储,不需要任何原生依赖,三端通用。对于音频文件,我使用path_provider获取应用文档目录,然后通过dart:io拷贝预设音频文件。 - 业务逻辑层:使用
provider进行状态管理。每个叶子记录、计时器状态、音频播放状态都通过ChangeNotifier暴露给UI层。provider在Flutter生态中是最轻量的选择,而且对OpenHarmony没有任何兼容问题。 - UI层:全部使用Flutter内置widget,没有使用任何原生UI组件。对于不同平台的差异,我通过
dart:io的Platform.isAndroid、Platform.isIOS等进行判断,调整一些细微的交互行为,比如iOS上使用CupertinoPageRoute的转场动画,Android和OpenHarmony上使用MaterialPageRoute。但为了让三端体验尽可能一致,我最终统一使用了自定义的SlideTransition动画,视觉上差异很小。
2.3 为什么没有选择React Native或原生开发
我周围有朋友问过,为什么不用React Native,毕竟它也有OpenHarmony社区支持。我的判断是:Flutter在性能上更可控,特别是动画方面。冥想类App需要大量的过渡动画、呼吸指示器、粒子效果(比如模拟落叶飘落),这些在Flutter中通过AnimationController和CustomPainter可以轻松实现60fps的流畅体验,而React Native在复杂动画场景下容易出现掉帧或者需要原生桥接。另外,Flutter的hot reload在开发调试UI时效率极高,这一点对于需要反复调整视觉细节的App来说太重要了。
至于原生开发,那意味着三端要写三套代码,维护成本翻三倍。对于一个小团队(其实就我一个人)的项目来说,Flutter是唯一合理的选择。
3. 核心功能模块实现:从“落叶”到“归根”的完整链路
3.1 “落叶”记录:轻量级输入的三种方式
用户的核心交互入口是“落下一片叶子”。我设计了三类记录方式,满足不同场景:
- 快速情绪标记:用户点击主界面的一片落叶图标,会弹出一个半透明面板,上面有六个情绪标签(焦虑、烦躁、走神、疲惫、平静、感恩)。用户选择后,系统自动记录当前时间戳和情绪标签,同时生成一片落叶动画飘落到屏幕下方的“落叶堆”中。这个操作只需要两秒钟,适合在开会、排队等极度碎片化场景下使用。
- 语音记录:长按主界面中央的“树”图标,可以开始录音,松手结束。录音文件以
timestamp_leaf_audio.wav格式保存在应用文档目录下,同时生成一条叶子记录,音频路径作为附件。语音记录的好处是,你不需要打字,把脑子里的话直接说出来,效果更接近“倾倒思绪”。我使用record插件(纯Dart实现,三端兼容)来实现录音,注意在OpenHarmony上需要申请麦克风权限,我在应用启动时用permission_handler插件统一处理。 - 文字笔记:点击叶子记录会进入详情页,用户可以在文本框中写下完整的想法。这个功能适合在安静环境中进行更深入的自我觉察。文本存储也是纯Dart,没有特殊之处。
3.2 “归根”冥想:引导音频与呼吸计时器
当用户完成一次“落叶”记录后,App会在底部出现一个“归根”按钮,点击后进入冥想引导界面。这里有两个核心模式:
引导音频模式:我预置了五个短音频,长度从2分钟到10分钟不等,内容包括呼吸引导、身体扫描、正念行走等。音频文件使用assets目录打包,通过audioplayers插件播放。这个插件在OpenHarmony上的表现出乎意料地好,因为它是基于flutter_shared的原生实现,而OpenHarmony的Flutter引擎已经包含了对应的桥接代码。不过有一个坑:在OpenHarmony上,音频播放的playerState回调有时会延迟几百毫秒,导致UI上的进度条不够平滑。我的解决方案是使用Timer.periodic每100ms主动查询当前播放位置,而不是依赖状态回调,这样UI更新更可靠。
呼吸计时器模式:这是一个纯视觉的引导,屏幕上会显示一个圆形呼吸指示器,4秒吸气、4秒屏息、4秒呼气,循环进行。用户不需要听音频,只需要跟着指示器的节奏做呼吸。这个功能完全由AnimationController驱动,通过CustomPainter绘制一个不断缩放和颜色渐变的圆。我特别优化了动画的Curve,吸气时使用easeInOutQuad,屏息时使用linear(保持静止),呼气时使用easeOutQuad,整体节奏感非常自然。在低端设备上,我通过RepaintBoundary把动画区域隔离,避免因为频繁重绘导致页面卡顿。
3.3 数据同步与备份方案
这个App暂时没有做云端同步,因为用户数据是高度私密的——没有人愿意把自己的情绪记录放在别人的服务器上。所以我选择了本地存储 + 手工备份的方案。
- 所有叶子记录存储在
hive的leaves盒子中。Hive的性能非常好,1000条记录的操作耗时不到10ms,完全够用。 - 音频文件存储在应用文档目录,通过
path_provider获取。 - 备份功能:用户可以在设置中点击“导出备份”,将整个数据库(Hive的二进制文件)和所有音频文件打包成一个ZIP文件,通过
share_plus插件分享到文件管理器或云盘。恢复时,用户选择ZIP文件,App自动解压并覆盖本地数据。这个功能在Android和iOS上都很顺畅,但在OpenHarmony上发现share_plus调用系统分享时,部分设备(如开发板)没有安装文件管理器,导致分享失败。我的处理方式是:在OpenHarmony上,直接使用file_picker让用户选择备份路径,保存到公开目录,而不是依赖系统分享。
4. 三端适配的实战踩坑:那些文档没写的东西
4.1 OpenHarmony上的插件兼容性排查
虽然Flutter官方声称支持OpenHarmony,但实际开发中,我遇到了好几个插件不兼容的问题。最典型的是permission_handler插件。在Android和iOS上,它可以自动处理权限请求,但在OpenHarmony上,它调用的是Android的权限模型,而OpenHarmony有自己的权限管理系统(基于ohos.permission)。结果是:在OpenHarmony设备上,permission_handler的request()方法不会弹出系统对话框,而是直接返回denied状态。
解决方案是:手动在原生侧处理权限。我需要为OpenHarmony平台编写一个MethodChannel处理程序,在ohos目录下的MainAbility中注册通道,调用context.requestPermissionsFromUser()。代码大致如下:
dart复制// Dart端
static const platform = MethodChannel('com.leaf.return/permission');
await platform.invokeMethod('requestMicrophonePermission');
java复制// OpenHarmony端(Java)
methodChannel.setMethodCallHandler((call, result) -> {
if ("requestMicrophonePermission".equals(call.method)) {
String[] permissions = {"ohos.permission.MICROPHONE"};
requestPermissionsFromUser(permissions, 0);
result.success(true);
}
});
这个坑花了我两天时间,因为官方文档只说“插件可能需要适配”,但没有给出具体适配方案。我的经验是:在涉及系统API调用的插件(权限、通知、传感器等)时,一定要先在OpenHarmony官方的开发者文档中确认对应API的存在,然后手动编写通道代码。
4.2 UI适配:不同屏幕尺寸和圆角风格
“落叶归根”的UI设计走的是“极简禅意”风格,大量使用留白和半透明元素。但在不同屏幕尺寸上,需要精细调整间距和字体大小。我使用了MediaQuery.of(context).size来获取屏幕尺寸,然后根据宽度动态计算:
- 手机(宽度<600dp):使用小号字体(14sp-16sp),叶子动画占屏幕高度的1/4。
- 平板(宽度>=600dp):使用中号字体(18sp-20sp),叶子动画占屏幕高度的1/3,并且增加更多的落叶粒子。
- 超大屏(宽度>=900dp,比如某些OpenHarmony平板):使用大号字体(22sp-24sp),左侧增加一个“落叶堆”的列表,右侧是主交互区,实现自适应布局。
另外,OpenHarmony的默认圆角风格与Android和iOS不同。在Android上,Material Design的圆角是4dp;在iOS上,Cupertino的圆角是10dp;而在OpenHarmony上,原生应用普遍使用8dp的圆角。为了让应用看起来更“原生”,我并没有统一使用一个圆角值,而是通过Platform判断来分别设置。例如:
dart复制double get cornerRadius {
if (Platform.isAndroid) return 4.0;
if (Platform.isIOS) return 10.0;
return 8.0; // 适配OpenHarmony和其他平台
}
4.3 性能优化:在低端OpenHarmony设备上的流畅性
测试用的OpenHarmony设备是RK3566开发板,配置非常低(4核A55,2GB RAM),运行Flutter应用时,如果动画过于复杂,就会出现明显的掉帧。我主要做了以下优化:
- 减少不必要的build:使用
const构造函数创建静态widget,避免每次都重建。比如叶子动画中的落叶粒子,每个粒子都是一个LeafWidget,我将其Key设置为ValueKey(leaf.id),这样Flutter可以复用状态。 - 使用
RepaintBoundary:将动画区域(落叶飘落、呼吸指示器)包裹在RepaintBoundary中,避免整个页面重绘。同时,静态区域(如按钮、文字)保持不变。 - 降低粒子数量:在低端设备上,动态计算粒子数量。我通过
DeviceInfo插件获取设备内存,如果内存小于3GB,则落叶粒子数量从30个减少到10个,同时降低粒子更新频率(从60fps降到30fps,通过TickerProvider的vsync参数控制)。 - 音频缓存:引导音频文件加载时,使用
AudioCache预加载到内存中,避免每次播放时从磁盘读取。在低端设备上,磁盘IO性能较差,预加载能显著减少播放延迟。
5. 应用发布与后续维护:三端商店的不同规则
5.1 Android / iOS / OpenHarmony 商店审核差异
Android(Google Play):审核相对宽松,但需要注意隐私政策。我的应用涉及麦克风权限(语音记录),需要在隐私政策中明确说明如何使用、是否上传。我使用了privacy_screen插件在应用切换时模糊屏幕内容,防止敏感数据泄露。另外,Google Play要求应用支持Android 12+的exported属性,Flutter生成的AndroidManifest.xml默认已经处理好,不需要额外修改。
iOS(App Store):审核最严格。冥想类应用需要特别注意是否包含“医疗建议”或“诊断功能”。我的应用明确声明“这不是医疗工具,只是一个辅助放松的工具”,并在描述中避免使用“治疗”、“治愈”等词汇。另外,iOS对音频后台播放有限制,我需要在Info.plist中添加UIBackgroundModes键,值为audio,否则用户锁屏后音频会停止。这个配置在Flutter的ios/Runner/Info.plist中手动添加即可。
OpenHarmony(华为应用市场):审核流程与Android类似,但更强调隐私合规。华为要求应用必须提供隐私政策链接,并且不能使用未经授权的第三方SDK。由于我的应用没有使用任何华为的HMS服务,所以通过审核没有遇到障碍。不过有一个细节:OpenHarmony应用包名(bundleName)必须与oh-package.json5中的name一致,且不能包含下划线。我最初使用的包名是com_leaf_return,被审核拒绝,后来改为com.leaf.return才通过。
5.2 用户反馈与迭代方向
上线第一个月,我收到了大约200条用户反馈,主要集中在三个方向:
- 希望增加白噪音背景:很多用户在冥想时喜欢同时播放雨声或溪水声。我目前正在实现一个混音功能,使用
audio_session插件支持多音频同时播放,并允许用户选择内置的几种白噪音。 - 希望有每日提醒:用户希望App能每天定时提醒他们“落下一片叶子”。这个功能需要使用
flutter_local_notifications插件,在OpenHarmony上同样需要适配系统通知权限。我目前正在测试中。 - 希望有数据统计:比如“本周累计落叶次数”、“最常记录的情绪”等。我计划在
hive中增加一个统计数据缓存,每次记录时更新,然后在主界面添加一个“落叶统计”卡片,用简单的折线图展示趋势。
最后分享一个小技巧:在OpenHarmony上调试时,很多开发者会遇到FlutterEngine无法初始化的问题,通常是因为ohos目录下的FlutterApplication没有正确配置。检查一下entry/src/main/ets/Application/AbilityStage.ts中是否导入了FlutterApplication,以及module.json5中是否声明了FlutterApplication。我最初漏掉了这一步,导致应用在开发板上直接崩溃,花了几个小时才定位到。
整体来说,“落叶归根”这个项目让我深刻体会到,Flutter + OpenHarmony的组合已经足够成熟,可以用于生产级应用。虽然还有一些小坑,但相比原生开发的多平台维护成本,简直不值一提。如果你也在考虑做跨平台应用,特别是要覆盖OpenHarmony,Flutter绝对是首选。
