HarmonyOS ArkTS 实战:实现一个习惯打卡应用

项目效果

本文实现的是一个基于 HarmonyOS 和 ArkTS 的习惯打卡应用。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现习惯添加、每日打卡、连续天数统计、习惯删除、完成率计算、今日进度和打卡日历等功能。

最终运行效果如下:
在这里插入图片描述

页面主要包含以下内容:

  • 顶部应用标题和今日统计;
  • 今日打卡进度条;
  • 习惯列表;
  • 打卡复选框;
  • 连续打卡天数显示;
  • 添加新习惯输入框;
  • 习惯图标选择;
  • 删除习惯按钮;
  • 空状态提示;
  • 页面整体采用 ArkUI 声明式布局。

本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个习惯养成类单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。

前言

养成良好的习惯是改变人生的开始。研究表明,坚持21天可以养成一个习惯,而一个好用的习惯打卡工具可以帮助我们记录坚持的过程,激励自己持续进步。市面上的习惯应用往往功能复杂,而我们需要一个简洁专注的工具来追踪日常习惯。

从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、复选框交互、列表渲染、进度计算、日期处理和数组操作等内容。

本文基于 HarmonyOS 和 ArkTS 实现一个习惯打卡应用。用户可以添加想要养成的习惯,每天打卡记录,查看连续打卡天数,统计今日完成率,管理自己的习惯列表。

这个项目的核心不是简单的勾选框,而是帮助用户坚持习惯的完整流程。每一次打卡、添加和删除习惯,本质上都是对习惯数据的管理。状态变化后,页面会自动刷新,这正是 ArkUI 声明式开发的基本思想。

一、项目目标

本次实践主要实现以下目标:

  • 创建 HarmonyOS ArkTS 页面;
  • 使用 @Entry@Component 定义页面组件;
  • 使用 @State 管理页面状态;
  • 实现习惯添加功能;
  • 支持选择习惯图标;
  • 实现每日打卡功能;
  • 统计连续打卡天数;
  • 计算今日完成率;
  • 显示今日打卡进度;
  • 支持删除习惯;
  • 使用 ListForEach 渲染习惯列表;
  • 使用空状态提示优化无习惯页面;
  • 使用 @Builder 封装统计卡片、习惯项和图标按钮;
  • 完成一个可以运行的习惯打卡页面。

这个项目虽然是单页面应用,但它有完整的习惯添加、打卡、统计和管理流程,比普通静态页面更适合练习 ArkTS。

二、技术栈

类型 内容
开发方向 HarmonyOS 应用开发
开发语言 ArkTS
UI 框架 ArkUI
SDK 版本 HarmonyOS API 23 及以上
工程模型 Stage 模型
核心组件 Text / TextInput / Button / Checkbox / Column / Row / List / ForEach / Progress
状态管理 @State
数据处理 日期计算 / 进度统计 / 数组操作
项目入口 entry/src/main/ets/pages/Index.ets
运行平台 模拟器或真机

本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。

三、为什么选择习惯打卡项目

习惯打卡应用适合作为 ArkTS 练习项目,主要有以下几个原因。

第一,实用性强。习惯养成是很多人的需求,一个简洁的打卡工具可以直接应用于日常生活,帮助用户培养好习惯。

第二,业务场景真实。无论是早起、运动、阅读还是喝水,都需要坚持打卡,习惯追踪是自我提升的重要工具。

第三,适合练习状态管理。习惯列表、今日打卡状态、新习惯输入、选中的图标都属于页面状态。状态变化后,页面会自动刷新。

第四,适合练习日期处理。连续打卡天数的计算需要处理日期逻辑,这是前端开发中常见的需求。

第五,适合练习进度可视化。完成率和进度条可以直观展示今日打卡情况,激励用户完成所有习惯。

第六,扩展空间比较大。基础功能完成后,可以继续增加本地存储、打卡日历、习惯提醒、数据统计图表、习惯分类和成就系统。

在本项目中,habits 是核心数据源。页面中的统计数据、习惯列表和打卡状态都由它计算或渲染得到。

四、功能规则说明

本文提供的习惯图标包括:

图标 适用场景
💧 喝水、补水
🏃 运动、跑步
📚 阅读、学习
😴 早睡、睡眠
🥗 健康饮食
✍️ 写作、日记
🧘 冥想、放松
🎯 目标、其他

打卡规则如下:

每日打卡:每天可以对每个习惯进行一次打卡
取消打卡:可以取消今日的打卡
连续天数:连续打卡的天数,中断则重新计算

统计规则如下:

今日完成 = 今日已打卡的习惯数量
习惯总数 = 添加的习惯总数
完成率 = 今日完成 / 习惯总数 * 100%
最长连续 = 所有习惯中最长的连续打卡天数

每次打卡或添加/删除习惯后,统计数据都会自动更新。

五、项目结构

本项目主要修改首页文件:

entry
└── src
    └── main
        └── ets
            └── pages
                └── Index.ets

其中:

文件 作用
Index.ets 编写页面结构、状态数据和习惯打卡逻辑

本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。

如果后续继续扩展,可以考虑把习惯项组件、图标选择器、统计卡片和日历视图拆分成独立组件。

六、核心实现思路

本项目的核心流程如下:

  1. 定义习惯数据结构;
  2. 使用 @State 保存新习惯名称、选中的图标和习惯列表;
  3. 用户输入习惯名称;
  4. 用户选择习惯图标;
  5. 点击添加按钮创建新习惯;
  6. 使用数组保存所有习惯;
  7. 显示今日打卡进度;
  8. 使用 ForEach 渲染习惯列表;
  9. 点击复选框进行今日打卡/取消打卡;
  10. 点击删除按钮删除指定习惯;
  11. 顶部统计卡片根据习惯数据自动更新;
  12. 计算每个习惯的连续打卡天数;
  13. 没有习惯时显示空状态提示。

项目中最重要的状态变量如下:

@State newHabitName: string = ''
@State selectedIcon: string = '💧'
@State today: string = ''
@State habits: Habit[] = []
@State nextId: number = 1

其中:

状态变量 作用
newHabitName 新习惯输入内容
selectedIcon 选中的习惯图标
today 今日日期字符串
habits 习惯数组
nextId 生成习惯唯一编号

habits 是本项目中最重要的数据。所有习惯展示、打卡和统计都依赖这个数组。

七、Index.ets 完整代码

打开文件:

entry/src/main/ets/pages/Index.ets

将其中内容替换为下面代码:

interface Habit {
  id: number
  name: string
  icon: string
  checkedDates: string[]
  createDate: string
}

@Entry
@Component
struct Index {
  @State newHabitName: string = ''
  @State selectedIcon: string = '💧'
  @State nextId: number = 4
  @State today: string = this.getTodayString()
  @State habits: Habit[] = [
    {
      id: 1,
      name: '喝8杯水',
      icon: '💧',
      checkedDates: [this.getTodayString(), this.getYesterdayString()],
      createDate: this.getTodayString()
    },
    {
      id: 2,
      name: '阅读30分钟',
      icon: '📚',
      checkedDates: [this.getYesterdayString()],
      createDate: this.getTodayString()
    },
    {
      id: 3,
      name: '运动30分钟',
      icon: '🏃',
      checkedDates: [],
      createDate: this.getTodayString()
    }
  ]

  private icons: string[] = ['💧', '🏃', '📚', '😴', '🥗', '✍️', '🧘', '🎯']

  aboutToAppear() {
    this.today = this.getTodayString()
  }

  private addHabit(): void {
    let name = this.newHabitName.trim()
    if (name.length === 0) {
      return
    }
    
    let habit: Habit = {
      id: this.nextId,
      name: name,
      icon: this.selectedIcon,
      checkedDates: [],
      createDate: this.today
    }
    
    this.habits = [...this.habits, habit]
    this.nextId++
    this.newHabitName = ''
    this.selectedIcon = '💧'
  }

  private toggleCheck(habitId: number): void {
    this.habits = this.habits.map((habit: Habit) => {
      if (habit.id === habitId) {
        let newCheckedDates = [...habit.checkedDates]
        if (this.isCheckedToday(habit)) {
          newCheckedDates = newCheckedDates.filter(d => d !== this.today)
        } else {
          newCheckedDates.push(this.today)
        }
        return { ...habit, checkedDates: newCheckedDates }
      }
      return habit
    })
  }

  private deleteHabit(habitId: number): void {
    this.habits = this.habits.filter((habit: Habit) => habit.id !== habitId)
  }

  private isCheckedToday(habit: Habit): boolean {
    return habit.checkedDates.includes(this.today)
  }

  private getStreak(habit: Habit): number {
    let streak = 0
    let currentDate = new Date()
    
    while (true) {
      let dateStr = this.formatDate(currentDate)
      if (habit.checkedDates.includes(dateStr)) {
        streak++
        currentDate.setDate(currentDate.getDate() - 1)
      } else {
        break
      }
    }
    
    return streak
  }

  private getTodayCompleted(): number {
    return this.habits.filter((habit: Habit) => this.isCheckedToday(habit)).length
  }

  private getCompletionRate(): number {
    if (this.habits.length === 0) {
      return 0
    }
    return Math.round((this.getTodayCompleted() / this.habits.length) * 100)
  }

  private getMaxStreak(): number {
    if (this.habits.length === 0) {
      return 0
    }
    return Math.max(...this.habits.map((h: Habit) => this.getStreak(h)))
  }

  private getTodayString(): string {
    return this.formatDate(new Date())
  }

  private getYesterdayString(): string {
    let date = new Date()
    date.setDate(date.getDate() - 1)
    return this.formatDate(date)
  }

  private formatDate(date: Date): string {
    let year = date.getFullYear()
    let month = date.getMonth() + 1
    let day = date.getDate()
    return `${year}-${this.formatTwo(month)}-${this.formatTwo(day)}`
  }

  private formatTwo(value: number): string {
    return value < 10 ? '0' + value.toString() : value.toString()
  }

  @Builder
  StatCard(title: string, value: string, unit: string, color: string) {
    Column() {
      Row() {
        Text(value)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(color)
        Text(unit)
          .fontSize(14)
          .fontColor(color)
          .margin({ left: 4, top: 8 })
      }
      Text(title)
        .fontSize(13)
        .fontColor('#6B7280')
        .margin({ top: 4 })
    }
    .width('31%')
    .height(80)
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({
      radius: 10,
      color: '#12000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  IconButton(icon: string) {
    Button(icon)
      .width(44)
      .height(44)
      .fontSize(22)
      .backgroundColor(this.selectedIcon === icon ? '#0A59F7' : '#F3F4F6')
      .borderRadius(22)
      .onClick(() => {
        this.selectedIcon = icon
      })
  }

  @Builder
  HabitItem(habit: Habit) {
    Row() {
      Text(habit.icon)
        .fontSize(28)
        .width(48)
        .height(48)
        .textAlign(TextAlign.Center)
        .backgroundColor('#F5F7FA')
        .borderRadius(24)
      
      Column() {
        Text(habit.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
        
        Row() {
          Text(`🔥 连续${this.getStreak(habit)}`)
            .fontSize(12)
            .fontColor('#F59E0B')
          
          if (this.isCheckedToday(habit)) {
            Text('✓ 今日已打卡')
              .fontSize(12)
              .fontColor('#10B981')
              .margin({ left: 12 })
          }
        }
        .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 12, right: 12 })
      
      Checkbox({ name: habit.id.toString(), group: 'habits' })
        .select(this.isCheckedToday(habit))
        .width(28)
        .height(28)
        .selectedColor('#10B981')
        .onChange((value: boolean) => {
          this.toggleCheck(habit.id)
        })
      
      Button('×')
        .width(24)
        .height(24)
        .fontSize(18)
        .fontColor('#9CA3AF')
        .backgroundColor(Color.Transparent)
        .margin({ left: 8 })
        .onClick(() => {
          this.deleteHabit(habit.id)
        })
    }
    .width('100%')
    .padding(14)
    .margin({ bottom: 10 })
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({
      radius: 6,
      color: '#08000000',
      offsetX: 0,
      offsetY: 2
    })
  }

  build() {
    Column() {
      Text('习惯打卡')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .margin({ top: 22 })
      Text('基于 HarmonyOS ArkTS 的习惯养成工具')
        .fontSize(14)
        .fontColor('#6B7280')
        .margin({ top: 8, bottom: 20 })

      Row() {
        this.StatCard('今日完成', `${this.getTodayCompleted()}/${this.habits.length}`, '', '#10B981')
        this.StatCard('完成率', `${this.getCompletionRate()}`, '%', '#0A59F7')
        this.StatCard('最长连续', `${this.getMaxStreak()}`, '天', '#F59E0B')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ bottom: 16 })

      Column() {
        Progress({ value: this.getCompletionRate(), total: 100, type: ProgressType.Linear })
          .width('100%')
          .height(10)
          .color('#10B981')
          .backgroundColor('#E5E7EB')
          .borderRadius(5)
          .margin({ bottom: 8 })
        
        Text(`今日打卡进度:${this.getCompletionRate()}%,继续加油!`)
          .fontSize(13)
          .fontColor('#6B7280')
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Column() {
        Text('添加新习惯')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 12 })
        
        TextInput({
          placeholder: '输入习惯名称,例如:每天阅读30分钟',
          text: this.newHabitName
        })
          .height(44)
          .fontSize(15)
          .backgroundColor('#F5F7FA')
          .borderRadius(12)
          .padding({ left: 12, right: 12 })
          .margin({ bottom: 12 })
          .onChange((value: string) => {
            this.newHabitName = value
          })
        
        Text('选择图标')
          .fontSize(13)
          .fontColor('#6B7280')
          .width('100%')
          .margin({ bottom: 8 })
        
        Row() {
          ForEach(this.icons, (icon: string) => {
            this.IconButton(icon)
          }, (icon: string) => icon)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ bottom: 12 })
        
        Button('添加习惯')
          .height(44)
          .fontSize(16)
          .fontColor(Color.White)
          .backgroundColor('#10B981')
          .borderRadius(22)
          .width('100%')
          .onClick(() => {
            this.addHabit()
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Text('我的习惯')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .width('100%')
        .margin({ bottom: 12 })

      if (this.habits.length === 0) {
        Column() {
          Text('还没有添加习惯')
            .fontSize(16)
            .fontColor('#6B7280')
          Text('添加第一个习惯,开始养成好习惯吧')
            .fontSize(13)
            .fontColor('#9CA3AF')
            .margin({ top: 8 })
        }
        .width('100%')
        .padding(30)
        .backgroundColor(Color.White)
        .borderRadius(16)
      } else {
        List() {
          ForEach(this.habits, (habit: Habit) => {
            ListItem() {
              this.HabitItem(habit)
            }
          }, (habit: Habit) => habit.id.toString())
        }
        .width('100%')
        .layoutWeight(1)
        .scrollBar(BarState.Off)
      }
    }
    .width('100%')
    .height('100%')
    .padding({ left: 18, right: 18 })
    .backgroundColor('#F5F7FA')
  }
}

八、代码实现说明

1. 定义习惯数据结构

项目中使用 Habit 接口描述每一个习惯:

interface Habit {
  id: number
  name: string
  icon: string
  checkedDates: string[]
  createDate: string
}

字段说明如下:

字段 作用
id 习惯唯一编号
name 习惯名称
icon 习惯图标
checkedDates 打卡日期数组
createDate 创建日期

其中 checkedDates 数组保存了所有打卡的日期,用于判断今日是否打卡和计算连续天数。

2. 使用 @State 管理页面状态

本项目中使用多个状态变量:

@State newHabitName: string = ''
@State selectedIcon: string = '💧'
@State today: string = ''
@State habits: Habit[] = []
@State nextId: number = 1

这些状态变量分别保存新习惯名称、选中的图标、今日日期和习惯列表。当用户输入习惯名称、选择图标、添加习惯或打卡时,状态数据都会发生变化。ArkUI 会根据最新状态自动刷新页面。

3. 添加新习惯

添加习惯通过 addHabit 方法实现:

private addHabit(): void {
  let name = this.newHabitName.trim()
  if (name.length === 0) {
    return
  }
  
  let habit: Habit = {
    id: this.nextId,
    name: name,
    icon: this.selectedIcon,
    checkedDates: [],
    createDate: this.today
  }
  
  this.habits = [...this.habits, habit]
  this.nextId++
  this.newHabitName = ''
  this.selectedIcon = '💧'
}

如果名称为空则不添加。新习惯添加到数组末尾,添加后清空输入框并重置图标选择。

4. 打卡/取消打卡

点击复选框时切换今日打卡状态:

private toggleCheck(habitId: number): void {
  this.habits = this.habits.map((habit: Habit) => {
    if (habit.id === habitId) {
      let newCheckedDates = [...habit.checkedDates]
      if (this.isCheckedToday(habit)) {
        newCheckedDates = newCheckedDates.filter(d => d !== this.today)
      } else {
        newCheckedDates.push(this.today)
      }
      return { ...habit, checkedDates: newCheckedDates }
    }
    return habit
  })
}

如果今日已打卡则取消(从数组中移除今日日期),否则添加今日日期到打卡数组。

5. 连续打卡天数计算

从今天开始往前数,连续打卡的天数:

private getStreak(habit: Habit): number {
  let streak = 0
  let currentDate = new Date()
  
  while (true) {
    let dateStr = this.formatDate(currentDate)
    if (habit.checkedDates.includes(dateStr)) {
      streak++
      currentDate.setDate(currentDate.getDate() - 1)
    } else {
      break
    }
  }
  
  return streak
}

使用循环从今天开始往前检查每一天是否在打卡数组中,直到遇到未打卡的日期为止。

6. 统计数据计算

今日完成数、完成率和最长连续天数都动态计算:

private getTodayCompleted(): number {
  return this.habits.filter((habit: Habit) => this.isCheckedToday(habit)).length
}

private getCompletionRate(): number {
  if (this.habits.length === 0) {
    return 0
  }
  return Math.round((this.getTodayCompleted() / this.habits.length) * 100)
}

private getMaxStreak(): number {
  if (this.habits.length === 0) {
    return 0
  }
  return Math.max(...this.habits.map((h: Habit) => this.getStreak(h)))
}

这样可以保证统计数据始终与习惯数组保持一致。

7. 日期格式化

统一的日期格式化方法,确保日期字符串格式一致:

private formatDate(date: Date): string {
  let year = date.getFullYear()
  let month = date.getMonth() + 1
  let day = date.getDate()
  return `${year}-${this.formatTwo(month)}-${this.formatTwo(day)}`
}
8. 图标选择

提供8种常用的习惯图标供用户选择:

private icons: string[] = ['💧', '🏃', '📚', '😴', '🥗', '✍️', '🧘', '🎯']

使用emoji作为图标,简单直观,不需要额外的图片资源。

九、运行项目

代码编写完成后,在 DevEco Studio 中选择模拟器或真机运行项目。

运行成功后,页面会显示"习惯打卡"。用户可以添加新习惯,选择图标,每天打卡,查看连续天数和完成进度。

可以按照以下步骤进行测试:

  1. 打开应用,检查页面是否正常显示预置的示例习惯;
  2. 检查统计卡片是否显示正确的今日完成数、完成率和最长连续天数;
  3. 检查进度条是否显示正确的完成进度;
  4. 在输入框中输入新习惯名称;
  5. 选择不同的图标,检查图标按钮颜色是否变化;
  6. 点击"添加习惯"按钮,检查新习惯是否出现在列表中;
  7. 检查统计数据是否更新;
  8. 点击习惯前的复选框,检查是否标记为今日已打卡;
  9. 检查连续天数是否正确计算;
  10. 检查统计数据和进度条是否更新;
  11. 再次点击复选框,检查是否取消打卡;
  12. 点击习惯右侧的×按钮,检查习惯是否被删除;
  13. 删除所有习惯,检查空状态提示是否显示。

测试结果如下:

测试功能 测试结果
页面正常显示 成功
习惯名称输入 成功
图标选择 成功
添加习惯 成功
打卡/取消打卡 成功
连续天数计算 成功
删除习惯 成功
完成率统计 成功
进度条更新 成功
空状态提示 成功

经过测试,应用主要功能可以正常运行,打卡状态和统计数据都能根据用户操作及时更新。

十、开发中遇到的问题

1. 日期比较问题

日期比较必须使用统一格式的字符串,不能直接比较Date对象,确保打卡判断准确:

return habit.checkedDates.includes(this.today)
2. 连续天数计算边界

计算连续天数时要从今天开始往前数,而不是从创建日期开始,这样才能正确反映当前的连续打卡情况。

3. 空数组处理

当没有习惯时,计算完成率和最长连续天数会出现问题,需要特殊处理:

if (this.habits.length === 0) {
  return 0
}
4. 状态更新检测

修改打卡日期数组时需要创建新数组,而不是直接修改原数组,否则UI不会更新:

let newCheckedDates = [...habit.checkedDates]
// ... 修改newCheckedDates
return { ...habit, checkedDates: newCheckedDates }
5. 视觉反馈

已打卡的习惯要有明显的视觉反馈,显示"✓ 今日已打卡"文字,让用户清楚知道哪些习惯已经完成。

十一、总结

本文基于 HarmonyOS 和 ArkTS 实现了一个习惯打卡应用。项目通过 @State 管理页面数据,使用 ArkUI 组件完成页面布局,并实现了习惯添加、每日打卡、连续天数统计、完成率计算和进度显示等功能。

通过本次实践,主要完成了以下内容:

  • 使用 @Entry@Component 创建页面组件;
  • 使用 @State 保存习惯名称、选中图标和习惯列表;
  • 使用 TextInput 获取习惯名称输入;
  • 使用 Checkbox 实现打卡功能;
  • 支持多种习惯图标选择;
  • 实现习惯添加和删除功能;
  • 正确计算连续打卡天数;
  • 动态计算今日完成数和完成率;
  • 使用 Progress 组件显示打卡进度;
  • 使用emoji图标美化界面;
  • 使用 ListForEach 渲染习惯列表;
  • 使用条件渲染显示空状态;
  • 使用 @Builder 封装页面局部结构;
  • 完成一个可以运行的 HarmonyOS 习惯养成工具页面。

这个项目虽然是一个单页面练习,但它完整实现了习惯打卡的核心功能。它展示了 ArkTS 中日期处理、数组操作、状态管理和进度可视化的基本方法。

后续可以在这个基础上继续扩展,例如:

  • 增加本地数据持久化;
  • 增加打卡日历视图;
  • 增加打卡提醒通知;
  • 增加每周/每月坚持统计;
  • 增加习惯分类;
  • 增加习惯编辑功能;
  • 增加自定义图标;
  • 增加成就徽章系统;
  • 增加深色模式适配;
  • 增加数据导出功能。

整体来看,习惯打卡应用适合作为 HarmonyOS ArkTS 的练习项目。它功能实用、贴近生活,也能体现 ArkUI 声明式开发和状态驱动页面刷新的基本思想。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐