为什么要开发这个日常提醒应用?
- 最近鸿蒙热度一直不减,而且前端的就业环境越来越差,所以心里面萌生了换一个赛道的想法。
- 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'); }} 
- Lift cycle management of Ability.
以上,总结了应用程序的主要代码内容。相关代码我把他放在了github上有需要的小伙伴自己下载
最后
小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。
为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:
鸿蒙(HarmonyOS NEXT)最新学习路线
 
该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案
路线图适合人群:
IT开发人员: 想要拓展职业边界
零基础小白: 鸿蒙爱好者,希望从0到1学习,增加一项技能。
**技术提升/进阶跳槽:**发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术
2.视频学习教程+学习PDF文档
HarmonyOS Next 最新全套视频教程

纯血版鸿蒙全套学习文档(面试、文档、全套视频等)
 
总结
参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线
