嵌入式鸿蒙系统开发速成快速学习第二部份,生命周期和网络请求篇
嵌入式鸿蒙核心开发:数据、状态、生命周期与网络请求实战
课程定位:鸿蒙(HarmonyOS NEXT / API 12+)应用开发核心能力课
目标听众:已掌握 ArkTS 基础语法,准备进入项目实战的学员
讲课时长:建议 90~120 分钟(含代码演示与互动问答)
核心理念:数据是血液,生命周期是心跳,网络请求是血管——三者缺一不可
配置视频可能在录制时发布bibi, 课程会现场快速学习
如果大家有过安卓开发和前端开发的话,理解下面的内容可能比较容易,但是如果没有编程经验,那也不怕,我们来一点点深入理解
前面你们学了组件怎么摆、布局怎么排、样式怎么写——这些好比是盖房子的砖瓦水泥。但真正决定一个 App 能不能跑、跑得好不好、会不会崩溃的,是今天我要讲的这四件事:
-
数据处理 —— 你的 App 每时每刻都在和数据打交道
-
数据状态 —— 数据变了,界面怎么跟着变?变错了怎么办?
-
生命周期 —— 页面什么时候生、什么时候死、什么时候该干活
-
网络请求 —— 什么时候发请求、什么时候停请求、页面死了请求怎么办
这四件事,我把它叫做嵌入式鸿蒙开发的"任督二脉"。任督二脉打通了,你写的代码就是工业级;打不通,你写的永远是 Demo。
好,我们正式开始。
第一部分:数据状态管理——鸿蒙开发的"任督二脉"
1.1 为什么状态管理比组件更重要?
我先问大家一个问题:你们写过一个页面,数据改了,但界面没刷新吗?
(停顿,等学员反应)
这就是状态管理没搞懂。在鸿蒙 ArkTS 里,不是你把数据改了,界面就会自动变。你必须告诉系统:"这个数据是状态,它变了你要重新渲染。"
鸿蒙提供了 6 种状态装饰器,但很多学员一上来就乱用
@State,结果对象属性改了不刷新、数组 push 了不刷新,调试半天找不到原因。今天我用项目实战的视角,告诉你什么时候该用什么。
1.2 六大状态装饰器——一张表讲清楚
表格
| 装饰器 | 数据流向 | 谁能改 | 典型场景 | 一句话记忆 |
|---|---|---|---|---|
@State |
组件内部 | 自己 | 开关、计数、输入框 | 我的地盘我做主 |
@Prop |
父 → 子 | 只能父改 | 列表项只读展示 | 爸爸给的,我不能动 |
@Link |
父 ↔ 子 | 父子都能改 | 表单双向绑定 | 父子连心 |
@Provide / @Consume |
祖先 → 任意后代 | 谁都能改 | 主题色、语言切换 | 家族遗传 |
@Observed + @ObjectLink |
对象深度监听 | 持有者 | 商品列表里改收藏状态 | 透视眼,看到骨头里 |
@Watch |
监听回调 | — | 计数超阈值弹提示 | 盯梢的 |
讲师强调:这张表建议学员截图保存,做项目时对照着选,不要凭感觉。
1.3 实战选择:一个电商商品列表该用什么?
假设你在做一个电商 App,页面结构是这样的:
plain
HomePage(首页)
└── ProductList(商品列表组件)
└── ProductItem(单个商品卡片)
└── FavoriteButton(收藏按钮)
现在问:收藏按钮点击后,数据该用哪个装饰器?
错误做法:
ProductItem 里用 @State 存 isFavorite,点击切换。结果退出页面再进来,收藏状态丢了——因为 @State 是组件私有的,页面重建就重置。正确做法:
TypeScript
// 1. 数据模型用 @Observed 标记
@Observed
class Product {
id: number
name: string
isFavorite: boolean = false
constructor(id: number, name: string) {
this.id = id
this.name = name
}
}
// 2. 首页持有真实数据
@Entry
@Component
struct HomePage {
@State productList: Product[] = [
new Product(1, 'Mate 70'),
new Product(2, 'FreeBuds')
]
build() {
List() {
ForEach(this.productList, (item: Product) => {
ListItem() {
ProductItem({ product: item }) // 传对象引用
}
})
}
}
}
// 3. 子组件用 @ObjectLink 深度监听
@Component
struct ProductItem {
@ObjectLink product: Product // 关键!深度监听对象内部变化
build() {
Row() {
Text(this.product.name)
Button(this.product.isFavorite ? '❤️' : '🤍')
.onClick(() => {
this.product.isFavorite = !this.product.isFavorite // 触发刷新!
})
}
}
}
理解要点:
-
@Observed必须打在 class 定义上,不是实例上 -
@ObjectLink接收的是 引用,不是拷贝 -
如果不用
@Observed+@ObjectLink,你点收藏按钮,心形图标不会变
1.4 数据处理三大坑——学员必踩,提前预警
坑一:数组 push 了,界面不刷新
TypeScript
// ❌ 错误:直接 push,ArkTS 检测不到引用变化
this.itemList.push(newItem)
// ✅ 正确:创建新数组,触发重新渲染
this.itemList = [...this.itemList, newItem]
// 或者 splice 后重新赋值
this.itemList.splice(index, 1)
this.itemList = this.itemList // 重新赋值触发刷新
坑二:对象属性改了,界面不刷新
TypeScript
// ❌ 错误:直接改属性,无 @Observed 时不刷新
this.user.name = '李四'
// ✅ 正确:整体替换对象
this.user = { ...this.user, name: '李四' }
// 或者给 class 加 @Observed,用 @ObjectLink
坑三:ForEach 没有 key,列表乱序/重复
TypeScript
// ❌ 错误:没有第三个参数
ForEach(this.list, (item) => { ... })
// ✅ 正确:提供唯一 key
ForEach(this.list, (item) => { ... }, (item) => item.id.toString())
反复强调:这三个坑,学员自己做项目时 100% 会遇到。今天提前讲,就是让你们少走弯路。
第二部分:生命周期——数据加载的"指挥棒"
2.1 为什么生命周期决定了你的 App 会不会崩溃?
我再问大家:你们有没有遇到过,页面退出了,但网络请求还在跑,结果控制台报错?或者定时器没清,页面重建后越来越快?
这就是生命周期管理失控。
生命周期不是"几个回调函数背下来就完事",它是鸿蒙系统告诉你的"现在该做什么"的信号。你收到了信号,做对了事,App 就稳;你无视信号,该清的不清,该停的不停,App 就崩。
2.2 页面生命周期完整流程——一张图讲透
plain
用户点击图标 → 打开 App
│
▼
┌─────────────────────────────────────┐
│ aboutToAppear() │ ← 【信号】"我出生了,准备干活"
│ 用途:初始化变量、注册监听、发起首屏请求 │
│ 注意:此时 UI 还没渲染,不要操作 DOM │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ build() │ ← 构建 UI 树
│ 用途:根据当前状态渲染界面 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ onDidBuild() (API 12+) │ ← 【信号】"我画完了"
│ 用途:可以安全地获取组件尺寸、做动画 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ onPageShow() │ ← 【信号】"用户看到我了"
│ 用途:刷新数据、恢复计时器、上报埋点 │
│ 注意:从其他页面返回时也会触发! │
└─────────────────────────────────────┘
│
▼
【用户正在使用页面】
│
▼
┌─────────────────────────────────────┐
│ 用户点击跳转 → 新页面压栈 │
│ onPageHide() │ ← 【信号】"我被盖住了"
│ 用途:暂停视频、暂停轮播、保存草稿 │
│ 注意:页面还在内存里,没死! │
└─────────────────────────────────────┘
│
▼
【用户按返回键】
│
▼
┌─────────────────────────────────────┐
│ onPageShow() │ ← 又回来了!再次触发
│ 用途:重新刷新数据、恢复状态 │
└─────────────────────────────────────┘
│
▼
【用户关闭页面】
│
▼
┌─────────────────────────────────────┐
│ aboutToDisappear() │ ← 【信号】"我要死了,遗言吧"
│ 用途:取消网络请求、清除定时器、 │
│ 解绑事件监听、保存必须持久化的数据 │
│ 注意:这是最后的机会,不做清理就泄漏! │
└─────────────────────────────────────┘
│
▼
【页面销毁,内存回收】
理解要点:
-
aboutToAppear和aboutToDisappear只触发一次,像人的出生和死亡 -
onPageShow和onPageHide可能触发多次,像人的上班和下班 -
所有清理工作必须在
aboutToDisappear里完成,这是铁律
2.3 关键问题:数据到底该在哪个生命周期加载?
这是学员问得最多的问题。我给出三条铁律:
铁律一:首屏数据 → aboutToAppear
TypeScript
aboutToAppear() {
// ✅ 正确:页面还没显示,就开始请求,用户看到时数据已就绪
this.loadHomeData()
}
为什么不在
onPageShow?因为 onPageShow 时页面已经可见了,你再发请求,用户会先看到空白或 Loading,体验差。铁律二:需要刷新的数据 → onPageShow
TypeScript
onPageShow() {
// ✅ 正确:从其他页面返回时,刷新未读消息数
this.loadUnreadCount()
// ✅ 正确:恢复页面时,重新连接 WebSocket
this.reconnectWebSocket()
}
铁律三:清理工作 → aboutToDisappear
TypeScript
aboutToDisappear() {
// ✅ 正确:取消进行中的 HTTP 请求
this.httpRequest.destroy()
// ✅ 正确:清除定时器
clearInterval(this.timerId)
clearTimeout(this.timeoutId)
// ✅ 正确:解绑事件监听
this.eventHub.off('dataUpdate')
}
反复强调:这三条铁律背下来,你的 App 稳定性超过 80% 的初学者。
2.4 组件生命周期——定时器的生死簿
自定义组件没有
onPageShow / onPageHide,只有 aboutToAppear 和 aboutToDisappear。但组件更容易出内存泄漏,因为学员经常忘了清定时器。TypeScript
@Component
struct CountdownTimer {
@State seconds: number = 60
private timerId: number = -1
aboutToAppear() {
// 启动倒计时
this.timerId = setInterval(() => {
if (this.seconds > 0) {
this.seconds--
} else {
clearInterval(this.timerId)
}
}, 1000)
}
aboutToDisappear() {
// ⚠️ 必须清理!否则组件销毁了,定时器还在跑,访问已销毁的状态会崩溃
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
}
build() {
Text(`${this.seconds}秒`)
.fontSize(24)
.fontColor(this.seconds <= 10 ? '#FA2C2C' : '#333333')
}
}
理解要点:
-
组件里用了
setInterval、setTimeout、eventHub.on、http.request,必须在aboutToDisappear里配对清理 -
建议把需要清理的资源 ID 都存在组件的私有变量里(如
timerId),方便统一清理
第三部分:网络请求与生命周期的"生死绑定"
3.1 网络请求不是"发出去就完事"
很多学员写网络请求,就三行代码:
TypeScript
// ❌ 初学者写法——裸奔请求
http.request('https://api.xxx.com/data', (err, data) => {
this.list = data
})
这代码在 Demo 里能跑,但在真机上:
-
网络慢,用户退出页面了,请求回来试图更新已销毁的组件 → 崩溃
-
用户快速切换页面,请求堆积 → 内存暴涨
-
没有 Loading、没有错误处理 → 用户体验为零
工业级网络请求,必须和生命周期绑定。
3.2 工业级 HTTP 封装——生命周期感知
TypeScript
import { http } from '@kit.NetworkKit'
/**
* 生命周期感知的 HTTP 请求封装
* 核心设计:页面销毁时自动取消进行中的请求,防止回调地狱
*/
class LifecycleHttpClient {
private httpClient: http.HttpRequest
private isDestroyed: boolean = false // 关键:标记是否已销毁
constructor() {
this.httpClient = http.createHttp()
}
/**
* 安全的 GET 请求
* @param url 请求地址
* @param onSuccess 成功回调
* @param onError 错误回调
*/
async safeGet<T>(
url: string,
onSuccess: (data: T) => void,
onError: (msg: string) => void
): Promise<void> {
// 如果页面已销毁,直接拒绝
if (this.isDestroyed) {
console.warn('请求被拒绝:HTTP Client 已销毁')
return
}
try {
const response = await this.httpClient.request(url, {
method: http.RequestMethod.GET,
header: { 'Content-Type': 'application/json' },
connectTimeout: 15000, // 15秒连接超时
readTimeout: 15000 // 15秒读取超时
})
// 再次检查:请求发出去后页面销毁了,回调不执行
if (this.isDestroyed) {
console.warn('响应被丢弃:页面已销毁')
return
}
if (response.responseCode === 200) {
const data = JSON.parse(response.result.toString()) as T
onSuccess(data)
} else {
onError(`服务器错误:${response.responseCode}`)
}
} catch (error) {
if (!this.isDestroyed) {
onError('网络请求失败,请检查网络连接')
}
}
}
/**
* 销毁客户端:页面 aboutToDisappear 时必须调用
*/
destroy(): void {
this.isDestroyed = true
this.httpClient.destroy()
console.info('HTTP Client 已安全销毁')
}
}
export const createHttpClient = () => new LifecycleHttpClient()
反复要求理解要点:
-
isDestroyed标志位是防崩溃的核心。页面销毁后,任何回调都不执行 -
超时设置不能省,默认无超时,请求挂住会泄漏
-
destroy()必须在aboutToDisappear里调用
3.3 完整实战:一个生命周期绑定的网络请求页面
下面这个页面,包含了工业级开发的所有要素:加载态、错误态、空态、成功态、下拉刷新、生命周期绑定。
TypeScript
import { router } from '@kit.ArkUI'
import { createHttpClient, LifecycleHttpClient } from '../utils/LifecycleHttpClient'
interface Product {
id: number
name: string
price: number
image: string
}
interface ApiResponse<T> {
code: number
data: T
message: string
}
@Entry
@Component
struct ProductListPage {
// ========== 数据状态 ==========
@State productList: Product[] = []
@State isLoading: boolean = false // 首次加载
@State isRefreshing: boolean = false // 下拉刷新
@State isLoadingMore: boolean = false // 上拉加载更多
@State errorMsg: string = '' // 错误信息
@State currentPage: number = 1
@State hasMore: boolean = true
// ========== 生命周期资源 ==========
private httpClient: LifecycleHttpClient = createHttpClient()
private scrollY: number = 0
// ========== 生命周期管理 ==========
aboutToAppear() {
console.info('【生命周期】aboutToAppear:页面即将出现,发起首屏请求')
this.loadData(true) // true = 首次加载
}
onPageShow() {
console.info('【生命周期】onPageShow:页面显示,检查是否需要刷新')
// 如果从详情页返回且购买了商品,可以在这里刷新列表
// this.loadData(true)
}
onPageHide() {
console.info('【生命周期】onPageHide:页面隐藏,暂停非必要任务')
// 暂停轮播、暂停视频等
}
aboutToDisappear() {
console.info('【生命周期】aboutToDisappear:页面即将销毁,清理所有资源')
// ⚠️ 铁律:销毁 HTTP 客户端,取消进行中的请求
this.httpClient.destroy()
// 如果有定时器、事件监听,也在这里清理
}
// ========== 数据加载逻辑 ==========
async loadData(isRefresh: boolean = false) {
if (isRefresh) {
this.isLoading = true
this.currentPage = 1
this.hasMore = true
} else {
if (this.isLoadingMore || !this.hasMore) return
this.isLoadingMore = true
}
this.errorMsg = ''
await this.httpClient.safeGet<ApiResponse<Product[]>>(
`https://api.example.com/products?page=${this.currentPage}&size=10`,
(response) => {
// ✅ 成功回调:此时页面一定还活着(isDestroyed 已检查)
if (response.code === 200) {
if (isRefresh) {
this.productList = response.data
} else {
this.productList = [...this.productList, ...response.data]
}
this.hasMore = response.data.length === 10
this.currentPage++
} else {
this.errorMsg = response.message || '数据异常'
}
},
(errorMsg) => {
// ✅ 错误回调
this.errorMsg = errorMsg
}
)
// 统一关闭加载态
this.isLoading = false
this.isRefreshing = false
this.isLoadingMore = false
}
// ========== UI 构建 ==========
build() {
Column() {
// 顶部标题栏
this.HeaderBuilder()
// 内容区域
if (this.isLoading && this.productList.length === 0) {
// 首次加载中
this.LoadingBuilder()
} else if (this.errorMsg !== '' && this.productList.length === 0) {
// 首次加载失败
this.ErrorBuilder()
} else if (this.productList.length === 0) {
// 空数据
this.EmptyBuilder()
} else {
// 正常列表
this.ListBuilder()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
HeaderBuilder() {
Row() {
Text('商品列表')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(`共${this.productList.length}件`)
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
.justifyContent(FlexAlign.SpaceBetween)
}
@Builder
LoadingBuilder() {
Column() {
LoadingProgress()
.width(40)
.height(40)
.color('#007DFF')
Text('正在加载商品...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 12 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
@Builder
ErrorBuilder() {
Column() {
Image($r('app.media.icon_error'))
.width(80)
.height(80)
.opacity(0.5)
Text(this.errorMsg)
.fontSize(16)
.fontColor('#666666')
.margin({ top: 16 })
Button('重新加载')
.width(120)
.height(40)
.fontSize(14)
.backgroundColor('#007DFF')
.margin({ top: 20 })
.onClick(() => {
this.loadData(true)
})
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
@Builder
EmptyBuilder() {
Column() {
Image($r('app.media.icon_empty'))
.width(80)
.height(80)
.opacity(0.5)
Text('暂无商品')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 16 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
@Builder
ListBuilder() {
List({ space: 12 }) {
ForEach(this.productList, (item: Product) => {
ListItem() {
Row() {
Image(item.image)
.width(80)
.height(80)
.borderRadius(8)
.objectFit(ImageFit.Cover)
Column() {
Text(item.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(`¥${item.price}`)
.fontSize(18)
.fontColor('#FF6600')
.fontWeight(FontWeight.Bold)
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
}
.onClick(() => {
router.pushUrl({ url: 'pages/ProductDetail', params: { id: item.id } })
})
}, (item: Product) => item.id.toString())
// 底部加载更多
if (this.hasMore) {
ListItem() {
Row() {
if (this.isLoadingMore) {
LoadingProgress().width(20).height(20).color('#007DFF')
Text('加载中...').fontSize(14).fontColor('#999999').margin({ left: 8 })
} else {
Text('上拉加载更多').fontSize(14).fontColor('#999999')
}
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
}
}
.width('100%')
.layoutWeight(1)
.padding(16)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
.onReachEnd(() => {
// 触底加载更多
if (!this.isLoadingMore && this.hasMore) {
this.loadData(false)
}
})
.refresh({
refreshing: $$this.isRefreshing,
builder: this.RefreshBuilder,
onRefresh: () => {
this.loadData(true)
}
})
}
@Builder
RefreshBuilder() {
Row() {
LoadingProgress().width(24).height(24).color('#007DFF')
Text('下拉刷新').fontSize(14).fontColor('#999999').margin({ left: 8 })
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.Center)
}
}
3.4 这个页面的工业级设计要点
讲师对照代码,逐条讲解:
-
四态管理:首次加载(Loading)、错误(Error)、空数据(Empty)、正常(List)——任何列表页面都必须有这四态,不能只写成功态。
-
生命周期绑定:
httpClient在aboutToDisappear里销毁,页面死了请求也跟着死,不会崩溃。 -
防重复请求:
isLoadingMore标志位防止用户快速滑动触发多次请求。 -
分页逻辑:
currentPage+hasMore,标准分页模式。 -
下拉刷新 + 上拉加载:
refresh属性 +onReachEnd事件,鸿蒙原生支持。 -
key 函数:
ForEach第三个参数(item) => item.id.toString(),防止列表错乱。
第四部分:综合串联——从页面打开到关闭的完整数据流
4.1 一张图讲清"数据 + 生命周期 + 网络"的协作关系
plain
用户点击"商品列表"
│
▼
┌──────────────────────────────────────────────┐
│ aboutToAppear() │
│ 1. 创建 httpClient(新的生命周期实例) │
│ 2. 初始化状态:isLoading=true, page=1 │
│ 3. 调用 loadData(true) 发起首屏请求 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ HTTP 请求发送中... │
│ 此时用户看到 Loading 动画 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 请求成功返回 / 失败返回 │
│ httpClient 检查 isDestroyed? │
│ ├── 是 → 丢弃响应,防止崩溃 │
│ └── 否 → 执行回调,更新 productList │
│ ↓ │
│ @State 触发 → build() 重新执行 → UI 刷新 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 用户滑动列表 → onReachEnd 触发 │
│ loadData(false) 发起分页请求 │
│ isLoadingMore=true 防止重复请求 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 用户点击某个商品 → router.pushUrl() │
│ 跳转详情页 │
│ onPageHide() 触发:暂停非必要任务 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 用户在详情页购买后返回 │
│ onPageShow() 触发:可选刷新列表数据 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 用户按返回键退出页面 │
│ aboutToDisappear() 触发: │
│ 1. httpClient.destroy() → 取消进行中的请求 │
│ 2. clearInterval / clearTimeout │
│ 3. eventHub.off() 解绑监听 │
│ 4. 保存需要持久化的数据(如草稿) │
└──────────────────────────────────────────────┘
│
▼
【页面销毁,内存干净释放】
讲师强调:这张图就是鸿蒙开发的"心法图"。你每写一个页面,脑子里都要过一遍这个流程。
4.2 实战口诀——三句背下来
-
"出生要干活,死前要收尸"
-
aboutToAppear里初始化 + 发首屏请求 -
aboutToDisappear里销毁请求 + 清定时器
-
-
"数据是状态,状态变才刷新"
-
必须用
@State/@ObjectLink等装饰器标记 -
数组/对象修改要创建新引用
-
-
"请求绑生命周期,死了不回调"
-
HTTP Client 封装
isDestroyed标志位 -
页面销毁 = 请求自动取消
-
第五部分:十大常见错误与最佳实践
5.1 十大坑点(讲师逐条讲解,学员对号入座)
表格
| # | 错误现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 1 | 数据改了,UI 不刷新 | 没用 @State 或 @ObjectLink |
给数据打上对应装饰器 |
| 2 | 数组 push 不刷新 | ArkTS 检测不到引用变化 | list = [...list, item] |
| 3 | 对象属性改了不刷新 | 无 @Observed |
class 加 @Observed,组件用 @ObjectLink |
| 4 | 页面退出后控制台报错 | 请求回调试图更新已销毁组件 | HTTP Client 加 isDestroyed 标志 |
| 5 | 列表滑动越来越卡 | ForEach 没有 key | 提供唯一 key 函数 |
| 6 | 快速切换页面后崩溃 | 定时器没清,访问已销毁状态 | aboutToDisappear 里 clearInterval |
| 7 | 返回页面数据没更新 | 没在 onPageShow 刷新 |
需要刷新的数据在 onPageShow 里重新加载 |
| 8 | 下拉刷新后列表重复 | 刷新时没清空原数组 | 刷新时 this.list = [] 再赋值 |
| 9 | 上拉加载触发多次 | 没有 isLoadingMore 标志 |
加布尔锁,请求中禁止再次触发 |
| 10 | 网络慢时页面白屏 | 没有 Loading 态 | 四态管理:Loading / Error / Empty / Content |
5.2 最佳实践清单
plain
□ 每个 @State 变量都要想清楚:谁创建?谁修改?谁消费?
□ 每个网络请求都要配:Loading 态 + Error 态 + 超时设置
□ 每个 setInterval / setTimeout 都要在 aboutToDisappear 里清理
□ 每个 ForEach 都要提供第三个 key 参数
□ 每个 HTTP Client 都要在 aboutToDisappear 里 destroy
□ 每个页面都要考虑:首次加载、下拉刷新、上拉加载、空数据、错误重试
□ 每个对象属性修改都要考虑:是否需要 @Observed + @ObjectLink
□ 每个页面跳转传参都要在目标页用 router.getParams() 接收
第六部分:课后作业与扩展思考
作业一:改造现有 Demo(必做)
找一个你之前写的列表页面,按今天的标准改造:
-
加上
@Observed+@ObjectLink(如果有对象数据) -
加上生命周期感知的 HTTP Client
-
加上四态管理(Loading / Error / Empty / Content)
-
加上
aboutToDisappear清理逻辑
作业二:思考题(选做)
-
如果用户在网络请求过程中按了 Home 键(
onPageHide),请求该不该取消?为什么? -
@Link和@Provide/@Consume都能实现跨组件通信,什么场景下该用哪个? -
如果商品列表有 1000 条数据,全部用
@State存会有性能问题吗?鸿蒙有什么优化手段?
扩展阅读
-
HarmonyOS 官方文档:状态管理(developer.huawei.com)
-
ArkTS 性能优化指南:List 懒加载、组件复用
-
鸿蒙网络请求最佳实践:请求拦截器、Token 自动刷新
我们用 90 分钟,把鸿蒙开发中最核心的四件事串在了一起:
-
数据状态 告诉你:什么数据该用什么方式管理
-
生命周期 告诉你:什么时候该加载、什么时候该清理
-
网络请求 告诉你:怎么发请求才不会崩溃
-
四态管理 告诉你:怎么让用户在任何情况下都有好体验
这四件事不是孤立的,它们是绑在一起工作的。你写一个页面,就是在写一条数据流:从生命周期启动,到网络请求获取,到状态管理更新,到 UI 渲染呈现,最后生命周期收尾,干净地释放资源。
这条数据流跑通了,你就是合格的鸿蒙开发工程师。
下节课,我们讲鸿蒙的自定义组件高级技巧和动画系统,把界面做得更漂亮。
下课!
文档版本:v1.0
编写日期:2026-07-25
适用 API:HarmonyOS API 12+
配套代码:LifecycleHttpClient.ts+ProductListPage.ets
浙公网安备 33010602011771号