嵌入式鸿蒙系统开发速成快速学习第二部份,生命周期和网络请求篇

嵌入式鸿蒙核心开发:数据、状态、生命周期与网络请求实战

课程定位:鸿蒙(HarmonyOS NEXT / API 12+)应用开发核心能力课
目标听众:已掌握 ArkTS 基础语法,准备进入项目实战的学员
讲课时长:建议 90~120 分钟(含代码演示与互动问答)
核心理念:数据是血液,生命周期是心跳,网络请求是血管——三者缺一不可

配置视频可能在录制时发布bibi, 课程会现场快速学习

如果大家有过安卓开发和前端开发的话,理解下面的内容可能比较容易,但是如果没有编程经验,那也不怕,我们来一点点深入理解

 
前面你们学了组件怎么摆、布局怎么排、样式怎么写——这些好比是盖房子的砖瓦水泥。但真正决定一个 App 能不能跑、跑得好不好、会不会崩溃的,是今天我要讲的这四件事:
  1. 数据处理 —— 你的 App 每时每刻都在和数据打交道
  2. 数据状态 —— 数据变了,界面怎么跟着变?变错了怎么办?
  3. 生命周期 —— 页面什么时候生、什么时候死、什么时候该干活
  4. 网络请求 —— 什么时候发请求、什么时候停请求、页面死了请求怎么办
这四件事,我把它叫做嵌入式鸿蒙开发的"任督二脉"。任督二脉打通了,你写的代码就是工业级;打不通,你写的永远是 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 这个页面的工业级设计要点

讲师对照代码,逐条讲解:
  1. 四态管理:首次加载(Loading)、错误(Error)、空数据(Empty)、正常(List)——任何列表页面都必须有这四态,不能只写成功态。
  2. 生命周期绑定:httpClient 在 aboutToDisappear 里销毁,页面死了请求也跟着死,不会崩溃。
  3. 防重复请求:isLoadingMore 标志位防止用户快速滑动触发多次请求。
  4. 分页逻辑:currentPage + hasMore,标准分页模式。
  5. 下拉刷新 + 上拉加载:refresh 属性 + onReachEnd 事件,鸿蒙原生支持。
  6. 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 实战口诀——三句背下来

  1. "出生要干活,死前要收尸"
    • aboutToAppear 里初始化 + 发首屏请求
    • aboutToDisappear 里销毁请求 + 清定时器
  2. "数据是状态,状态变才刷新"
    • 必须用 @State / @ObjectLink 等装饰器标记
    • 数组/对象修改要创建新引用
  3. "请求绑生命周期,死了不回调"
    • 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(必做)

找一个你之前写的列表页面,按今天的标准改造:
  1. 加上 @Observed + @ObjectLink(如果有对象数据)
  2. 加上生命周期感知的 HTTP Client
  3. 加上四态管理(Loading / Error / Empty / Content)
  4. 加上 aboutToDisappear 清理逻辑

作业二:思考题(选做)

  1. 如果用户在网络请求过程中按了 Home 键(onPageHide),请求该不该取消?为什么?
  2. @Link 和 @Provide/@Consume 都能实现跨组件通信,什么场景下该用哪个?
  3. 如果商品列表有 1000 条数据,全部用 @State 存会有性能问题吗?鸿蒙有什么优化手段?

扩展阅读

  • HarmonyOS 官方文档:状态管理(developer.huawei.com)
  • ArkTS 性能优化指南:List 懒加载、组件复用
  • 鸿蒙网络请求最佳实践:请求拦截器、Token 自动刷新

 

我们用 90 分钟,把鸿蒙开发中最核心的四件事串在了一起:
  • 数据状态 告诉你:什么数据该用什么方式管理
  • 生命周期 告诉你:什么时候该加载、什么时候该清理
  • 网络请求 告诉你:怎么发请求才不会崩溃
  • 四态管理 告诉你:怎么让用户在任何情况下都有好体验
这四件事不是孤立的,它们是绑在一起工作的。你写一个页面,就是在写一条数据流:从生命周期启动,到网络请求获取,到状态管理更新,到 UI 渲染呈现,最后生命周期收尾,干净地释放资源。
这条数据流跑通了,你就是合格的鸿蒙开发工程师。
下节课,我们讲鸿蒙的自定义组件高级技巧和动画系统,把界面做得更漂亮。
下课!

文档版本:v1.0
编写日期:2026-07-25
适用 API:HarmonyOS API 12+
配套代码:LifecycleHttpClient.ts + ProductListPage.ets
posted @ 2026-07-25 16:42  鬼门元歌  阅读(15)  评论(0)    收藏  举报