一、機能の概要
HarmonyOS Nextのカスタムコンポーネント凍結機能は、複雑なUIページのパフォーマンスを最適化するために設計されています。多ページスタック、長いリスト、またはグリッドレイアウトなどのシナリオで特に効果的です。状態変数が複数のUIコンポーネントにバインドされている場合、その変化は大量のUIコンポーネントのリフレッシュを引き起こし、インターフェースのカクつきや応答遅延を招く可能性があります。この機能は、freezeWhenInactive属性を設定することでコンポーネント凍結メカニズムを有効にし、アクティブ状態のカスタムコンポーネントのみを更新することで更新範囲を縮小し、複雑なUIシナリオにおけるリフレッシュ効率を向上させます。非アクティブ状態のコンポーネントが再びアクティブになると、状態管理フレームワークはUIが正しく表示されるように必要なリフレッシュ操作を実行し、多ページスタック、長いリスト、またはグリッドレイアウトなどで現在表示されているコンポーネントのみをリフレッシュし、不可視コンポーネントのリフレッシュを遅延させる、必要に応じたリフレッシュを実現します。
適用シナリオ
- ページルーティング:現在のスタックトップページがactiveで、非表示の非スタックトップページがinactiveです。
- TabContent:現在表示されているTabContent内のカスタムコンポーネントはactive状態にあり、他はinactiveです(初回レンダリング時、Tabは現在表示されているTabContentのみを作成し、すべてのTabContentを切り替えた後で初めてすべてを作成します)。
- LazyForEach:現在表示されているLazyForEach内のカスタムコンポーネントのみがactiveで、キャッシュされたノードのコンポーネントはinactiveです。
- Navigation:現在表示されているNavDestination内のカスタムコンポーネントはactiveで、他の表示されていないNavDestinationコンポーネントはinactiveです。
注意点
コンポーネントのactive/inactiveは可視性と同一ではありません。例えば、スタックレイアウト(Stack)でマスクされたコンポーネントは不可視ですが、inactive状態とは見なされず、コンポーネント凍結の適用範囲外です。この機能はAPIバージョン11からサポートされています。
二、機能の例
(一)ページルーティング
- ページAからページBへ遷移
- ページAの「first page storageLink + 1」ボタンをクリックすると、
storageLink状態変数が変更され、@Watchで登録されたfirstメソッドが呼び出されます。 router.pushUrl({url: 'pages/second'})でページBに遷移すると、ページAは非表示になり、状態がinactiveになります。この時、ページBの「this.storageLink2 += 2」ボタンをクリックすると、ページAの状態変数が凍結されているため、ページBで登録された@Watchのsecondメソッドのみがコールバックされます。- 「back」をクリックしてページAに戻ると、ページAの状態がinactiveからactiveに戻り、以前に凍結された状態変数が再リフレッシュされ、
@Watchで登録されたfirstメソッドが再度呼び出されます。
- 関連コード例
// ページA
import { router } from '@kit.ArkUI';
@Entry
@Component({ freezeWhenInactive: true })
struct PageA {
@StorageLink('PropA') @Watch("onCounterChange") counter: number = 47;
onCounterChange() {
console.info("ページA: " + `${this.counter}`)
}
build() {
Column() {
Text(`ページAから: ${this.counter}`).fontSize(50)
Button('カウントアップ').fontSize(30)
.onClick(() => {
this.counter += 1
})
Button('次のページへ').fontSize(30)
.onClick(() => {
router.pushUrl({ url: 'pages/PageB' })
})
}
}
}
// ページB
import { router } from '@kit.ArkUI';
@Entry
@Component({ freezeWhenInactive: true })
struct PageB {
@StorageLink('PropA') @Watch("onCounterChangeB") counter: number = 1;
onCounterChangeB() {
console.info("ページB: " + `${this.counter}`)
}
build() {
Column() {
Text(`ページBから: ${this.counter}`).fontSize(50)
Button('戻る').fontSize(30)
.onClick(() => {
router.back()
})
Button('カウントアップ x2').fontSize(30)
.onClick(() => {
this.counter += 2
})
}
}
}
(二)TabContent
- 操作と応答
- 「change message」をクリックして
messageの値を変更すると、現在表示されているTabContentコンポーネント内の@Watchで登録されたonDataChangeメソッドがトリガーされます。 - 別のTabContentに切り替えると、そのTabContentの状態がinactiveからactiveに変わり、対応する
@Watchで登録されたonDataChangeメソッドがトリガーされます。 - 再度「change message」をクリックして
messageの値を変更すると、現在表示されているTabContentの子コンポーネント内の@Watchで登録されたonDataChangeメソッドのみがトリガーされます。
- 関連コード例
@Entry
@Component
struct TabDemo {
@State @Watch("onDataChange") data: number = 0;
private tabs: number[] = [0, 1]
onDataChange() {
console.info(`TabContent データ変更コールバック: ${this.data}`)
}
build() {
Row() {
Column() {
Button('データ変更').onClick(() => {
this.data++
})
Tabs() {
ForEach(this.tabs, (item: number ) => {
TabContent() {
TabItemComponent({ data: this.data, index: item })
}.tabBar(`タブ${item}`)
}, (item: number) => item.toString())
}
}
.width('100%')
}
.height('100%')
}
}
@Component({ freezeWhenInactive: true })
struct TabItemComponent {
@Link @Watch("onDataChange") data: number
private index: number = 0
onDataChange() {
console.info(`子コンポーネント データ変更コールバック: ${this.data}, インデックス: ${this.index}`)
}
build() {
Text(`データ: ${this.data}, インデックス: ${this.index}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
(三)LazyForEach
- 操作と応答
- 「change message」をクリックして
messageの値を変更すると、現在表示されているListItem内の子コンポーネントの@Watchで登録されたonValueChangeメソッドがトリガーされます。キャッシュされたノードの@Watchで登録されたメソッドはトリガーされません(コンポーネント凍結を使用しない場合、現在表示されているListItemとcachecountでキャッシュされたノードの@Watchで登録されたonValueChangeメソッドが両方ともwatchコールバックをトリガーします)。 - List領域外のListItemがList領域内にスライドすると、状態がinactiveからactiveに変わり、対応する
@Watchで登録されたonValueChangeメソッドがトリガーされます。 - 再度「change message」をクリックして
messageの値を変更すると、現在表示されているListItem内の子コンポーネントの@Watchで登録されたonValueChangeメソッドのみがトリガーされます。
- 関連コード例
// データソース関連クラス
class DataSource implements IDataSource {
// 省略されたコード
}
class MyDataSource extends DataSource {
// 省略されたコード
}
@Entry
@Component
struct LazyListDemo {
private dataSource: MyDataSource = new MyDataSource();
@State @Watch("onValueChange") value: number = 0;
onValueChange() {
console.info(`LazyForEach データ変更コールバック: ${this.value}`)
}
aboutToAppear() {
for (let i = 0; i <= 20; i++) {
this.dataSource.pushData(`アイテム ${i}`)
}
}
build() {
Column() {
Button('データ変更').onClick(() => {
this.value++
})
List({ space: 3 }) {
LazyForEach(this.dataSource, (item: string ) => {
ListItem() {
ListItemComponent({ value: this.value, index: item })
}
}, (item: string) => item)
}.cachedCount(5).height(500)
}
}
}
@Component({ freezeWhenInactive: true })
struct ListItemComponent {
@Link @Watch("onValueChange") value: number;
private index: string = "";
aboutToAppear() {
console.info(`ListItemComponent aboutToAppear インデックス: ${this.index}`)
}
onValueChange() {
console.info(`ListItemComponent データ変更コールバック: ${this.value}, インデックス: ${this.index}`)
}
build() {
Text(`データ: ${this.value}, インデックス: ${this.index}`)
.width('90%')
.height(160)
.backgroundColor(0xAFEEEE)
.textAlign(TextAlign.Center)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
(四)Navigation
- 操作と応答
- 「change message」をクリックして
messageの値を変更すると、現在表示されているMyNavigationTestStackコンポーネント内の@Watchで登録されたinfoメソッドがトリガーされます。 - 「Next Page」をクリックしてPageOneに切り替え、pageOneStackノードを作成します。再度「change message」をクリックすると、pageOneStack内のNavigationContentMsgStack子コンポーネントの
@Watchで登録されたinfoメソッドのみがトリガーされます。 - このように、ページ切り替えの過程で、現在表示されているページに関連するコンポーネントのみが状態変数の変化に応答します。 「Back Page」をクリックして戻ると、対応するページのコンポーネントが再び状態変化に応答します。
- 関連コード例
@Entry
@Component
struct NavigationDemo {
@Provide('pageInfo') navStack: NavPathStack = new NavPathStack();
@State @Watch("onStatusChange") status: number = 0;
@State logCount: number = 0;
onStatusChange() {
console.info(`Navigation データ変更コールバック: ${this.status}`);
}
@Builder
PageBuilder(name: string) {
if (name === 'pageOne') {
NavPageOne({ status: this.status, logCount: this.logCount })
} else if (name === 'pageTwo') {
NavPageTwo({ status: this.status, logCount: this.logCount })
} else if (name === 'pageThree') {
NavPageThree({ status: this.status, logCount: this.logCount })
}
}
build() {
Column() {
Button('ステータス変更')
.onClick(() => {
this.status++;
})
Navigation(this.navStack) {
Column() {
Button('次のページへ', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.navStack.pushPath({ name: 'pageOne' });
})
}
}.title('ナビゲーション')
.navDestination(this.PageBuilder)
.mode(NavigationMode.Stack)
}
}
}
@Component
struct NavPageOne {
// 省略されたコード
build() {
NavDestination() {
Column() {
NavPageContent({ status: this.status, index: this.index, logCount: this.logCount })
Text("現在のスタックサイズ: " + `${this.navStack.size()}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
Button('次のページへ', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.navStack.pushPathByName('pageTwo', null);
})
Button('前のページへ', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.navStack.pop();
})
}.width('100%').height('100%')
}.title('ページ1')
.onBackPressed(() => {
this.navStack.pop();
return true;
})
}
}
// NavPageTwoとNavPageThreeの構造は同様で、省略されています。
@Component({ freezeWhenInactive: true })
struct NavPageContent {
@Link @Watch("onStatusChange") status: number;
@Link index: number;
@Link logCount: number;
onStatusChange() {
console.info(`NavigationContent データ変更コールバック: ${this.status}`);
console.info(`NavigationContent ---- コンテンツ ${this.index} によって呼び出されました`);
this.logCount++;
}
build() {
Column() {
Text(`ステータス: ${this.status}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
Text(`ログカウント: ${this.logCount}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
}