
【推荐+1】HarmonyOS官方模板优秀案例 (第9期:工具行业 · 综合工具)
💡 鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞 💡
★ 一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★
工具应用中有一款“六边形选手”,可以提供计算、笔记、记账、日历等生活中高频服务
本期为您介绍综合工具类应用的开发案例
👉 覆盖20+行业,点击查看往期案例汇总贴,持续更新,点击收藏!一键三连!常看常新!
【第9期】工具行业 · 综合工具
一、概述
1.行业洞察
1)行业诉求:
- ·用户获取与留存:用户留存率低,“用完即走”;预装和渠道推广成本高,大部分工具app手机厂商直接开发。
- 跨平台兼容性难:设备碎片化,多系统,需要分别适配;性能问题(启动慢、卡顿、崩溃);后期维护更新困难。
- 商业化与变现:广告变现效果下降、应用内购买订阅制接受度待提高。
- 数据安全与合规:隐私违规高发、安全漏洞普遍。
2)行业常用三方SDK
分类 | 三方库名称 | 功能 | SDK链接 |
登录认证 | 中国移动一键登录SDK/易盾一键登录SDK/创蓝闪验/极光安全认证/阿里云号码认证SDK/中国电信一键登录SDK | 登录 | 腾讯优量汇 高德地图 百度地图 腾讯地图定位 高德地图定位 |
分享 | 友盟/ShareSDK/微信分享/QQ分享/新浪微博SDK/MobTech ShareSDK | 统计/推送/分享 | |
支付 | 支付宝支付/微信支付/银联支付 | 支付 | |
数据分析 | 友盟移动统计SD/神策数据SDK | 数据收集、处理、分析、运用 | |
性能监控 | 腾讯Bugly SDK/听云SDK/岳鹰全景监控SDK | 异常上报和运营统计 | |
地图 | 高德地图SDK | 地图 | |
推送 | 个推/华为推送/极光PUSH/阿里推送SDK | 消息推送 | |
媒体 | 阿里云视频播放器SDK | 音视频 |
说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”
2.案例概览(下载模板)
基于以上行业分析,本期将介绍鸿蒙生态市场工具类行业模板——综合工具应用模板,为行业提供常用功能的开发案例,模板主要分首页、我的两大模块。
- Stage开发模型 + 声明式UI开发范式。
- ·分层架构设计 + 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。
- 本模板只需做少量配置和定制即可快速实现数字计算、日期查询、空调遥控、个税计算等功能。
本模板主要页面及核心功能如下所示:
综合工具
|-- 首页
| |-- 顶部操作区
| | | └-- 工具管理
| |-- 计算器
| | └-- 数字计算
| |-- 个税计算器
| | └-- 个税计算
| |-- 汇率计算器
| | |-- 汇率计算
| |-- 热量计算
| | |-- 饮食计划
| | | └-- 食物搜索
| | | └-- 食物添加
| |-- 日期计算
| | |-- 日期间隔
| | |-- 日期计算
| |-- 黄历
| | |-- 黄历查看
| |-- 万能空调遥控器
| | |-- 遥控器列表
| |-- 房贷计算器
| | |-- 房贷计算
| |-- 科学计算器
| | └-- 科学计算
| |-- 倒计日
| | └-- 倒计日列表
| |-- 记账
| | └-- 账单统计
| |-- 隐私笔记
| | └-- 添加笔记
| |-- 图片水印
| | └-- 编辑水印
| | └-- 历史记录
| |-- 手机NFC
| | └-- 读取门禁卡
| | └-- 读取公交卡
| | └-- 读取银行卡
| | └-- 克隆卡片
| |-- 敲木鱼
└-- 我的
|-- 问题反馈
| └-- 提交反馈
|-- 用户协议
└-- 隐私政策
二、应用架构设计
1.分层模块化设计
- 产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。
本实践暂时只支持直板机,为单HAP包形式,包含路由根节点、底部导航栏等。
- 基础特性层:用于存放相对独立的功能UI和业务逻辑实现。
本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块。
每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。
- 公共能力层:存放公共能力,包括公共UI组件、数据管理、外部交互和工具库等共享功能。
本实践的公共能力层分为公共基础能力和可分可合组件,均打包为HAR包被上层业务 组件引用。
公共基础能力包含日志、文件处理等工具类,公共类型定义,网络库,以及弹窗、加载等公共组件。
可分可合组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。
2.业务组件设计
为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。
三、行业场景技术方案
1.万能空调遥控器
1)场景说明
- 点击空调遥控器的开关按钮,通过空调是否有响应确定是否添加设备。
- 添加完成后,在空调遥控器页面通过发送不同电平信号控制空调行为。
2)技术方案
- 红外信号发送
通过@ohos.multimodalInput.infraredEmitter (红外管理)发送红外信号控制空调,且遥控按钮按下时发送当前遥控器所有设置信息,例如:按下开关按钮时,红外电平信号中包含“on,送风,26摄氏度,制冷”等信息。
2.热量计算
1)场景说明
- 点击搜索结果可自定义输入食物的摄入量,精准控制摄入热量。
2)技术方案
- 用餐时间自定义:通过Select的menuItemContentModifier属性自定义用餐时间下拉菜单的内容,细分用餐时间。
- 通过Text、Image组件在Grid组件中的混编实现自定义键盘。
3.工具管理
1)场景说明
- 长按工具卡片并拖拽可以调整工具在首页展示的顺序,并且可以对工具进行自动分类。
- 点击“默认打开”下拉菜单,并选择某个工具,清理应用后下次打开会默认打开所选工具。
2)技术方案
- 首页菜单持久化存储
使用PersistenceV2实现对首页工具卡片的持久化数据管理,并对各分类工具卡片进行单独存储。
- 首页菜单持久化存储
配合Grid组件的editMode属性、onItemDragStart事件和onItemDrop事件实现对工具卡片的拖拽。
四、模板代码
1.工程结构(下载模板)
详细代码结构如下所示:
ComprehensiveTool
|- components // 可分可合组件层
| |- almanac/src/main/ets // 黄历(har)
| | |- apis // 模块接口
| | |- constants // 模块常量
| | |- components // 公共组件
| | |- model // 模型定义
| | |- pages // 页面
| | |- style // 模块样式
| | |- utils // 模块工具
| | |- viewmodel // 与页面一一对应的vm层
| |
| |- basic_calculator/src/main/ets // 计算器(har)
| | |- common // 模块常量
| | |- components // 组件
| | |- model // 模型定义
| | └- pages // 页面
| | └- utils // 模块工具类
| | └- viewmodel // 与页面一一对应的vm层
| |
| |- calories_calculate/src/main/ets // 热量计算(har)
| | |- common // 公共路由
| | |- apis // 模块接口
| | |- components // 模块组件
| | |- pages // 页面
| | |- mocks // mock数据
| | |- model // 模型定义
| | |- viewmodel // 与页面一一对应的vm层
| | └- utils // 模块工具类
| |- date_calculate/src/main/ets // 日期计算(har)
| | |- apis // 模块接口
| | |- constants // 模块常量
| | |- components // 模块组件
| | |- model // 模型定义
| | |- pages // 页面
| | |- utils // 模块工具
| |- exchange_calculator/src/main/ets // 汇率计算器(har)
| | |- common // 公共路由
| | |- constants // 模块常量
| | |- components // 模块组件
| | |- model // 模型定义
| | |- pages // 页面
| | |- viewmodel // 与页面一一对应的vm层
| |- income_calculator/src/main/ets // 个税计算器(har)
| | |- common // 模块常量
| | |- components // 模块组件
| | |- model // 模型定义
| | |- pages // 页面
| | |- utils // 模块工具类
| | |- viewmodel // 与页面一一对应的vm层
| |- remote_control/src/main/ets // 空调遥控器(har)
| | |- constant // 模块常量定义
| | |- components // 模块组件
| | |- model // 模型定义
| | |- util // 模块工具类
| | |- http // 请求定义
| | |- pages // 页面
| | |- viewmodel // 与页面一一对应的vm层
| |- mortgage_calculator/src/main/ets // 房贷计算器(har)
| | |- common // 模块常量定义
| | |- components // 模块组件
| | |- types // 模型定义
| | |- pages // 页面
| | |- viewmodel // 与页面一一对应的vm层
|
| |- entry/src/main/ets
| | |- apis // 模块接口
| | |- constant // 主页常量
| | |- components // UIAbility实例
| | |- utils // 工具类
| | |- entryability // UIAbility实例
| | |- entrybackupability // 备份实例
| | |- pages // 页面
| | |- mocks // mock数据
| | |- model // 模型定义
| | |- viewmodel // 与页面一一对应的vm层
2.关键代码解读
本篇代码非应用的全量代码,只包括应用的部分能力的关键代码。
若需获取全量代码,请查看模板集成章节。
1)空调遥控器-红外发送
- 代码实现效果
当设备未添加至空调列表,点击开关,可发送红外开关信号等待设备响应。
当设备已添加至空调列表,可在遥控器页面,发送红外信号控制空调。
- ·核心代码实现
获取红外电平信号。
export class RemoteApi {
// mock的红外协议库
static PROTOCOL_LIST: InfraredInfo[] = [
{
label: 'NEC协议',
id: '01',
pattern: '9012,4528,628,1686,628,1686,628,582,628,1686,628,582,628,582,628,582,628,582,628,' +
'1686,628,1686,628,582,628,1686,628,582,628,582,628,582,628,582,628,582,628,582,628,582' +
',628,582,628,582,628,1686,628,582,628,582,628,582,628,582,628,582,628,582,628,1686,628,582' +
',628,1686,628,582,628,582,628,1686,628,582,628,20050,628,582,628,582,628,582,628,582,628,582,' +
'628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,1686,628,582,628,582,628,582,' +
'628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628' +
',1686,628,582,628,582,628,40132,9000,4532,628,1686,628,1686,628,582,628,1686,628,582,628,582,628,582' +
',628,582,628,1686,628,1686,628,582,628,1686,628,582,628,582,628,582,628,582,628,582,628,582,628,582' +
',628,582,628,582,628,1686,628,582,628,582,628,582,628,582,628,582,628,582,628,1686,628,1686,628,' +
'1686,628,582,628,582,628,1686,628,582,628,20030,628,582,628,582,628,582,628,582,628,582,628,582,' +
'628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,' +
'582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,628,582,' +
'628,582,628,582,628,582,628',
frequency: 38000,
},
{
label: 'R05D协议',
id: '02',
pattern: '8960,4494,596,1690,596,1690,596,590,596,1690,596,590,596,590,596,' +
'590,596,590,596,1690,596,1690,596,590,596,1690,596,590,596,590,596,590,' +
'596,590,596,590,596,590,596,590,596,590,596,590,596,1690,596,590,596,590,' +
'596,590,596,590,596,590,596,590,596,1690,596,590,596,1690,596,590,596,590,' +
'596,1690,596,590,596,20020,596,590,596,590,596,590,596,590,596,590,596,590,' +
'596,590,596,590,596,590,596,590,596,590,596,590,596,590,596,1690,596,590,596,' +
'590,596,590,596,590,596,590,596,590,596,590,596,590,596,590,596,590,596,590,596,' +
'590,596,590,596,590,596,590,596,1690,596,590,596,590,596',
frequency: 38000,
},
];
// 根据空调匹配的协议获取电信号
static getInfrared(data: ControlInfo): Promise<InfraredInfo> {
const res: InfraredInfo = RemoteApi.PROTOCOL_LIST.find((item) => item.id === data.protocol)!;
return Promise.resolve(res);
}
}
通过红外管理发送。
export class SendUtil {
static sendInfrared(infraredFrequency: number, patternStr: string) {
const pattern: number[] = patternStr.split(',').map((item) => {
return Number(item);
});
try {
infraredEmitter.transmitInfrared(infraredFrequency, pattern);
} catch (error) {
hilog.error(0xff00, 'Template', `transmitInfrared failed, error: ${JSON.stringify(error, [`code`, `message`])}`);
}
}
}
2)热量管理-食物搜索与用餐时间自定义
- ·代码使用效果
搜索框内输入食物关键词,可自动搜索出关联食物。
点击搜索结果,可拉起自定义键盘输入摄入食物重量。
- ·核心代码实现
食物自定义半模态。
@Builder
addFoodSheet(item: FoodCalories) {
Column() {
Column() {
Row().width(48).height(4).backgroundColor($r('sys.color.icon_fourth')).borderRadius(2);
Row() {
Select(this.vm.menuList)
.selected(this.vm.selectIndex)
.value(this.vm.selectText)
.font({ size: 16, weight: FontWeight.Medium })
.fontColor($r('sys.color.font_primary'))
.space(16)
.width(124)
.optionWidth(224)
.optionHeight(296)
.menuItemContentModifier(new MyMenuItem())
.onSelect((index: number, text?: string | undefined) => {
console.info('Select:' + index);
this.vm.selectIndex = index;
if (text) {
this.vm.selectText = text;
}
});
Image($r('app.media.ic_close')).width(40).onClick(() => {
this.vm.showAddFoodSheet = false;
});
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween);
};
Column() {
Text(item.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor($r('sys.color.font_primary'));
Text(`${item.calories}千卡/${item.weight ?? 100}克`)
.fontSize(12)
.fontColor($r('sys.color.font_secondary'))
.margin({ top: 2 });
Divider().strokeWidth(1).margin({ top: 4 });
Text('请输入重量/容量')
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor($r('sys.color.font_primary'))
.margin({ top: 24 });
Stack() {
TextInput({ text: $$this.vm.inputText, placeholder: '100' })
.fontSize(16)
.fontColor($r('sys.color.font_primary'))
.placeholderColor($r('sys.color.font_secondary'))
.enabled(false);
Text('克').fontSize(16).fontColor($r('sys.color.font_secondary')).margin({ right: 13 });
}.alignContent(Alignment.End).margin({ top: 12 });
}
.alignItems(HorizontalAlign.Start)
.backgroundColor($r('sys.color.background_primary'))
.margin({ top: 8 })
.padding(12)
.borderRadius(16);
Row({ space: 12 }) {
Grid() {
ForEach(this.vm.keyboardList, (item: string) => {
GridItem() {
Button(item, { type: ButtonType.Normal })
.fontWeight(FontWeight.Medium)
.fontColor($r('sys.color.font_primary'))
.width(72)
.height(48)
.borderRadius(8)
.backgroundColor($r('sys.color.background_primary'))
.shadow({
offsetX: 0,
offsetY: 1,
color: '#90949C',
radius: 0,
})
.onClick(() => {
this.vm.btnClick(item);
});
};
}, (item: string) => item);
}
.width(240)
.height(228)
.rowsGap(12)
.columnsGap(12)
.columnsTemplate('1fr 1fr 1fr');
Column({ space: 12 }) {
Button({ type: ButtonType.Normal }) {
Image($r('app.media.ic_close')).width(24);
}
.layoutWeight(1)
.width('100%')
.borderRadius(8)
.backgroundColor($r('sys.color.background_primary'))
.onClick(() => {
this.vm.deleteBtn();
});
Button('确定', { type: ButtonType.Normal })
.layoutWeight(1)
.borderRadius(8)
.width('100%')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor($r('sys.color.font_on_primary'))
.backgroundColor('#E84026')
.onClick(() => {
this.vm.confirmBtn(item);
});
}.layoutWeight(1).height(228);
}.margin({ top: 24 });
}
.width('100%')
.height(533)
.padding({ left: 16, right: 16, top: 8 })
.backgroundColor($r('sys.color.background_secondary'))
.borderRadius({ topLeft: 32, topRight: 32 })
.constraintSize({ maxWidth: '100%' });
}
3)工具管理-工具卡片拖拽与持久化
- 代码使用效果
工具卡片可拖拽排序,删减在首页展示的工具并进行分类。
选择默认打开工具后,清理应用进程后,可默认打开选择的工具。
- 代码使用效果
工具卡片拖拽。
Grid() {
ForEach(this.toolList, (item: ToolTypeModule, index: number) => {
GridItem() {
Stack({ alignContent: Alignment.TopEnd }) {
this.toolCard(item);
if (this.manageStatus !== ManageStatus.NONE) {
Image(this.manageIcon)
.size({ width: 16, height: 16 })
.alignSelf(ItemAlign.End)
.margin({ top: 4, right: 4 });
}
}
.border({ width: 1, radius: 8, color: $r('sys.color.white') })
.backgroundColor('rgba(10, 89, 247, 0.05)')
.border({ width: 0 })
.width('100%')
.height(104);
}.onClick(() => {
this.vm.handleToolLayout(this.manageStatus, item, this.toolList, index);
});
}, (item: ToolTypeModule) => JSON.stringify(item));
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.padding(12)
.supportAnimation(true)
.edgeEffect(EdgeEffect.Spring)
.editMode(this.manageStatus === ManageStatus.DELETE ? true : false)
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => {
return this.dragBuilder(this.toolList[itemIndex]);
})
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number,
isSuccess: boolean) => {
if (!isSuccess || insertIndex >= this.toolList.length || itemIndex === -1 || insertIndex === -1) {
return;
}
this.vm.changeIndex(itemIndex, insertIndex);
});
首页默认打开工具。
// 初始化各分类工具,打开默认工具
init() {
let list: ToolTypeModule[] = JSON.parse(this.homeToolTypes.list);
if (!list.length) {
this.homeToolTypes.list = JSON.stringify(TOOL_TYPE_MODULES.slice(1));
}
this.homeToolList = JSON.parse(this.homeToolTypes.list);
this.calculatorToolList = JSON.parse(this.calculatorToolTypes.list);
this.lifeToolList = JSON.parse(this.lifeToolListTypes.list);
if (this.selectedToolType.url) {
this.pathStack.pushPathByName(this.selectedToolType.url, null);
}
}
// 默认打开选择菜单
Select(this.allTypes)
.selected(this.vm.selectedToolType.index)
.value(`默认打开: ${this.vm.selectedToolType.title}`)
.fontColor($r('sys.color.font_primary'))
.height(40)
.borderRadius($r('sys.float.corner_radius_level10'))
.selectedOptionFont({ weight: FontWeight.Medium })
.optionFont({ weight: FontWeight.Medium })
.optionWidth(224)
.backgroundColor($r('sys.color.comp_background_tertiary'))
.menuItemContentModifier(new MenuItemContentModifier())
.onSelect((index: number, text?: string | undefined) => {
let type = TOOL_TYPE_MODULES.find((item: ToolTypeModule) => item.id === index);
this.vm.selectedToolType.title = text ?? '';
this.vm.selectedToolType.index = index;
this.vm.selectedToolType.url = type?.url ?? '';
});
3.模板集成
本模板提供了两种代码集成方式,供开发者自由选用。
1)整体集成(下载模板)
开发者可以选择直接基于模板工程开发自己的应用工程。
- 模板代码获取:
通过IDE插件创建模板工程,开发指导。
通过生态市场下载源码,下载模板。
通过开源仓访问源码,仓库地址。
- 打开模板工程,根据README说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。
- 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据
。
将commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets文件中的mock接口替换为真实的服务器接口。
在commons/lib_common/src/main/ets/httprequest/HttpRequest.ets文件中将云侧开发者自定义的数据结构转换为端侧数据结构。
根据自己的业务内容修改模板,进行定制化开发。
2)按需集成
若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。
- 组件代码获取:
通过IDE插件下载组件源码。开发指导
通过生态市场下载组件源码。下载地址
- 下载组件源码,根据README中的说明,将组件包配置在自己的工程中。
- 根据API参考和示例代码,将组件集成在自己的对应场景中。
以上是第9期“综合工具”行业优秀案例的内容,更多行业敬请期待~
欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解XX行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~
同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新&活动不错过!
👉 HarmonyOS官方模板优秀案例系列持续更新, 点击查看往期案例汇总贴, 点击收藏 “”方便查找!
👉【互动有礼】邀请你成为HarmonyOS官方模板产品经理,优化方案由你制定!点击参加
👉【集成有礼】HarmonyOS官方模板集成创新活动,挥洒创意,赢精美大礼!点击参加
👉【HarmonyOS行业解决方案】为各行业鸿蒙应用提供全流程技术方案。点击查看
