HarmonyOS Nextの状態管理におけるベストプラクティス

HarmonyOS Nextアプリ開発において、適切な状態管理はアプリのパフォーマンスと応答性を確保する鍵となります。以下に、ベストプラクティスに基づいた詳細な解説を示します。各プラクティスには、反例の分析と正例の改善が含まれており、対応するコード例も提供します。

  1. @ObjectLinkを使用して不要な深いコピーを削減する 問題シナリオ 親子コンポーネント間のデータ受け渡しにおいて、子コンポーネントが受け取ったデータを変更する必要がない場合、@Propデコレータを使用すると不要な深いコピーが発生し、パフォーマンスに影響を与える可能性があります。

反例分析 以下のコードは、親コンポーネントProductListと子コンポーネントProductItem間のデータ受け渡しを示しています。親コンポーネントには@Stateで修飾されたproductData配列があり、ProductItemproductDataを@Propで受け取ります。

// 反例
@Observed
class ProductInfo {
  public price: number = 0;
  constructor(price: number) {
    this.price = price;
  }
}

@Component
struct ProductItem {
  @Prop productData: ProductInfo; // @Propはデータを深くコピーする
  build() {
    Text(`ProductItem price: ${this.productData.price}`)
  }
}

@Entry
@Component
struct ProductList {
  @State productData: ProductInfo[] = [new ProductInfo(100)];
  build() {
    Column() {
      Text(`ProductList price: ${this.productData[0].price}`)
      .onClick(() => {
          this.productData[0].price += 10;
        })
      // ProductItemは@Prop productData: ProductInfoの値を変更しないが、@Propはデータを深くコピーするため、パフォーマンスに影響がある
      ProductItem({ productData: this.productData[0] })
    }
  }
}

正例改善 ProductItem@Prop@ObjectLinkに変更することで、深いコピーを回避し、パフォーマンスを向上させます。

// 正例
@Observed
class ProductInfo {
  public price: number = 0;
  constructor(price: number) {
    this.price = price;
  }
}

@Component
struct ProductItem {
  @ObjectLink productData: ProductInfo; // @ObjectLinkはデータを深くコピーしない
  build() {
    Text(`ProductItem price: ${this.productData.price}`)
  }
}

@Entry
@Component
struct ProductList {
  @State productData: ProductInfo[] = [new ProductInfo(100)];
  build() {
    Column() {
      Text(`ProductList price: ${this.productData[0].price}`)
      .onClick(() => {
          this.productData[0].price += 10;
        })
      // 子コンポーネントがデータを変更する必要がない場合、@ObjectLinkを使用するとパフォーマンスが向上する
      ProductItem({ productData: this.productData[0] })
    }
  }
}
  1. 状態変数ではない変数を強制的に更新しない 問題シナリオ 開発者は、カスタムUI状態変数を使用して、状態変数としてデコレートされていない通常の変数を更新すべきではありません。なぜなら、ArkUIでは、UIの更新はフレームワークによって状態変数の変更を自動的に検出して実行されるからです。

反例分析 DataDisplayコンポーネント内で、itemsbgColorは状態変数としてデコレートされていません。これらの値を変更してもUIの更新はトリガーされず、代わりにtriggerRefresh状態変数を用いて更新を誘発するのは、不合理でパフォーマンスが悪い方法です。

// 反例
@Entry
@Component
struct DataDisplay {
  @State triggerRefresh: boolean = true;
  items: Array<number> = [4, 1, 3, 2]; // 状態変数デコレータを使用していない
  bgColor: Color = Color.Yellow;
  updateUIArr(param: Array<number>): Array<number> {
    const triggerAGet = this.triggerRefresh;
    return param;
  }
  updateUI(param: Color): Color {
    const triggerAGet = this.triggerRefresh;
    return param;
  }
  build() {
    Column({ space: 20 }) {
      ForEach(this.updateUIArr(this.items),
        (item: Array<number>) => {
          Text(`${item}`)
        })
      Text("アイテムを追加")
      .onClick(() => {
          // itemsを変更してもUIビューは更新されない
          this.items.push(this.items[this.items.length - 1] + 1);
          // UIビューを更新するために
          this.triggerRefresh = !this.triggerRefresh;
        })
      Text("色を変更")
      .onClick(() => {
          // bgColorを変更してもUIビューは更新されない
          this.bgColor = this.bgColor == Color.Yellow? Color.Red : Color.Yellow;
          // UIビューを更新するために
          this.triggerRefresh = !this.triggerRefresh;
        })
    }.backgroundColor(this.updateUI(this.bgColor))
    .width(200).height(500)
  }
}

正例改善 itemsbgColorを@Stateでデコレートし、状態変数にすることで、値の変更が直接UIの更新をトリガーするようにします。

// 正例
@Entry
@Component
struct CompA {
  @State items: Array<number> = [4, 1, 3, 2];
  @State bgColor: Color = Color.Yellow;
  build() {
    Column({ space: 20 }) {
      ForEach(this.items,
        (item: Array<number>) => {
          Text(`${item}`)
        })
      Text("アイテムを追加")
      .onClick(() => {
          // itemsを変更するとUIビューが更新される
          this.items.push(this.items[this.items.length - 1] + 1);
        })
      Text("色を変更")
      .onClick(() => {
          // bgColorを変更するとUIビューが更新される
          this.bgColor = this.bgColor == Color.Yellow? Color.Red : Color.Yellow;
        })
    }.backgroundColor(this.bgColor)
    .width(200).height(500)
  }
}
  1. 状態変数に関連するコンポーネントの数を正確に制御する 問題シナリオ 同じ状態変数を複数の同レベルのコンポーネントのプロパティにバインドすると、状態変数が変更された際に、すべての関連コンポーネントがリフレッシュされます。これらのコンポーネントの変更が同じであっても、不要なコンポーネントのリフレッシュが発生し、パフォーマンスに影響を与える可能性があります。状態変数を親コンポーネントにバインドすることで、リフレッシュが必要なコンポーネントの数を減らし、パフォーマンスを向上させることができます。

反例分析 MainPageコンポーネント内で、positionDataxOffsetプロパティは、複数の同レベルの子コンポーネント(Header内のImageTextStackButton)にバインドされています。xOffsetが変化すると、これらすべてのコンポーネントがリフレッシュされます。

// 反例
@Observed
class Position {
  xOffset: number = 20;
}

@Component
struct Header {
  @ObjectLink positionData: Position;
  build() {
    Row() {
      // ここでは'app.media.icon'は例示のため、開発者が独自に置き換える必要があります。そうしないと、imageSourceの作成に失敗し、後続の処理が正常に実行されません。
      Image($r('app.media.icon'))
      .width(50)
      .height(50)
      .translate({
          x: this.positionData.xOffset // this.positionData.xOffsetはImageとTextコンポーネントにバインドされている
        })
      Text("ヘッダー")
      .fontSize(20)
      .translate({
          x: this.positionData.xOffset
        })
    }
  }
}

@Entry
@Component
struct MainPage {
  @State positionData: Position = new Position();
  build() {
    Column() {
      Header({
        positionData: this.positionData
      })
      Stack() {
      }
      .backgroundColor("black")
      .width(200)
      .height(400)
      .translate({
          x: this.positionData.xOffset // this.positionData.xOffsetはStackとButtonコンポーネントにバインドされている
        })
      Button("移動")
      .translate({
          x: this.positionData.xOffset
        })
      .onClick(() => {
          animateTo({
            duration: 50
          }, () => {
            this.positionData.xOffset = (this.positionData.xOffset + 50) % 150
          })
        })
    }
  }
}

正例改善 子コンポーネントに共通するtranslateプロパティを親コンポーネントColumnに統一設定することで、状態変数に関連するコンポーネントの数を減らします。

// 正例
@Observed
class Position {
  xOffset: number = 20;
}

@Component
struct Header {
  build() {
    Row() {
      // ここでは'app.media.icon'は例示のため、開発者が独自に置き換える必要があります。そうしないと、imageSourceの作成に失敗し、後続の処理が正常に実行されません。
      Image($r('app.media.icon'))
      .width(50)
      .height(50)
      Text("ヘッダー")
      .fontSize(20)
    }
  }
}

@Entry
@Component
struct OptimizedPage {
  @State positionData: Position = new Position();
  build() {
    Column() {
      Header()
      Stack() {
      }
      .backgroundColor("black")
      .width(200)
      .height(400)
      Button("移動")
      .onClick(() => {
          animateTo({
            duration: 50
          }, () => {
            this.positionData.xOffset = (this.positionData.xOffset + 50) % 150
          })
        })
    }
    .translate({ // 子コンポーネントStackとButtonは同じtranslateプロパティを持つため、Columnに統一して設定する
        x: this.positionData.xOffset
      })
  }
}
  1. オブジェクト型の状態変数に関連するコンポーネントの数を合理的に制御する 問題シナリオ 複雑なオブジェクトが状態変数として定義されている場合、そのメンバープロパティの変更は、関連するすべてのコンポーネントをリフレッシュさせます。一部のコンポーネントがその変更されたプロパティを使用していない場合でも、「冗長なリフレッシュ」が発生し、パフォーマンスに影響を与える可能性があります。

解決方法 複雑なオブジェクトを合理的に分割し、関連するコンポーネントの数を制御することで、不要なコンポーネントのリフレッシュを回避します。具体的な方法については、関連記事(例えば、ドキュメントで言及されているコンポーネントの更新範囲の正確な制御や状態管理の適切な使用に関する開発ガイド)を参照してください。

  1. 状態変数に関連するコンポーネントの数を確認する 操作方法 アプリ開発では、HiDumperを使用して状態変数に関連するコンポーネントの数を確認し、パフォーマンスの最適化を行うことができます。具体的な操作方法については、状態変数コンポーネントの位置特定ツールの実践を参照してください。

  2. for、whileなどのループロジックで頻繁に状態変数を読み取らない 問題シナリオ ループロジック内で頻繁に状態変数を読み取ると、パフォーマンスに影響を与える可能性があります。なぜなら、各読み取り操作が関連する更新メカニズムをトリガーする可能性があるからです。

反例分析 LogViewerコンポーネント内のonClickイベントのforループ内で、毎回@State logMessage状態変数を読み取っているため、パフォーマンスに影響します。

// 反例
import hilog from '@ohos.hilog';

@Entry
@Component
struct LogViewer {
  @State logMessage: string = '';
  build() {
    Column() {
      Button('ログを印刷')
      .onClick(() => {
          for (let i = 0; i < 10; i++) {
            hilog.info(0x0000, 'TAG', '%{public}s', this.logMessage);
          }
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}

正例改善 ループの外で状態変数を一時変数に読み取り、ループ内で一時変数を使用することで、状態変数の読み取り回数を減らし、パフォーマンスを向上させます。

// 正例
import hilog from '@ohos.hilog';

@Entry
@Component
struct LogViewer {
  @State logMessage: string = '';
  build() {
    Column() {
      Button('ログを印刷')
      .onClick(() => {
          let tempMessage: string = this.logMessage;
          for (let i = 0; i < 10; i++) {
            hilog.info(0x0000, 'TAG', '%{public}s', tempMessage);
          }
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}
  1. 一時変数で状態変数を置き換えることを推奨する 問題シナリオ 状態変数に直接値を代入すると、ArkUIのクエリとレンダリング動作が複数回トリガーされる可能性があります。なぜなら、各代入操作が状態変数の変更として扱われるからです。これにより、ArkUIの不必要な動作が増え、パフォーマンスが低下します。

反例分析 MessageBuilderコンポーネントのaddTextメソッドで、@State displayText状態変数を直接操作すると、計算関数が複数回トリガーされ、ArkUIの不必要なクエリとレンダリングが増加し、パフォーマンスが悪化します。

// 反例
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct MessageBuilder {
  @State displayText: string = '';
  addText(newText: string) {
    // パフォーマンス計測
    hiTraceMeter.startTrace('StateVariable', 1);
    this.displayText += newText;
    this.displayText += ';';
    this.displayText += '<br/>';
    hiTraceMeter.finishTrace('StateVariable', 1);
  }
  build() {
    Column() {
      Button('ログを印刷')
      .onClick(() => {
          this.addText('状態変数を操作');
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}

正例改善 データ計算に一時変数を使用し、最後に計算結果を状態変数に代入することで、ArkUIの不必要な動作を減らし、パフォーマンスを向上させます。

// 正例
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct MessageBuilder {
  @State displayText: string = '';
  addText(newText: string) {
    // パフォーマンス計測
    hiTraceMeter.startTrace('TemporaryVariable', 2);
    let tempText = this.displayText;
    tempText += newText;
    tempText += ';';
    tempText += '<br/>';
    this.displayText = tempText;
    hiTraceMeter.finishTrace('TemporaryVariable', 2);
  }
  build() {
    Column() {
      Button('ログを印刷')
      .onClick(() => {
          this.addText('一時変数を操作');
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}

これらの状態管理のベストプラクティスに従うことで、HarmonyOS Next開発者はアプリのパフォーマンスを最適化し、ユーザーエクスペリエンスを向上させ、アプリがあらゆるシナリオで効率的に動作することを確保できます。

タグ: HarmonyOS Next ArkUI 状態管理 @ObjectLink @Prop

8月8日 01:58 投稿