動態

詳情 返回 返回

鴻蒙 HarmonyOS 6|ArkUI(03):狀態管理 - 動態 詳情

前言

我們這篇文章會把 ArkUI 在鴻蒙 6 裏的狀態管理講清楚。

我們會按照三個層次來拆,也就是組件內局部狀態、父子之間的同步、跨層的共享。我們會配上可運行的最小片段,並在最後給出一張決策表和一份常見誤用清單。

一、先把三個層次的全景圖説清楚

在 HarmonyOS 6 的 ArkUI 裏,狀態管理有一套清晰的裝飾器體系。組件自己的可變數據用 @State 來承載,父子之間的單向同步用 @Prop 來承載,父子之間需要雙向同步時用 @Link 來承載,跨多層的共享用 @Provide@Consume 來承載。

二、組件內局部狀態:@State

@State 表示這個組件的本地可變數據。我們只要更新它,ArkUI 就會定位到依賴它的節點並觸發重渲染。這個特性讓我們可以放心把組件的細小交互放在本地管理,不必把所有變量都提到共享層。

@Component
export struct CounterCard {
  @State count: number = 0;

  build() {
    Column({ space: 10 }) {
      Text(`當前計數是 ${this.count}`).fontSize(16)
      Button('加一').onClick(() => {
        this.count += 1;
      })
    }
    .padding(12)
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      CounterCard()
    }
    .justifyContent(FlexAlign.Center) // 讓組件在豎直方向居中
    .width('100%')
    .height('100%')
    .padding(24)
  }
}

這段代碼體現了狀態驅動的基本套路。我們把界面寫成狀態的投影,用户點一次按鈕,count 改一次,界面就會跟着更新。官方的狀態章節明確説明了這種渲染綁定的機制和適用範圍,我們保持就近封裝就可以。

三、父子單向同步:@Prop

當父組件有一個數據需要交給子組件展示,但不希望子組件反向修改父組件時,我們使用 @Prop。父組件的狀態變化會單向同步到子組件,而子組件對 @Prop 的修改不會再寫回父組件的數據源。

@Component
export struct TitleView {
  @Prop title: string

  build() {
    Text(this.title)
      .fontSize(18)
      .fontWeight(FontWeight.Medium)
  }
}

@Component
export struct PageHeader {
  @State pageTitle: string = '我的頁面'

  build() {
    Column({ space: 8 }) {
      TitleView({ title: this.pageTitle })
      Button('改標題')
        .onClick(() => {
          this.pageTitle = '新的標題'
        })
    }
    .padding(12)
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      PageHeader()
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .padding(24)
  }
}

在這段代碼裏,PageHeader 持有真實狀態,TitleView 只接收並展示。官方的 @Prop 文檔還強調了初始化規則和與其他裝飾器的組合關係,我們按文檔理解數據流,就能避免意外的雙向寫回。

四、父子雙向同步:@Link

當子組件需要既接到父組件的值,又要把自己的修改同步回父組件時,我們使用 @Link。這相當於在父子之間建立一條雙向的通道,父變子跟,子改父也變,非常適合輸入框這類場景。(Medium)

@Component
export struct NameInput {
  @Link name: string

  build() {
    Column({ space: 8 }) {
      Text('請輸入名字').fontSize(14)
      TextInput({ text: this.name })
        .onChange((v: string) => {
          this.name = v
        })
        .width('100%')
    }
    .padding(12)
  }
}

@Component
export struct ProfileForm {
  @State userName: string = '小雨'

  build() {
    Column({ space: 10 }) {
      // 關鍵:@Link 需要按“引用”傳參
      NameInput({ name: $userName })
      Text(`歡迎你,${this.userName}`).fontSize(16)
    }
    .padding(12)
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      ProfileForm()
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .padding(24)
  }
}

這裏的 NameInput 收到初始值後,可以把輸入變化回寫到 ProfileForm

五、跨層共享:@Provide@Consume

當多個層級的組件都需要訪問同一份數據,而中間層只是路過時,我們使用 @Provide@Consume。父輩通過 @Provide 暴露一個可共享的狀態,後代通過 @Consume 直接拿到這份狀態,並且形成雙向同步的關係。

// 頂層提供共享狀態
@Component
export struct AppShell {
  @Provide theme: 'light' | 'dark' = 'light';

  build() {
    Column({ space: 12 }) {
      ThemeSwitcher()
      ContentArea()
    }
    .justifyContent(FlexAlign.Center)
    .padding(24)
    .width('100%')
    .height('100%')
    .backgroundColor(this.theme === 'dark' ? '#0f0f0f' : '#ffffff')
  }
}

// 任意後代直接消費共享狀態
@Component
export struct ThemeSwitcher {
  @Consume theme: 'light' | 'dark';

  build() {
    Row({ space: 12 }) {
      Text(`當前主題是 ${this.theme}`).fontSize(16)
      Button('切到淺色').onClick(() => { this.theme = 'light' })
      Button('切到深色').onClick(() => { this.theme = 'dark' })
      Button('切換').onClick(() => {
        this.theme = this.theme === 'dark' ? 'light' : 'dark'
      })
    }
  }
}

// 內容區域:隨主題變化
@Component
export struct ContentArea {
  @Consume theme: 'light' | 'dark';

  build() {
    Column({ space: 8 }) {
      Text(this.theme === 'dark' ? '暗色模式已啓用' : '淺色模式已啓用')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
      Text('這是一段示例內容').opacity(0.8)
    }
    .padding(16)
    .width('100%')
    .backgroundColor(this.theme === 'dark' ? '#1e1e1e' : '#f6f6f6')
    .borderRadius(12)
  }
}

// 入口頁:根節點必須是“容器”
@Entry
@Component
struct Index {
  build() {
    Column() {
      AppShell()      // 自定義組件仍然作為容器的子節點
    }
    .width('100%')
    .height('100%')
  }
}

這段代碼説明了共享的真正含義。我們在頂層放一次 @Provide,就能讓深層組件通過 @Consume 直接拿到同一份變量。

六、把狀態和導航配合起來更順手

在鴻蒙 6 的推薦導航範式裏,我們通常會在入口頁通過 @Provide 暴露一個 NavPathStack,讓子頁在需要返回時直接 pop,或者在需要進入下一個子頁時 pushPath。這種寫法把頁面狀態和頁面棧都放在一個可控的範圍裏,跳轉和返回更容易理解。

// 明確聲明允許的路由名
type RouteName = 'Detail'

// 如果後續要傳參,在這裏把字段寫清楚:比如 { id: number; title?: string }
interface DetailParam {}

// 入口:用 Navigation 作為根容器
@Entry
@Component
export struct Index {
  @Provide('stack') stack: NavPathStack = new NavPathStack()

  // navDestination 的 builder:顯式類型避免 any/unknown
  @Builder
  pageMap(name: RouteName, param?: DetailParam) {
    if (name === 'Detail') {
      NavDestination() {
        DetailPage()
      }
    }
  }

  build() {
    Navigation(this.stack) {
      Column({ space: 12 }) {
        Button('進入詳情')
          .onClick(() => {
            // 不傳參也符合 DetailParam
            this.stack.pushPath({ name: 'Detail' })
          })
      }
      .padding(24)
    }
    .navDestination(this.pageMap)
  }
}

@Component
export struct DetailPage {
  @Consume('stack') stack: NavPathStack

  build() {
    Column({ space: 12 }) {
      Text('Detail Page').fontSize(18).fontWeight(FontWeight.Medium)
      Button('返回').onClick(() => this.stack.pop())
    }
    .padding(24)
  }
}

我們把頁面棧作為共享狀態提供給後代,這樣子頁就不需要額外拿路由句柄了。官方的導航文檔和相關問答都建議用這條思路來組織頁面,代碼更簡潔,也更符合鴻蒙 6 的範式。

七、選型決策表:先局部,後父子,再跨層

我們現在把日常開發裏的常見情景放進一張表,方便快速決策。

場景 推薦裝飾器 數據方向 説明
組件內部的小交互 @State 組件內單點更新 變更隻影響本組件,渲染範圍最小,成本最低。
父傳子展示 @Prop 父到子單向 子組件不回寫,穩定可控,利於定位問題。
父子聯動輸入 @Link 父子雙向 僅在確需回寫時使用,避免到處雙向同步。
多層共享主題或會話 @Provide/@Consume 祖先與後代雙向 避免層層傳參,控制好共享的邊界與變更頻率。

這張表背後的原則很直白。共享範圍越小,優先級越高;能局部就局部,能單向就不雙向,能父子就不要全局。等到確實需要跨層共享時,我們再引入 @Provide/@Consume,並把更新頻率和依賴範圍都控制住。

八、最佳實踐清單

第一條建議是控制更新面。我們儘量在計算階段使用臨時變量,把最終結果一次性寫回狀態變量,這樣可以減少多次寫狀態帶來的重複渲染。

第二條建議是分層管理狀態。我們把組件內狀態與共享狀態分開,避免把所有變量都提升為共享,這樣渲染邊界會更清楚。

第三條建議是謹慎使用雙向同步。我們在能用單向數據流完成需求的情況下,就不要輕易引入 @Link,因為它會擴大可變範圍。只有在輸入控件確實需要回寫到父組件時,才使用雙向。

第四條建議是把跨層共享和導航解耦。我們讓 @Provide/@Consume 只處理需要跨層的狀態,讓 Navigation 只處理頁面棧,這樣兩個系統各司其職,問題更容易定位。

九、總結

我們已經把 HarmonyOS6 的 ArkUI 狀態管理拆成了三個層次,並且用示例把每個層次都跑了起來。

Add a new 評論

Some HTML is okay.