HarmonyOS Nextにおける@Paramデコレータの活用

HarmonyOS Nextのコンポーネント指向フレームワークにおいて、子コンポーネントが親からデータを受け取る際の柔軟性と明確性を向上させるため、@Paramデコレータが導入されました。API version 12以降、@ComponentV2で定義されたカスタムコンポーネント内で@Paramを使用することが可能です。

@Paramデコレータの概要

@Paramは、親コンポーネントから子コンポーネントへ一方向のデータフローを実現するための仕組みです。その主な特性は以下の通りです。

  • 外部入力の表現: 親コンポーネントからのデータ入力を示し、親子のデータ同期を可能にします。
  • 変数初期化: ローカルでの初期化をサポートしますが、コンポーネント内部で変数を直接変更することはできません(ただし、オブジェクト型変数の内部プロパティは変更可能です)。ローカルで初期化しない場合、@Requireデコレータと併用することで、外部からの初期化が必須となります。
  • 同期タイプ: 親から子への一方向同期です。
  • 多様なデータソース: プリミティブ型変数、状態変数、定数、関数の戻り値など、あらゆる型のデータソースを受け入れます。nullundefined、および共用体型もサポートします。
  • UI更新のトリガー: @Paramで修飾された変数の値が変更されると、その変数に関連するUIが自動的に更新されます。

従来のデータフローとの比較

HarmonyOSの状態管理V1では、@State@Prop@Link@ObjectLinkなど、外部からのデータを受け入れるための多くのデコレータが存在しました。しかし、これらのデコレータはそれぞれに特定の制約があり、使い分けが複雑で、不適切な使用はパフォーマンス問題を引き起こす可能性がありました。例えば、@Stateは初期化時にのみ参照を得られ、変更が同期されませんでした。@Propは複雑な型の一方向同期においてディープコピーによるパフォーマンスオーバーヘッドがありました。@Link@ObjectLinkはデータソースが特定の状態変数である必要がありました。@Paramはこれらの課題を解決し、親子コンポーネント間での値渡しルールを簡素化することを目的としています。

デコレータの仕様

  • 引数: @Paramデコレータ自体は引数を取りません。
  • 内部での値変更: 変数自身の値は子コンポーネント内部で直接変更できません。値の変更が必要な場合は、@Eventデコレータと組み合わせて親コンポーネントに変更を通知する必要があります。ただし、オブジェクト型の変数については、その内部プロパティは変更可能です。
  • 同期方向: 親から子への一方向のみです。
  • サポートされる変数型: Objectclassstringnumberbooleanenumなどのプリミティブ型に加え、ArrayDateMapSetなどの組み込み型もサポートします。また、nullundefined、および共用体型にも対応しています。
  • 初期化: ローカルでの初期化を許可します。ローカルで初期化しない場合、@Requireデコレータと併用して外部からの初期化を必須とします。ローカル初期値と外部からの入力値が同時に存在する場合は、外部入力値が優先されます。

データ伝達ルール

  • 親コンポーネントからの初期化: @Paramで修飾された変数は、ローカルで初期化するか、または親コンポーネントから初期値を受け取る必要があります。
  • 子コンポーネントへの初期化: @Paramで修飾された変数は、さらにその下位の子コンポーネントの@Param変数を初期化することも可能です。
  • 同期動作: @Paramは、親コンポーネントから渡される@Localまたは別の@Paramで修飾された変数と同期します。親のデータソースが変更されると、その変更は子コンポーネントの@Param変数に自動的に反映されます。

データ変更の監視

@Paramデコレータは、データソースからの変更を型に応じて異なる方法で監視します。

プリミティブ型 (boolean, string, number)

データソースの変更が直接監視され、UIが更新されます。


@Entry
@ComponentV2
struct ParentComponent {
  @Local counterValue: number = 10;
  @Local statusMessage: string = "Initial Text";
  @Local toggleState: boolean = false;

  build() {
    Column() {
      Text(`親: カウンター ${this.counterValue}`)
      Text(`親: メッセージ ${this.statusMessage}`)
      Text(`親: ステータス ${this.toggleState}`)

      Button("親の状態を変更")
        .onClick(() => {
          this.counterValue++;
          this.statusMessage += " Changed";
          this.toggleState = !this.toggleState;
        })

      ChildComponent({
        count: this.counterValue,
        message: this.statusMessage,
        flag: this.toggleState
      })
    }
  }
}

@ComponentV2
struct ChildComponent {
  @Require @Param count: number;
  @Require @Param message: string;
  @Require @Param flag: boolean;

  build() {
    Column() {
      Text(`子: カウンター ${this.count}`)
      Text(`子: メッセージ ${this.message}`)
      Text(`子: ステータス ${this.flag}`)
    }
  }
}
  

カスタムオブジェクト

@Paramは、オブジェクト全体が新しいインスタンスに置き換えられた場合の変更を監視します。オブジェクトの内部プロパティへの変更を監視するには、@ObservedV2クラスと@Traceデコレータを使用する必要があります。


class UntrackedData {
  id: string;
  constructor(id: string) {
    this.id = id;
  }
}

@ObservedV2
class TrackedData {
  @Trace label: string;
  constructor(label: string) {
    this.label = label;
  }
}

@Entry
@ComponentV2
struct AppRoot {
  @Local untrackedItem: UntrackedData = new UntrackedData("Untracked A");
  @Local trackedItem: TrackedData = new TrackedData("Tracked B");

  build() {
    Column() {
      Text(`非追跡データ: ${this.untrackedItem.id}`)
      Text(`追跡データ: ${this.trackedItem.label}`)

      Button("オブジェクト全体を更新")
        .onClick(() => {
          // オブジェクト全体の置き換えは@Paramによって検出される
          this.untrackedItem = new UntrackedData("Untracked A New");
          this.trackedItem = new TrackedData("Tracked B New");
        })

      Button("プロパティを更新")
        .onClick(() => {
          // UntrackedDataのid変更は@Paramを通じては検出されない (オブジェクト全体の置換ではないため)
          this.untrackedItem.id = "Untracked A Property Change";
          // TrackedDataのlabel変更は@Traceにより検出される
          this.trackedItem.label = "Tracked B Property Change";
        })

      DataDisplayComponent({
        untracked: this.untrackedItem,
        tracked: this.trackedItem
      })
    }
  }
}

@ComponentV2
struct DataDisplayComponent {
  @Require @Param untracked: UntrackedData;
  @Require @Param tracked: TrackedData;

  build() {
    Column() {
      Text(`子コンポーネント: 非追跡 ${this.untracked.id}`)
      Text(`子コンポーネント: 追跡 ${this.tracked.label}`)
    }
  }
}
  

シンプルな配列型

配列全体が新しい配列に置き換えられた場合、または配列の項目が変更された場合に監視されます。


@Entry
@ComponentV2
struct ArrayParent {
  @Local numberSeries: number[] = [10, 20, 30];
  @Local gridValues: number[][] = [[1, 2], [3, 4]];

  build() {
    Column() {
      Text(`配列項目: ${this.numberSeries[0]}, ${this.numberSeries[1]}`)
      Text(`二次元配列項目: ${this.gridValues[0][0]}, ${this.gridValues[1][1]}`)

      Button("配列項目を変更")
        .onClick(() => {
          this.numberSeries[0] += 5;
          this.gridValues[0][0] = 99;
        })

      Button("配列全体を置換")
        .onClick(() => {
          this.numberSeries = [5, 4, 3];
          this.gridValues = [[0, 0], [1, 1]];
        })

      ArrayChild({
        numbers: this.numberSeries,
        grid: this.gridValues
      })
    }
  }
}

@ComponentV2
struct ArrayChild {
  @Require @Param numbers: number[];
  @Require @Param grid: number[][];

  build() {
    Column() {
      Text(`子配列項目: ${this.numbers[0]}, ${this.numbers[1]}`)
      Text(`子二次元配列項目: ${this.grid[0][0]}, ${this.grid[1][1]}`)
    }
  }
}
  

ネストされたクラスやオブジェクト配列

@Paramは、ネストされたオブジェクトの深い階層のプロパティ変更を直接は監視しません。このような深い変更を追跡するには、@ObservedV2クラスと@Traceデコレータを適切に適用する必要があります。


@ObservedV2
class Coordinate {
  @Trace lat: number;
  @Trace lon: number;
  constructor(lat: number, lon: number) {
    this.lat = lat;
    this.lon = lon;
  }
}

@ObservedV2
class LocationData {
  @Trace label: string;
  @Trace position: Coordinate;
  constructor(label: string, lat: number, lon: number) {
    this.label = label;
    this.position = new Coordinate(lat, lon);
  }
}

@Entry
@ComponentV2
struct LocationApp {
  @Local locationList: LocationData[] = [
    new LocationData("City A", 35, 139),
    new LocationData("Town B", 34, 135)
  ];
  @Local mainLocation: LocationData = new LocationData("Headquarters", 0, 0);

  build() {
    Column() {
      ForEach(this.locationList, (data: LocationData) => {
        Row() {
          Text(`リストアイテム: ${data.label}`)
          Text(`座標: ${data.position.lat}, ${data.position.lon}`)
        }
      })
      Row() {
        Text(`メイン: ${this.mainLocation.label}`)
        Text(`座標: ${this.mainLocation.position.lat}, ${this.mainLocation.position.lon}`)
      }

      Button("リスト内の項目を更新")
        .onClick(() => {
          // @Traceにより、リスト内のオブジェクトのプロパティ変更が検出される
          this.locationList[0].label = "New City Name";
        })
      Button("メインロケーション全体を置換")
        .onClick(() => {
          // @Localにより、オブジェクト全体の置換が検出される
          this.mainLocation = new LocationData("Global Office", 90, 180);
        })
      Button("メインロケーションの座標を更新")
        .onClick(() => {
          // @Traceにより、ネストされたオブジェクトのプロパティ変更が検出される
          this.mainLocation.position.lat = 40;
          this.mainLocation.position.lon = -74;
        })
    }
  }
}

@ComponentV2
struct LocationDisplay {
  @Param locationList: LocationData[] = [];
  @Param mainLocation: LocationData = new LocationData("Default", 0, 0);

  build() {
    Column() {
      ForEach(this.locationList, (data: LocationData) => {
        Row() {
          Text(`子リストアイテム: ${data.label}`)
          Text(`子座標: ${data.position.lat}, ${data.position.lon}`)
        }
      })
      Row() {
        Text(`子メイン: ${this.mainLocation.label}`)
        Text(`子座標: ${this.mainLocation.position.lat}, ${this.mainLocation.position.lon}`)
      }
    }
  }
}
  

組み込みコレクション型 (Array, Date, Map, Set)

  • Array: 配列全体が置換された場合、またはpush, pop, shift, unshift, spliceなどのAPIを介した変更が監視されます。
  • Date: Dateオブジェクト全体が置換された場合、またはsetFullYear, setMonth, setDateなどの変更用APIを介した変更が監視されます。
  • Map: Mapオブジェクト全体が置換された場合、またはset, clear, deleteなどのAPIを介した変更が監視されます。
  • Set: Setオブジェクト全体が置換された場合、またはadd, clear, deleteなどのAPIを介した変更が監視されます。

利用上の注意点

@Paramデコレータを利用する際には、以下の制限事項に留意してください。

  • @Paramは、@ComponentV2で修飾されたカスタムコンポーネント内でのみ使用可能です。
  • @Paramで修飾された変数は、コンポーネントの外部からの入力として機能するため、必ず初期化が必要です。ローカルで初期値を設定することも可能ですが、外部から値が渡された場合は、外部の値が優先されます。
  • 子コンポーネント内で@Param変数を直接変更することはできません(オブジェクト型の内部プロパティは変更可能です)。

適用シナリオ

@Paramデコレータは、以下のような状況で特に有用です。

親コンポーネントから子コンポーネントへのデータ伝達と同期

親コンポーネントの@Local変数や別の@Param変数をデータソースとして、階層的なコンポーネント構造全体でデータを効率的に伝達し、同期させることができます。


@ObservedV2
class Area {
  @Trace xCoord: number;
  @Trace yCoord: number;
  constructor(x: number, y: number) {
    this.xCoord = x;
    this.yCoord = y;
  }
}

@ObservedV2
class UserProfile {
  @Trace userName: string;
  @Trace userAge: number;
  @Trace userArea: Area;
  constructor(name: string, age: number, x: number, y: number) {
    this.userName = name;
    this.userAge = age;
    this.userArea = new Area(x, y);
  }
}

@Entry
@ComponentV2
struct UserAppRoot {
  @Local userProfiles: UserProfile[] = [
    new UserProfile("山田", 25, 10, 20),
    new UserProfile("田中", 30, 30, 40),
    new UserProfile("鈴木", 35, 50, 60)
  ];

  build() {
    Column() {
      ForEach(this.userProfiles, (profile: UserProfile) => {
        IntermediateComponent({ profileData: profile })
      })

      Button("ユーザーデータを更新")
        .onClick(() => {
          this.userProfiles[0] = new UserProfile("佐藤", 40, 70, 80);
          this.userProfiles[1].userName = "山本";
          this.userProfiles[2].userArea = new Area(90, 100);
        })
    }
  }
}

@ComponentV2
struct IntermediateComponent {
  @Require @Param profileData: UserProfile;

  build() {
    Column() {
      Text(`中間: 名前 ${this.profileData.userName}`)
      Text(`中間: 年齢 ${this.profileData.userAge}`)
      FinalComponent({ areaInfo: this.profileData.userArea })
    }
  }
}

@ComponentV2
struct FinalComponent {
  @Require @Param areaInfo: Area;

  build() {
    Column() {
      Text(`最終: 座標 ${this.areaInfo.xCoord}-${this.areaInfo.yCoord}`)
    }
  }
}
  

特定の組み込み型変数の受け渡し

DateMapSetなどの組み込み型オブジェクトを子コンポーネントに渡し、親からの変更を監視する場合に有効です。オブジェクト全体への代入や、各型の変更用APIの呼び出しを通じて変更が同期されます。

共用体型データのサポート

nullundefinedを含む共用体型の変数を@Paramで受け入れることができ、データが存在しない可能性のあるシナリオにも柔軟に対応できます。


@Entry
@ComponentV2
struct RootComponent {
  @Local optionalValue: number | undefined = 100;

  build() {
    Column() {
      Text(`親: オプション値 ${this.optionalValue ?? '未定義'}`)
      Button("値を切り替え")
        .onClick(() => {
          this.optionalValue = this.optionalValue === undefined ? 200 : undefined;
        })
      OptionalDisplayComponent({ data: this.optionalValue })
    }
  }
}

@ComponentV2
struct OptionalDisplayComponent {
  @Param data: number | undefined;

  build() {
    Column() {
      Text(`子: 表示データ ${this.data ?? '子で未定義'}`)
    }
  }
}
  

タグ: HarmonyOS ArkUI StateManagementV2 ComponentV2 decorator

7月5日 17:00 投稿