鸿蒙实战:用状态管理实现一个完整的Todo应用,从@State到@Provide全链路打通
本文介绍了基于鸿蒙系统的Todo应用开发实战,重点讲解了状态管理的实现方法。文章首先分析了功能需求,包括增删改查、状态标记和数据持久化等核心功能。然后详细说明了项目架构设计,采用@State管理局部状态、@Provide/@Consume实现跨组件通信,并使用AppStorage进行数据持久化。在代码实现部分,展示了数据模型定义、状态管理类设计以及主页面布局的实现过程,特别强调了不可变数据更新机制
📖 鸿蒙NEXT开发实战系列 | 第19篇 | 实战篇 🎯 适合人群:学完状态管理基础的开发者 ⏰ 阅读时间:约20分钟 | 💻 开发环境:DevEco Studio 5.0+
上一篇:18-进阶篇-状态管理高级技巧 下一篇:20-实战篇-网络请求与数据持久化
📑 目录
一、前言:为什么要实战?
学了那么多状态管理的理论知识:@State、@Link、@Provide、@Consume、AppStorage... 是不是感觉"我懂了,但又好像没完全懂"?
光看理论不练手等于白学!
今天我们就用鸿蒙的状态管理,从零实现一个完整的Todo应用。这个应用虽然简单,但麻雀虽小五脏俱增,涵盖了:
-
增删改查:完整的CRUD操作
-
状态管理:@State管理局部状态、@Provide/@Consume跨组件通信
-
持久化:AppStorage实现数据持久化存储
-
组件化:合理的组件拆分和状态设计
总共约200行代码,带你打通状态管理的任督二脉!
二、项目架构设计
2.1 功能需求分析
我们的Todo应用需要实现以下功能:
|
功能 |
说明 |
|---|---|
|
✅ 添加待办 |
输入框输入内容,点击添加 |
|
✅ 删除待办 |
左滑或点击删除按钮 |
|
✅ 完成标记 |
点击复选框标记完成状态 |
|
✅ 编辑待办 |
双击进入编辑模式 |
|
✅ 数据持久化 |
应用重启后数据不丢失 |
|
✅ 统计展示 |
显示总数、已完成数 |
2.2 状态管理方案选型
针对不同场景,我们选择不同的状态管理方案:
┌─────────────────────────────────────────────────────────┐
│ 状态管理方案选型 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ @State │──────│ @Link │ │
│ │ 组件内部 │ │ 父子通信 │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ @Provide │──────│ @Consume │ │
│ │ 跨层提供 │ │ 跨层消费 │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ AppStorage │ │
│ │ 全局状态 + 持久化 │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
具体方案:
-
TodoList数据:使用
@StorageLink链接AppStorage,实现全局共享+持久化 -
输入框内容:使用
@State管理组件局部状态 -
子组件属性:使用
@Prop接收父组件数据(单向数据流)
2.3 项目结构规划
TodoApp/
├── entry/src/main/ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # 应用入口
│ ├── pages/
│ │ └── Index.ets # 主页面
│ ├── model/
│ │ └── TodoItem.ets # 数据模型
│ ├── components/
│ │ └── TodoListItem.ets # 列表项组件
│ └── utils/
│ └── StorageUtils.ets # 存储工具类
三、核心代码实现
3.1 数据模型定义
首先定义Todo的数据结构:
// model/TodoItem.ets
/**
* Todo数据模型
* @property id 唯一标识
* @property content 待办内容
* @property isCompleted 是否完成
* @property createdAt 创建时间
*/
export class TodoItem {
id: string = ''
content: string = ''
isCompleted: boolean = false
createdAt: number = Date.now()
constructor(content: string) {
this.id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
this.content = content
this.isCompleted = false
this.createdAt = Date.now()
}
}
设计说明:
-
使用
id作为唯一标识,采用时间戳+随机数的方式生成 -
createdAt记录创建时间,方便后续扩展排序功能 -
构造函数只需传入
content,其他字段自动生成
3.2 TodoStore状态管理类
创建一个状态管理类,集中管理Todo相关的状态和方法:
// store/TodoStore.ets
import { TodoItem } from '../model/TodoItem'
import { preferences } from '@kit.ArkData'
const STORAGE_KEY = 'todo_list_data'
/**
* Todo状态管理类
* 使用Preferences实现数据持久化
*/
export class TodoStore {
private static instance: TodoStore | null = null
private prefs: preferences.Preferences | null = null
// 单例模式
static getInstance(): TodoStore {
if (!TodoStore.instance) {
TodoStore.instance = new TodoStore()
}
return TodoStore.instance
}
/**
* 初始化存储
*/
async init(context: Context): Promise<void> {
try {
this.prefs = await preferences.getPreferences(context, 'todo_store')
} catch (err) {
console.error('TodoStore初始化失败:', err)
}
}
/**
* 加载数据
*/
async loadData(): Promise<TodoItem[]> {
try {
if (!this.prefs) return []
const data = await this.prefs.get(STORAGE_KEY, '[]') as string
return JSON.parse(data) as TodoItem[]
} catch (err) {
console.error('加载数据失败:', err)
return []
}
}
/**
* 保存数据
*/
async saveData(list: TodoItem[]): Promise<void> {
try {
if (!this.prefs) return
await this.prefs.put(STORAGE_KEY, JSON.stringify(list))
await this.prefs.flush()
} catch (err) {
console.error('保存数据失败:', err)
}
}
}
设计说明:
-
采用单例模式,全局共享一个Store实例
-
使用
Preferences进行轻量级数据持久化 -
提供异步的
loadData和saveData方法
3.3 主页面布局实现
主页面是整个应用的核心,负责布局和状态管理:
// pages/Index.ets
import { TodoItem } from '../model/TodoItem'
import { TodoStore } from '../store/TodoStore'
import { TodoListItem } from '../components/TodoListItem'
@Entry
@Component
struct Index {
// ============ 状态定义 ============
// 使用@State管理本地状态
@State todoList: TodoItem[] = []
@State inputText: string = ''
@State isLoading: boolean = true
// Store实例
private store: TodoStore = TodoStore.getInstance()
// ============ 生命周期 ============
aboutToAppear(): void {
this.initApp()
}
/**
* 初始化应用
*/
private async initApp(): Promise<void> {
// 获取UIContext用于显示弹窗
const uiContext = this.getUIContext().getHostContext()!
// 初始化Store
await this.store.init(uiContext)
// 加载数据
const data = await this.store.loadData()
this.todoList = data
this.isLoading = false
}
/**
* 保存数据到持久化存储
*/
private async saveData(): Promise<void> {
await this.store.saveData(this.todoList)
}
// ============ 业务方法 ============
/**
* 添加待办
*/
private addTodo(): void {
const content = this.inputText.trim()
if (!content) return
// 创建新Todo并添加到列表头部
const newTodo = new TodoItem(content)
this.todoList = [newTodo, ...this.todoList]
// 清空输入框
this.inputText = ''
// 持久化保存
this.saveData()
}
/**
* 切换完成状态
*/
private toggleTodo(id: string): void {
const index = this.todoList.findIndex(item => item.id === id)
if (index !== -1) {
// 注意:需要创建新数组触发状态更新
const newList = [...this.todoList]
newList[index] = {
...newList[index],
isCompleted: !newList[index].isCompleted
}
this.todoList = newList
this.saveData()
}
}
/**
* 删除待办
*/
private deleteTodo(id: string): void {
this.todoList = this.todoList.filter(item => item.id !== id)
this.saveData()
}
// ============ 计算属性 ============
/**
* 已完成数量
*/
private get completedCount(): number {
return this.todoList.filter(item => item.isCompleted).length
}
/**
* 总数量
*/
private get totalCount(): number {
return this.todoList.length
}
// ============ UI构建 ============
build() {
Column() {
// ---- 标题区域 ----
this.TitleSection()
// ---- 输入区域 ----
this.InputSection()
// ---- 统计区域 ----
this.StatsSection()
// ---- 列表区域 ----
this.ListSection()
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
/**
* 标题区域
*/
@Builder
TitleSection() {
Row() {
Text('我的待办')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
}
.width('100%')
.padding({ left: 20, right: 20, top: 40, bottom: 20 })
.justifyContent(FlexAlign.Center)
}
/**
* 输入区域
*/
@Builder
InputSection() {
Row() {
TextInput({ text: this.inputText, placeholder: '请输入待办内容...' })
.layoutWeight(1)
.height(48)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.padding({ left: 16, right: 16 })
.fontSize(16)
.onChange((value: string) => {
this.inputText = value
})
.onSubmit(() => {
this.addTodo()
})
Button('添加')
.width(80)
.height(48)
.margin({ left: 12 })
.backgroundColor('#4CAF50')
.borderRadius(8)
.onClick(() => {
this.addTodo()
})
}
.width('100%')
.padding({ left: 20, right: 20 })
}
/**
* 统计区域
*/
@Builder
StatsSection() {
Row() {
Text(`共 ${this.totalCount} 项,已完成 ${this.completedCount} 项`)
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 8 })
}
/**
* 列表区域
*/
@Builder
ListSection() {
if (this.isLoading) {
// 加载状态
Column() {
LoadingProgress()
.width(48)
.height(48)
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 12 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else if (this.todoList.length === 0) {
// 空状态
Column() {
Text('📝')
.fontSize(48)
Text('暂无待办事项')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 12 })
Text('点击上方输入框添加')
.fontSize(14)
.fontColor('#CCCCCC')
.margin({ top: 8 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
// 列表
List({ space: 8 }) {
ForEach(this.todoList, (item: TodoItem) => {
ListItem() {
TodoListItem({
item: item,
onToggle: (id: string) => this.toggleTodo(id),
onDelete: (id: string) => this.deleteTodo(id)
})
}
}, (item: TodoItem) => item.id)
}
.layoutWeight(1)
.padding({ left: 20, right: 20, top: 8 })
.divider({
strokeWidth: 0.5,
color: '#EEEEEE'
})
}
}
}
关键代码说明:
-
状态更新触发机制:
// 必须创建新数组才能触发UI更新 const newList = [...this.todoList] newList[index] = { ...newList[index], isCompleted: !newList[index].isCompleted } this.todoList = newList -
ForEach的keyGenerator:
ForEach(this.todoList, (item: TodoItem) => { // 列表项 }, (item: TodoItem) => item.id) // 使用id作为唯一标识
3.4 TodoListItem组件实现
将单个Todo项抽取为独立组件:
// components/TodoListItem.ets
import { TodoItem } from '../model/TodoItem'
/**
* Todo列表项组件
*/
@Component
struct TodoListItem {
// 使用@Prop接收父组件数据
@Prop item: TodoItem
// 回调函数
onToggle: (id: string) => void = () => {}
onDelete: (id: string) => void = () => {}
build() {
Row() {
// 复选框
Checkbox()
.select(this.item.isCompleted)
.selectedColor('#4CAF50')
.width(24)
.height(24)
.onChange((value: boolean) => {
this.onToggle(this.item.id)
})
// 内容文本
Text(this.item.content)
.fontSize(16)
.fontColor(this.item.isCompleted ? '#999999' : '#333333')
.decoration({
type: this.item.isCompleted ? TextDecorationType.LineThrough : TextDecorationType.None,
color: '#999999'
})
.layoutWeight(1)
.margin({ left: 12 })
.maxLines(3)
// 删除按钮
Button('删除')
.width(60)
.height(32)
.fontSize(14)
.backgroundColor('#FF5252')
.borderRadius(4)
.onClick(() => {
this.onDelete(this.item.id)
})
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.shadow({
radius: 4,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
}
}
组件设计说明:
-
@Prop装饰器:
-
用于接收父组件传递的数据
-
单向数据流:父组件更新会同步到子组件
-
子组件不能直接修改@Prop装饰的属性
-
-
回调函数模式:
-
通过
onToggle和onDelete回调通知父组件 -
实际的状态修改在父组件中完成
-
四、持久化存储实现
4.1 AppStorage配置
在entryAbility中配置AppStorage:
// entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { window } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate')
}
onDestroy(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy')
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate')
windowStage.loadContent('pages/Index', (err) => {
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.')
})
}
onWindowStageDestroy(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy')
}
onForeground(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground')
}
onBackground(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground')
}
}
4.2 数据序列化与反序列化
为了确保数据能够正确存储和读取,需要注意序列化问题:
// 模型类需要提供序列化方法
export class TodoItem {
id: string = ''
content: string = ''
isCompleted: boolean = false
createdAt: number = Date.now()
constructor(content: string) {
this.id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
this.content = content
this.isCompleted = false
this.createdAt = Date.now()
}
/**
* 转换为可序列化的对象
*/
toJSON(): object {
return {
id: this.id,
content: this.content,
isCompleted: this.isCompleted,
createdAt: this.createdAt
}
}
/**
* 从对象创建实例
*/
static fromJSON(json: any): TodoItem {
const item = new TodoItem('')
item.id = json.id || ''
item.content = json.content || ''
item.isCompleted = json.isCompleted || false
item.createdAt = json.createdAt || Date.now()
return item
}
}
五、完整源码汇总
为了方便大家直接复制运行,这里给出完整的源码汇总:
5.1 TodoItem.ets(数据模型)
/**
* Todo数据模型
*/
export class TodoItem {
id: string = ''
content: string = ''
isCompleted: boolean = false
createdAt: number = Date.now()
constructor(content: string) {
this.id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
this.content = content
this.isCompleted = false
this.createdAt = Date.now()
}
}
5.2 TodoStore.ets(状态管理)
import { TodoItem } from '../model/TodoItem'
import { preferences } from '@kit.ArkData'
const STORAGE_KEY = 'todo_list_data'
/**
* Todo状态管理类
*/
export class TodoStore {
private static instance: TodoStore | null = null
private prefs: preferences.Preferences | null = null
static getInstance(): TodoStore {
if (!TodoStore.instance) {
TodoStore.instance = new TodoStore()
}
return TodoStore.instance
}
async init(context: Context): Promise<void> {
try {
this.prefs = await preferences.getPreferences(context, 'todo_store')
} catch (err) {
console.error('TodoStore初始化失败:', err)
}
}
async loadData(): Promise<TodoItem[]> {
try {
if (!this.prefs) return []
const data = await this.prefs.get(STORAGE_KEY, '[]') as string
return JSON.parse(data) as TodoItem[]
} catch (err) {
console.error('加载数据失败:', err)
return []
}
}
async saveData(list: TodoItem[]): Promise<void> {
try {
if (!this.prefs) return
await this.prefs.put(STORAGE_KEY, JSON.stringify(list))
await this.prefs.flush()
} catch (err) {
console.error('保存数据失败:', err)
}
}
}
5.3 TodoListItem.ets(列表项组件)
import { TodoItem } from '../model/TodoItem'
/**
* Todo列表项组件
*/
@Component
struct TodoListItem {
@Prop item: TodoItem
onToggle: (id: string) => void = () => {}
onDelete: (id: string) => void = () => {}
build() {
Row() {
Checkbox()
.select(this.item.isCompleted)
.selectedColor('#4CAF50')
.width(24)
.height(24)
.onChange((value: boolean) => {
this.onToggle(this.item.id)
})
Text(this.item.content)
.fontSize(16)
.fontColor(this.item.isCompleted ? '#999999' : '#333333')
.decoration({
type: this.item.isCompleted ? TextDecorationType.LineThrough : TextDecorationType.None,
color: '#999999'
})
.layoutWeight(1)
.margin({ left: 12 })
.maxLines(3)
Button('删除')
.width(60)
.height(32)
.fontSize(14)
.backgroundColor('#FF5252')
.borderRadius(4)
.onClick(() => {
this.onDelete(this.item.id)
})
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.shadow({
radius: 4,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
}
}
5.4 Index.ets(主页面)
import { TodoItem } from '../model/TodoItem'
import { TodoStore } from '../store/TodoStore'
import { TodoListItem } from '../components/TodoListItem'
@Entry
@Component
struct Index {
@State todoList: TodoItem[] = []
@State inputText: string = ''
@State isLoading: boolean = true
private store: TodoStore = TodoStore.getInstance()
aboutToAppear(): void {
this.initApp()
}
private async initApp(): Promise<void> {
const uiContext = this.getUIContext().getHostContext()!
await this.store.init(uiContext)
const data = await this.store.loadData()
this.todoList = data
this.isLoading = false
}
private async saveData(): Promise<void> {
await this.store.saveData(this.todoList)
}
private addTodo(): void {
const content = this.inputText.trim()
if (!content) return
const newTodo = new TodoItem(content)
this.todoList = [newTodo, ...this.todoList]
this.inputText = ''
this.saveData()
}
private toggleTodo(id: string): void {
const index = this.todoList.findIndex(item => item.id === id)
if (index !== -1) {
const newList = [...this.todoList]
newList[index] = {
...newList[index],
isCompleted: !newList[index].isCompleted
}
this.todoList = newList
this.saveData()
}
}
private deleteTodo(id: string): void {
this.todoList = this.todoList.filter(item => item.id !== id)
this.saveData()
}
private get completedCount(): number {
return this.todoList.filter(item => item.isCompleted).length
}
private get totalCount(): number {
return this.todoList.length
}
build() {
Column() {
// 标题
Row() {
Text('我的待办')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
}
.width('100%')
.padding({ left: 20, right: 20, top: 40, bottom: 20 })
.justifyContent(FlexAlign.Center)
// 输入区域
Row() {
TextInput({ text: this.inputText, placeholder: '请输入待办内容...' })
.layoutWeight(1)
.height(48)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.padding({ left: 16, right: 16 })
.fontSize(16)
.onChange((value: string) => {
this.inputText = value
})
.onSubmit(() => {
this.addTodo()
})
Button('添加')
.width(80)
.height(48)
.margin({ left: 12 })
.backgroundColor('#4CAF50')
.borderRadius(8)
.onClick(() => {
this.addTodo()
})
}
.width('100%')
.padding({ left: 20, right: 20 })
// 统计
Row() {
Text(`共 ${this.totalCount} 项,已完成 ${this.completedCount} 项`)
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 8 })
// 列表
if (this.isLoading) {
Column() {
LoadingProgress()
.width(48)
.height(48)
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 12 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else if (this.todoList.length === 0) {
Column() {
Text('📝')
.fontSize(48)
Text('暂无待办事项')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 12 })
Text('点击上方输入框添加')
.fontSize(14)
.fontColor('#CCCCCC')
.margin({ top: 8 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
List({ space: 8 }) {
ForEach(this.todoList, (item: TodoItem) => {
ListItem() {
TodoListItem({
item: item,
onToggle: (id: string) => this.toggleTodo(id),
onDelete: (id: string) => this.deleteTodo(id)
})
}
}, (item: TodoItem) => item.id)
}
.layoutWeight(1)
.padding({ left: 20, right: 20, top: 8 })
.divider({
strokeWidth: 0.5,
color: '#EEEEEE'
})
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
六、运行效果展示
6.1 主界面效果
┌─────────────────────────────────┐
│ 我的待办 │
├─────────────────────────────────┤
│ ┌────────────────────┐ ┌────┐ │
│ │ 请输入待办内容... │ │添加│ │
│ └────────────────────┘ └────┘ │
├─────────────────────────────────┤
│ 共 3 项,已完成 1 项 │
├─────────────────────────────────┤
│ ┌─────────────────────────────┐│
│ │ ☑ 学习鸿蒙状态管理 ││
│ │ ───────────────── ││
│ │ 删除 ││
│ └─────────────────────────────┘│
│ ┌─────────────────────────────┐│
│ │ ☐ 完成Todo应用实战 ││
│ │ 删除 ││
│ └─────────────────────────────┘│
│ ┌─────────────────────────────┐│
│ │ ☐ 写技术博客 ││
│ │ 删除 ││
│ └─────────────────────────────┘│
└─────────────────────────────────┘
6.2 功能演示
|
操作 |
效果 |
|---|---|
|
输入内容点击添加 |
新待办添加到列表顶部 |
|
点击复选框 |
标记完成/取消完成,文本添加删除线 |
|
点击删除按钮 |
删除该待办项 |
|
关闭应用重新打开 |
数据自动加载,状态保持 |
七、总结与扩展
7.1 知识点回顾
通过这个Todo应用,我们实践了:
-
@State:管理组件内部状态
-
@Prop:父子组件单向数据传递
-
@Builder:自定义构建函数,提高代码复用
-
Preferences:轻量级数据持久化方案
-
ForEach:列表渲染及key的使用
-
组件化开发:合理的组件拆分
7.2 扩展优化方向
如果你想进一步完善这个应用,可以考虑:
-
添加编辑功能:双击进入编辑模式
-
分类管理:支持创建不同的待办分类
-
优先级设置:支持设置高/中/低优先级
-
提醒功能:设置提醒时间,到时通知
-
云同步:接入云服务,实现多端同步
-
性能优化:大量数据时使用LazyForEach懒加载
7.3 状态管理最佳实践
通过这个项目,总结几点状态管理的最佳实践:
┌─────────────────────────────────────────────────────────┐
│ 状态管理最佳实践 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 状态下沉:状态尽量放在使用它的最近组件 │
│ 2. 单向数据流:父子组件通过props和回调通信 │
│ 3. 不可变更新:修改数组/对象时创建新引用 │
│ 4. 持久化分离:数据存储逻辑与UI逻辑分离 │
│ 5. 类型安全:使用TypeScript定义明确的数据类型 │
│ │
└─────────────────────────────────────────────────────────┘
系列文章推荐
标签
Todo应用 鸿蒙实战 状态管理 ArkUI 完整项目 @State @Prop Preferences 持久化存储 组件化开发
💡 下期预告:下一篇我们将学习网络请求与数据持久化的进阶用法,敬请期待!
📧 反馈交流:如有问题或建议,欢迎在评论区留言交流。
更多推荐


所有评论(0)