为什么要开发这个日常提醒应用?
- 最近鸿蒙热度一直不减,而且前端的就业环境越来越差,所以心里面萌生了换一个赛道的想法。
- HarmonyOS NEXT 是华为打造的国产之光,而且是纯血版不再是套壳,更加激起了我的好奇心。
- ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。所以对于我们前端开发来说非常友好。
- HarmonyOS NEXT 文档也比较齐全。而且官方也有相关示例极大的方便了开发。
- 根据文档以及自己之前开发经验做一个日常提醒demo 加深自己对HarmonyOS NEXT的理解和实际应用。
日常提醒主要包含功能有哪些?
-
首页和个人中心tab页切换,以及tab 底部自定义实现。
-
封装公共弹窗日期选择、列表选择等组件。
-
访问本地用户首选项preferences实现数据本地化持久存储。
-
实现后台任务reminderAgentManager提醒。
-
提醒列表展示,以及删除等。
-
新增编辑日常提醒记录。
1.实现首页自定义tab页切换
主要依据tab组件以及tab 组件的BottomTabBarStyle的构造函数。
首页page/MinePage.ets代码如下
import {HomeTabs} from "./components/TabHome"
import {UserBaseInfo} from "./components/UserBaseInfo"@Entry
@Component
struct MinePage {@State currentIndex: number = 0// 构造类 自定义底部切换按钮@Builder TabBuilder(index: number,icon:Resource,selectedIcon:Resource,name:string) {Column() {Image(this.currentIndex === index ? selectedIcon : icon).width(24).height(24).margin({ bottom: 4 }).objectFit(ImageFit.Contain)Text(`${name}`).fontColor(this.currentIndex === index ? '#007DFF' : '#000000').fontSize('14vp').fontWeight(500).lineHeight(14)}.width('100%').height('100%').backgroundColor('#ffffff')}build() {Column() {Tabs({ barPosition: BarPosition.End }) {TabContent() {HomeTabs(); //首页}.tabBar(this.TabBuilder(0,$r('app.media.ic_home'),$r('app.media.ic_home_selected'),'首页'))TabContent() {UserBaseInfo()//个人中心}.tabBar(this.TabBuilder(1,$r('app.media.ic_mine'),$r('app.media.ic_mine_selected'),'我的'))}.vertical(false).scrollable(true).barMode(BarMode.Fixed).onChange((index: number) => {this.currentIndex = index;}).width('100%')}.width('100%').height('100%').backgroundColor('#f7f7f7')}
}
2.封装公共弹窗组件
在ets/common/utils 目录下新建 CommonUtils.ets 文件
import CommonConstants from '../constants/CommonConstants';/*** This is a pop-up window tool class, which is used to encapsulate dialog code.* Developers can directly invoke the methods in.*/
export class CommonUtils {/*** 确认取消弹窗*/alertDialog(content:{message:string},Callback: Function) {AlertDialog.show({message: content.message,alignment: DialogAlignment.Bottom,offset: {dx: 0,dy: CommonConstants.DY_OFFSET},primaryButton: {value: '取消',action: () => {Callback({type:1})}},secondaryButton: {value: '确认',action: () => {Callback({type:2})}}});}/*** 日期选择*/datePickerDialog(dateCallback: Function) {DatePickerDialog.show({start: new Date(),end: new Date(CommonConstants.END_TIME),selected: new Date(CommonConstants.SELECT_TIME),lunar: false,onAccept: (value: DatePickerResult) => {let year: number = Number(value.year);let month: number = Number(value.month) + CommonConstants.PLUS_ONE;let day: number = Number(value.day);let birthdate: string = `${year}-${this.padZero(month)}-${this.padZero(day)}`dateCallback(birthdate,[year, month, day]);}});}/*** 时间选择*/timePickerDialog(dateCallback: Function) {TimePickerDialog.show({selected:new Date(CommonConstants.SELECT_TIME),useMilitaryTime: true,onAccept: (value: TimePickerResult) => {let hour: number = Number(value.hour);let minute: number = Number(value.minute);let time: string =`${this.padZero(hour)}:${this.padZero(minute)}`dateCallback(time,[hour, minute]);}});}padZero(value:number):number|string {return value < 10 ? `0${value}` : value;}/*** 文本选择*/textPickerDialog(sexArray?: string[], sexCallback?: Function) {if (this.isEmpty(sexArray)) {return;}TextPickerDialog.show({range: sexArray,selected: 0,onAccept: (result: TextPickerResult) => {sexCallback(result.value);},onCancel: () => {}});}/*** Check obj is empty** @param {object} obj* @return {boolean} true(empty)*/isEmpty(obj: object | string): boolean {return obj === undefined || obj === null || obj === '';}}export default new CommonUtils();
3.封装本地持久化数据preferences操作
在ets/model/database 新建文件 PreferencesHandler.ets
import data_preferences from '@ohos.data.preferences';
import CommonConstants from '../../common/constants/CommonConstants';
import PreferencesListener from './PreferencesListener';/*** Based on lightweight databases preferences handler.*/
export default class PreferencesHandler {static instance: PreferencesHandler = new PreferencesHandler();private preferences: data_preferences.Preferences | null = null;private defaultValue = '';private listeners: PreferencesListener[];private constructor() {this.listeners = new Array();}/*** Configure PreferencesHandler.** @param context Context*/public async configure(context: Context) {this.preferences = await data_preferences.getPreferences(context, CommonConstants.PREFERENCE_ID);this.preferences.on('change', (data: Record<string, Object>) => {for (let preferencesListener of this.listeners) {preferencesListener.onDataChanged(data.key as string);}});}/*** Set data in PreferencesHandler.** @param key string* @param value any*/public async set(key: string, value: string) {if (this.preferences != null) {await this.preferences.put(key, value);await this.preferences.flush();}}/*** 获取数据** @param key string* @param defValue any* @return data about key*/public async get(key: string) {let data: string = '';if (this.preferences != null) {data = await this.preferences.get(key, this.defaultValue) as string;}return data;}/*** 删除数据** @param key string* @param defValue any* @return data about key*/public async delete(key: string) {if (this.preferences != null) {await this.preferences.delete(key);}}/*** Clear data in PreferencesHandler.*/public clear() {if (this.preferences != null) {this.preferences.clear();}}/*** Add preferences listener in PreferencesHandler.** @param listener PreferencesListener*/public addPreferencesListener(listener: PreferencesListener) {this.listeners.push(listener);}
}
4.封装代理提醒reminderAgentManager
在ets/model 目录下新建 ReminderService.ets
/** Copyright (c) 2022 Huawei Device Co., Ltd.* Licensed under the Apache License,Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/import reminderAgent from '@ohos.reminderAgentManager';
import notification from '@ohos.notificationManager';
import ReminderItem from '../viewmodel/ReminderItem';/*** Base on ohos reminder agent service*/
export default class ReminderService {/*** 打开弹窗*/public openNotificationPermission() {notification.requestEnableNotification().then(() => {}).catch((err: Error) => {});}/*** 发布相应的提醒代理** @param alarmItem ReminderItem* @param callback callback*/public addReminder(alarmItem: ReminderItem, callback?: (reminderId: number) => void) {let reminder = this.initReminder(alarmItem);reminderAgent.publishReminder(reminder, (err, reminderId: number) => {if (callback != null) {callback(reminderId);}});}/*** 根据需要删除提醒任务。** @param reminderId number*/public deleteReminder(reminderId: number) {reminderAgent.cancelReminder(reminderId);}private initReminder(item: ReminderItem): reminderAgent.ReminderRequestCalendar {return {reminderType: reminderAgent.ReminderType.REMINDER_TYPE_CALENDAR,title: item.title,content: item.content,dateTime: item.dateTime,repeatDays: item.repeatDays,ringDuration: item.ringDuration,snoozeTimes: item.snoozeTimes,timeInterval: item.timeInterval,actionButton: [{title: '关闭',type: reminderAgent.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE,},{title: '稍后提醒',type: reminderAgent.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE},],wantAgent: {pkgName: 'com.example.wuyandeduihua2',// 点击提醒通知后跳转的目标UIAbility信息abilityName: 'EntryAbility'},maxScreenWantAgent: { // 全屏显示提醒到达时自动拉起的目标UIAbility信息pkgName: 'com.example.wuyandeduihua2',abilityName: 'EntryAbility'},notificationId: item.notificationId,expiredContent: '消息已过期',snoozeContent: '确定要延迟提醒嘛',slotType: notification.SlotType.SERVICE_INFORMATION}}
}
5.新增编辑提醒页面
新增编辑AddNeedPage.ets页面 代码如下
import router from '@ohos.router';import {FormList,FormItemType,formDataType} from '../viewmodel/AddNeedModel'
import CommonUtils from '../common/utils/CommonUtils';
import addModel from '../viewmodel/AddNeedModel';@Entry
@Component
struct AddNeedPage {@State formData:formDataType={id:0,title:"",content:"",remindDay:[], //日期remindDay_text:"",remindTime:[],//时间remindTime_text:"",ringDuration:0,ringDuration_text:"", //提醒时长snoozeTimes:0,snoozeTimes_text:"", //延迟提醒次数timeInterval:0,timeInterval_text:"", //延迟提醒间隔}private viewModel: addModel = addModel.instant;aboutToAppear() {let params = router.getParams() as Record<string, Object|undefined>;if (params !== undefined) {let alarmItem: formDataType = params.alarmItem as formDataType;if (alarmItem !== undefined) {this.formData = {...alarmItem}}}}build() {Column() {Column(){ForEach(FormList,(item:FormItemType)=>{Row(){Row(){Text(item.title)}.width('35%')Row(){TextInput({ text: item.type.includes('Picker')?this.formData[`${item.key}_text`]: this.formData[item.key],placeholder: item.placeholder }).borderRadius(0).enabled(item.isPicker?false:true) //禁用.backgroundColor('#ffffff').onChange((value: string) => {if(!item.type.includes('Picker')){this.formData[item.key] = value;}})Image($r('app.media.ic_arrow')).visibility(item.isPicker?Visibility.Visible:Visibility.Hidden).width($r('app.float.arrow_image_width')).height($r('app.float.arrow_image_height')).margin({ right: $r('app.float.arrow_right_distance') })}.width('65%').padding({right:15}).onClick(()=>{if(item.isPicker){switch (item.type) {case 'datePicker':CommonUtils.datePickerDialog((value: string,timeArray:string[]) => {this.formData[`${item.key}_text`] = value;this.formData[item.key] = timeArray;});break;case 'timePicker':CommonUtils.timePickerDialog((value: string,timeArray:string[]) => {this.formData[`${item.key}_text`] = value;this.formData[item.key] = timeArray;});break;case 'TextPicker':CommonUtils.textPickerDialog(item.dicData, (value: string) => {this.formData[`${item.key}_text`] = value;this.formData[`${item.key}`] =item.dicMap[value];});break;default:break;}}})}.width('100%').justifyContent(FlexAlign.SpaceBetween).backgroundColor('#ffffff').padding(10).borderWidth({bottom:1}).borderColor('#f7f7f7')})Button('提交',{ type: ButtonType.Normal, stateEffect: true }).fontSize(18).width('90%').height(40).borderRadius(15).margin({ top:45 }).onClick(()=>{this.viewModel.setAlarmRemind(this.formData);router.back();})}}.width('100%').height('100%').backgroundColor('#f7f7f7')}
}
AddNeedModel.ets页面代码如下
import CommonConstants from '../common/constants/CommonConstants';
import ReminderService from '../model/ReminderService';
import DataTypeUtils from '../common/utils/DataTypeUtils';
import { GlobalContext } from '../common/utils/GlobalContext';
import PreferencesHandler from '../model/database/PreferencesHandler';/*** Detail page view model description*/
export default class DetailViewModel {static instant: DetailViewModel = new DetailViewModel();private reminderService: ReminderService;private alarms: Array<formDataType>;private constructor() {this.reminderService = new ReminderService();this.alarms = new Array<formDataType>();}/*** 设置提醒** @param alarmItem AlarmItem*/public async setAlarmRemind(alarmItem: formDataType) {let index = await this.findAlarmWithId(alarmItem.id);if (index !== CommonConstants.DEFAULT_NUMBER_NEGATIVE) {this.reminderService.deleteReminder(alarmItem.id);} else {index = this.alarms.length;alarmItem.notificationId = index;this.alarms.push(alarmItem);}alarmItem.dateTime={year: alarmItem.remindDay[0],month: alarmItem.remindDay[1],day: alarmItem.remindDay[2],hour: alarmItem.remindTime[0],minute: alarmItem.remindTime[1],second: 0}// @ts-ignorethis.reminderService.addReminder(alarmItem, (newId: number) => {alarmItem.id = newId;this.alarms[index] = alarmItem;let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;preference.set(CommonConstants.ALARM_KEY, JSON.stringify(this.alarms));})}/*** 删除提醒** @param id number*/public async removeAlarmRemind(id: number) {this.reminderService.deleteReminder(id);let index = await this.findAlarmWithId(id);if (index !== CommonConstants.DEFAULT_NUMBER_NEGATIVE) {this.alarms.splice(index, CommonConstants.DEFAULT_SINGLE);}let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;preference.set(CommonConstants.ALARM_KEY, JSON.stringify(this.alarms));}private async findAlarmWithId(id: number) {let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;let data = await preference.get(CommonConstants.ALARM_KEY);if (!DataTypeUtils.isNull(data)) {this.alarms = JSON.parse(data);for (let i = 0;i < this.alarms.length; i++) {if (this.alarms[i].id === id) {return i;}}}return CommonConstants.DEFAULT_NUMBER_NEGATIVE;}
}export interface FormItemType{title:string;placeholder:string;type:string;key:string;isPicker:boolean;dicData?:string[]dicMap?:object
}export interface formDataType{id:number;notificationId?:number;title:string;content:string;remindDay:number[];remindDay_text:string;remindTime:number[];remindTime_text:string;ringDuration:number;ringDuration_text:string;snoozeTimes:number;snoozeTimes_text:string;timeInterval:number;timeInterval_text:string;dateTime?:Object}export const FormList: Array<FormItemType> = [{title:"事项名称",placeholder:"请输入",key:"title",isPicker:false,type:"text"},{title:"事项描述",placeholder:"请输入",key:"content",isPicker:false,type:"text"},{title:"提醒日期",placeholder:"请选择",key:"remindDay",isPicker:true,type:"datePicker"},{title:"提醒时间",placeholder:"请选择",key:"remindTime",isPicker:true,type:"timePicker"},{title:"提醒时长",placeholder:"请选择",key:"ringDuration",isPicker:true,type:"TextPicker",dicData:['30秒','1分钟','5分钟'],dicMap:{'30秒':30,'1分钟':60,'5分钟':60*5,}},{title:"延迟提醒次数",placeholder:"请选择",key:"snoozeTimes",isPicker:true,type:"TextPicker",dicData:['1次','2次','3次','4次','5次','6次'],dicMap:{'1次':1,'2次':2,'3次':3,'4次':4,'5次':5,'6次':6,}},{title:"延迟提醒间隔",placeholder:"请选择",key:"timeInterval",isPicker:true,type:"TextPicker",dicData:['5分钟','10分钟','15分钟','30分钟'],dicMap:{'5分钟':5*60,'10分钟':10*60,'15分钟':15*60,'30分钟':15*60,}}
]
注意事项
- 本项目用到了代理通知需要在module.json5 文件中 requestPermissions 中声明权限
{"module": {"name": "entry","type": "entry","description": "$string:module_desc","mainElement": "EntryAbility","deviceTypes": ["phone","tablet"],"deliveryWithInstall": true,"installationFree": false,"pages": "$profile:main_pages","abilities": [{"name": "EntryAbility","srcEntry": "./ets/entryability/EntryAbility.ets","description": "$string:ability_desc","icon": "$media:icon","label": "$string:ability_label","startWindowIcon": "$media:icon","startWindowBackground": "$color:start_window_background","exported": true,"skills": [{"entities": ["entity.system.home"],"actions": ["action.system.home"]}]}],"requestPermissions": [{"name": "ohos.permission.PUBLISH_AGENT_REMINDER","reason": "$string:reason","usedScene": {"abilities": ["EntryAbility"],"when": "always"}}]}
}
- 本项目还用到了 应用上下文Context在入口文件EntryAbility.ets中注册
import type AbilityConstant from '@ohos.app.ability.AbilityConstant';
import display from '@ohos.display';
import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
import type Want from '@ohos.app.ability.Want';
import type window from '@ohos.window';
import PreferencesHandler from '../model/database/PreferencesHandler';
import { GlobalContext } from '../common/utils/GlobalContext';
/*** Lift cycle management of Ability.*/
export default class EntryAbility extends UIAbility {onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');GlobalContext.getContext().setObject('preference', PreferencesHandler.instance);}onDestroy(): void {hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');}async onWindowStageCreate(windowStage: window.WindowStage) {// Main window is created, set main page for this abilitylet globalDisplay: display.Display = display.getDefaultDisplaySync();GlobalContext.getContext().setObject('globalDisplay', globalDisplay);let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;await preference.configure(this.context.getApplicationContext());// Main window is created, set main page for this abilityhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');windowStage.loadContent("pages/MinePage", (err, data) => {if (err.code) {hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');return;}hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');});}onWindowStageDestroy(): void {// Main window is destroyed, release UI related resourceshilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');}onForeground(): void {// Ability has brought to foregroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');}onBackground(): void {// Ability has back to backgroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');}
}
以上,总结了应用程序的主要代码内容。相关代码我把他放在了github上有需要的小伙伴自己下载
最后
小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。
为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:
鸿蒙(HarmonyOS NEXT)最新学习路线
该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案
路线图适合人群:
IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术
2.视频学习教程+学习PDF文档
HarmonyOS Next 最新全套视频教程
纯血版鸿蒙全套学习文档(面试、文档、全套视频等)
总结
参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线