前言
根据研究机构Counterpoint Research发布的最新数据,2024年第一季度,鸿蒙OS份额由去年一季度的8%上涨至17%,iOS份额则从20%下降至16%。
这意味着,华为鸿蒙OS在中国市场的份额超越苹果iOS,已成中国第二大操作系统。
随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式的增长。这对于想要换赛道的程序员来说是一个非常好的消息,话说大家最近有想法转型鸿蒙开发吗?
什么是HarmonyOS NEXT?
●HarmonyOS Next为鸿蒙内核,并非Android操作系统的Linux内核,也不再兼容Android的APK格式安装文件,这也是头部软件、游戏公司都在加紧开发鸿蒙原生应用的最主要原因。
●市面上主流的手机操作系统:iOS和Android在研发之初都是为了手机这一单一设备研发的,鸿蒙则是通过一套操作系统来满足多种终端(手机、平板、手表等),从而实现互联互通的目的。HarmonyOScNext就像是乐高,采用组件设计,开发者可以根据需要灵活组合、弹性部署。
●安全性上,HarmonyOS Next的鸿蒙内核已经获得行业三大最高等级的安全认证。并且华为会在最大程度上保障开发者利益和消费者体验。HarmonyOS Next的系统性安全机制和工具可以保障开发者应用不被破解 篡改和仿冒,建立健康纯净的生态秩序,给消费者提供高品质的应用。
为什么要开发这个日常提醒应用?
●最近鸿蒙热度一直不减,而且前端的就业环境越来越差,所以心里面萌生了换一个赛道的想法。
●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-ignore
this.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 ability
let 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 ability
hilog.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 resources
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
}
}
以上,总结了应用程序的主要代码内容。相关代码我把他放在了github上有需要的小伙伴自己下载