大家好,我是一个小透明,也就是你们能在各种技术论坛里见到的,ID万年不变,头像可能是猫也可能是动漫人物的典型技术爱好者🤡。
不知不觉,代码敲了十几年,身体的一些指标也开始悄悄亮起红灯。这不,前阵子体检报告一出来,尿酸那一栏的箭头,红得刺眼,红得让我心慌。
医生的话说得轻描淡写,却字字戳心:「少吃海鲜,戒酒,多喝水,多排尿,促进代谢。」 前两条还好,毕竟我也开始有意识地调整生活习惯。但「多喝水多排尿」这事儿,说起来简单,执行起来却成了我的新难题。工作一忙,别说喝水,连上厕所都得憋到最后一刻。等想起来要记录每天的饮水和排尿次数时,早就忘得一干二净。用 App 手动记?得了吧,能坚持三天以上的App,除了微信,恐怕就没几个了。至少我是这样的。
上个周末,我正百无聊赖地刷着小红书,一条关于鸿蒙「一碰连」的分享视频突然弹了出来。视频里,手机轻轻一碰NFC贴纸,家里的灯就开了、咖啡机开始工作。我脑子里「叮」地一下,亮了。如果……如果我把NFC贴片贴在厕所门上,每次进去用手机一碰,不就能自动记录一次「排泄事件」了吗?这个念头一旦冒出来,就跟藤蔓似的在我心里疯长……💩
一个属于我自己的,甚至可以说是为我量身定做的「活水计划」App的雏形,就这么诞生了。
我决定,就用鸿蒙,来把这个想法折腾出来。不为上架,不为盈利,就为了解决自己一个非常朴素的健康需求,也为了体验一把,传说中「万物互联」的鸿蒙开发,到底是个什么滋味。今天,我就把这次虽然可能没有「善终」但绝对有趣的开发心得,分享给大家。
鸿蒙,一个既熟悉又陌生的新世界
在动手之前,我必须承认,我对鸿蒙的了解,更多停留在发布会的PPT和数码博主的评测视频里。什么分布式、原子化服务、一次开发多端部署……这些概念听着很「性感」,但对于一个习惯了在其他IDE里「搬砖」的开发者来说,它到底好不好用,还得自己上手试试。
我为这个「活水记录」App设定的功能逻辑异常清晰:
- NFC触发:手机解锁状态下,进入厕所时碰一下门上的NFC贴片。
- 事件选择:手机立刻弹出一个对话框,让我选择「小便」还是「大便」。
- 不同流程:选「小便」,App自动记录一次事件,并弹出一个15秒的洗手倒计时(可选)。选「大便」,App启动一个5分钟倒计时,防止我「带薪如厕」过久伤身体,倒计时结束振动提醒,并自动记录事件。
- 数据可视化:在App主界面或者桌面卡片上,能看到今天喝了多少水(手动记)、排了多少次尿。
这个需求听起来不复杂,但它完美地契合了我对智能生活的所有幻想:无感、自动、恰到好处的提醒。作为一个开源精神的拥护者,鸿蒙开放的文档和社区资源也是吸引我的重要一点。由华为维护,整个生态系统在飞速演进,这给了我这种「野生开发者」极大的信心。开干!
DevEco Studio:第一声「Hello World」的亲切感
万事开头难,但鸿蒙的开发环境搭建却异常顺滑。下载安装DevEco Studio,过程和安装其他IDE没什么两样,一路「下一步」即可。IDE基于IntelliJ IDEA,对于老开发者来说,这界面简直不要太亲切。
我选择创建一个新的「空Ability」项目,语言默认选择了ArkTS。ArkTS是TypeScript的超集,对于有前端开发经验的人来说,几乎是零学习成本。这种感觉很奇妙,你明明在开发一个原生App,写的却是类似Web开发的代码,但性能和体验又是实打实的原生级别。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
const TAG = 'StageAbility';
export default class StageAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onCreate');
}
// ... [其他生命周期函数]
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, TAG, 'Succeeded in loading the content.');
});
}
}这就是项目入口文件的样子,干净、直接。当我点击预览器按钮,一个「Hello World」的界面几乎是秒级呈现在虚拟手机上时,我心里那股折腾的劲儿彻底被点燃了。不得不说,这种所见即所得的开发体验,对开发者实在是太友好了。

核心功能探索:痛并快乐的啃骨头之旅
好了,环境搭好了,接下来就是啃硬骨头了。我的「活水计划」核心在于自动化,而自动化的起点,就是NFC。
1. 「万物一碰」的起点:NFC标签读取(的艰难尝试)
我做的第一件事,就是去翻阅鸿蒙官方文档,寻找NFC相关的API。果然,在「连接与通信」模块下,我找到了@ohos.nfc.tag。要实现我的「无感」记录,关键在于App必须能在后台甚至锁屏时响应NFC。在鸿蒙开发中,大部分流程都遵循着「声明权限 → 导入模块 → 调用API」三部曲。
首先,在module.json5配置文件里声明权限:
// module.json5
{
"module": {
// ... 其他配置
"requestPermissions": [
{
"name": "ohos.permission.NFC_TAG"
}
]
}
}
然后,真正的挑战来了。根据我的理解,要实现后台响应,需要用到一个叫做「WantAgent」的机制。它像一个「委托人」,可以代表你的应用在特定事件发生时执行操作。下面是我根据文档和社区帖子,设计的理论代码:
// 这是我设计的NFC事件监听逻辑(概念代码)
import nfc from '@ohos.nfc.tag';
import wantAgent from '@ohos.wantAgent';
// ...
async function registerNfcListener() {
// 1. 定义一个WantAgentInfo,指定NFC触发后要干什么
const wantAgentInfo = {
wants: [
{
bundleName: "com.yourcompany.healthlog",
abilityName: "EntryAbility",
parameters: { "source": "nfc_trigger" } // 传递参数,告知应用是NFC拉起的
}
],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
};
try {
// 2. 异步获取WantAgent对象
const agent = await wantAgent.getWantAgent(wantAgentInfo);
// 3. 理论上,使用这个agent启动NFC发现,就能在后台响应
// 注意:startNfcDiscovery在实际测试中发现更适用于前台
// 要实现全局后台感应,需要更复杂的配置或系统级权限,这是我遇到的第一个主要卡点。
nfc.startNfcDiscovery(agent);
console.log('NFC discovery registration attempted.');
} catch (error) {
console.error(`Error setting up NFC listener: ${JSON.stringify(error)}`);
}
}
然而,现实是骨感的。我很快发现,要实现全局的后台NFC感应,远比调用一个API要复杂。这涉及到更深层的系统权限和应用类型配置,对于个人开发者的小项目来说,门槛相当高。这里我学到了重要一课:鸿蒙对后台行为和权限的管控非常严格,任何「小动作」都得通过WantAgent或BackgroundTaskManager这类「正规渠道」申请,这既是约束,也是对用户的保护。于是我调整了目标:先实现App在前台时能响应NFC。至少,技术路径是通的!

2. UI交互:ArkUI的声明式之美与「灵魂拷问」对话框
当NFC触发的逻辑建立后(哪怕只是前台),我需要一个对话框来问用户:「是大的还是小的?」这一点上,ArkUI的声明式UI写法,带来了巨大的愉悦感。以最简单的对话框为例:
// 在pages/Index.ets中
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct Index {
// ...
// 这个函数用于显示选择对话框
showToiletDialog() {
promptAction.showDialog({
title: '如厕记录',
message: '请选择您的如厕类型:',
buttons: [
{ text: '小便💦', color: '#007DFF' },
{ text: '大便💩', color: '#333333' }
]
}).then(result => {
if (result.index === 0) {
// 用户选择了小便
this.handleUrination();
} else {
// 用户选择了大便
this.handleBowelMovement();
}
}).catch(err => {
console.log('Dialog dismissed.');
});
}
// ...
}
你看,调用一个showDialog,传入配置对象,然后用Promise处理回调。整个过程行云流水,完全没有传统开发方式中操作UI控件的繁琐感。这种「你只管改数据,UI自然会更新」的开发体验,一旦习惯了,真的回不去。这部分代码是我在整个项目里写得最顺畅的。当然,也可以让AI先搓一个整体的框架出来,再慢慢微调,效率更高。
3. 数据写入:从云端「劝退」到本地深耕
这是我整个开发过程中,遇到的最大挑战,也是一个重要的转折点。我最初的宏伟蓝图是把数据写进系统的「健康」App里,这需要使用@kit.HealthServiceKit这个运动健康服务。
然而,当我兴冲冲地查阅文档时,两个「拦路虎」出现了。首先,要接入运动健康服务,似乎需要单独申请开发者资质,流程比想象中复杂。其次,我翻遍了所有预设的数据类型,压根就没找到「小便次数」或「排尿量」这类指标。这意味着,我的核心需求和官方提供的能力根本不匹配。


理想很丰满,现实很骨感。在碰壁之后,我果断放弃了不切实际的幻想。条条大路通罗马,既然不能「上云」,那我就把数据踏踏实实地「种」在本地。我决定使用鸿蒙提供的轻量级键值对存储方案 @ohos.data.preferences 来记录数据。它对于我这个小应用来说,不仅绰绰有余,而且数据完全掌握在自己手里,更加安心。
我的新计划是:在本地存储一个JSON数组,每次如厕就往这个数组里追加一条记录。
import data_preferences from '@ohos.data.preferences';
import promptAction from '@ohos.promptAction';
// 假设在UI组件的某个方法中调用
// 需要传入context,通常是 this.context
async function writeUrinationRecordToLocal(context) {
const PREFERENCES_KEY = 'urination_records';
const STORE_NAME = 'health_log_store';
try {
// 1. 获取 preferences 实例
const pref = await data_preferences.getPreferences(context, STORE_NAME);
// 2. 读取旧的记录(它是一个JSON字符串)
const recordsJson = await pref.get(PREFERENCES_KEY, '[]') as string; // 如果没有,默认为空数组字符串
const records = JSON.parse(recordsJson);
// 3. 创建一条新纪录并追加
const newRecord = {
type: 'urination',
timestamp: new Date().getTime()
};
records.push(newRecord);
// 4. 将更新后的数组转回JSON字符串,并写入存储
await pref.put(PREFERENCES_KEY, JSON.stringify(records));
await pref.flush(); // 确保存储
console.log('Successfully wrote urination record to local preferences.');
promptAction.showToast({ message: '小便记录成功!' });
} catch (error) {
console.error(`Failed to write to preferences: ${JSON.stringify(error)}`);
promptAction.showToast({ message: '记录失败,请稍后重试' });
}
}
这几行代码的逻辑清晰多了:获取存储实例、读取旧数据、追加新数据、写回。整个过程完全在应用内部闭环,没有任何外部依赖和复杂的权限申请。这番折腾下来,虽然没能用上高大上的系统级服务,但却让我对应用数据存储有了更实际的认识。有时候,最直接、最简单的方案,反而是最有效的。
扩展畅想:一个桌面卡片带来的「小确幸」
虽然核心功能还没完全跑通,但我还是忍不住去探索了鸿蒙的服务卡片(Form)机制。我想,如果能做一个2x2的桌面卡片,实时显示今天的小便次数,那体验就完美了。这需要创建一个FormExtensionAbility。
根据文档,我设计了卡片能力的蓝图:
// HealthWidgetCard.ts (FormExtensionAbility的设想)
import FormExtension from '@ohos.app.form.FormExtensionAbility';
import formProvider from '@ohos.app.form.formProvider';
export default class HealthWidgetCard extends FormExtension {
// 当用户添加卡片时触发
onAddForm(want) {
// 理想情况下,从我们本地的preferences中读取今日次数
const count = this.queryTodayCountFromLocalDB();
const formData = {
"count": count.toString() // 传递给卡片UI的数据
};
// 创建并返回卡片数据
return formProvider.createFormBindingData(formData);
}
// 当数据变化时,可以调用这个方法主动更新所有卡片
updateForms() {
// ...
}
}
现在很多iOS和Android应用,已经把桌面卡片玩成了核心卖点:它的层级和应用图标一样高,能即时展示核心信息,兼具美观和实用,本质上就是个「超级图标」。从这个层面来看,我是真心希望自己的应用也能拥有服务卡片的。
虽然我没有真正实现这个卡片并看到它在桌面上跳动,但研究它的过程,让我深刻理解了鸿蒙的「原子化服务」理念。一个App不再是铁板一块,而是由多个可以独立运行、可以「亮出来」的Ability组成。桌面卡片,就是App核心能力的一种「外显」。
结尾:一次未完成的旅程,也是一次全新的开始
所以,我的「活水计划」App最终成功运行了吗?没有。它现在还静静地躺在我的开发机里,NFC后台触发的难题尚未攻克,服务卡片也还停留在设想阶段。
要说有什么遗憾吗?有的。但这个遗憾,并非来自鸿蒙本身,更多的是源于探索一个年轻生态系统时的必然经历。很多疑难杂症,你没法像在Stack Overflow上搜iOS/Android问题那样,一搜就有十几个不同角度的解决方案。很多时候,你得做那个第一个「吃螃蟹」的人,去啃文档,去开发者论坛提问,去等待官方的解答。
但这又何尝不是一种乐趣呢?几年前的我觉得,一个操作系统,稳定、流畅、应用多就够了。如今,当我作为一个开发者,真正深入到鸿蒙的体系里时,我感受到的是一种可能性。它试图通过分布式、原子化这些底层设计,去打破设备间的壁垒,让服务跟着人走。比起「更优」,在当下这个节点,我更喜欢「不一样」的答案。
这次开发经历,没有给我带来一个可用的应用,但它让我对「操作系统」四个字有了新的理解。我不再仅仅是它的使用者,也成为了它的共建者,哪怕我的贡献,只是一个解决自己健康焦虑的、充满BUG的小工具原型。
最后,对于所有对鸿蒙开发感兴趣的朋友,我的建议是:别犹豫,别只是看。去下载DevEco Studio,去写下你的第一行ArkTS代码。你会遇到问题,会卡住,甚至会失败。但这个过程本身,就是最有价值的收获。鸿蒙无法提供给你一个适用于所有人的现成方案,但它提供了一个足够宽广的舞台,让你能基于自己的设备和需求,去创造真正属于你的「活水」。
所以,祝大家玩得愉快!
