HarmonyOS NextにおけるページレベルストレージLocalStoreの完全ガイド

はじめに

LocalStoreはArkTSが提供する、ページレベルの状態変数を保持するためのメモリ内「データベース」です。アプリケーションは複数のLocalStoreインスタンスを作成でき、UIAbilityインスタンス内で複数のページ間で状態を共有できます。ページ内でも共有可能であり、GetSharedインターフェースを通じて異なるページ間での共有も可能です。コンポーネントツリーのルートノード(@Entryデコレータ付きの@Component)にLocalStoreインスタンスを割り当てると、そのすべての子コンポーネントが自動的にアクセス権限を得ます。LocalStore内のプロパティはすべて変更可能であり、そのライフサイクルはアプリケーションによって管理されます。

制限事項

  1. パラメータ型要件:@LocalStorePropおよび@LocalStoreLinkのパラメータは文字列型である必要があります。そうでない場合、コンパイル時にエラーが発生します。例:
let store = new LocalStore();
store.setOrCreate('PropA', 48);
// エラー:コンパイルエラー
@LocalStoreProp() localStoreProp: number = 1;
@LocalStoreLink() localStoreLink: number = 2;
// 正しい記述
@LocalStoreProp('PropA') localStoreProp: number = 1;
@LocalStoreLink('PropA') localStoreLink: number = 2;

  1. Function型のサポート不可:@StorePropと@StoreLinkはFunction型の変数をデコレートできません。フレームワークは実行時エラーをスローします。
  2. プロパティ型の変更不可:LocalStoreが作成された後、名前付きプロパティの型は変更できません。後続のset呼び出しでは同じ型の値を使用する必要があります。
  3. ページレベルストレージの制限:getSharedインターフェースは現在のStageからwindowStage.loadContentで渡されたLocalStoreインスタンスのみを取得でき、それ以外の場合はundefinedを返します。

@LocalStorePropデコレータ

(1)デコレータの使用ルール

  1. パラメータ要件
  • keyは定数文字列であり、必須で引用符で囲む必要があります。
  • デコレート可能な変数型にはObject、class、string、number、boolean、enumおよびその配列が含まれます。API12以降ではMap、Set、Date型もサポートされ、anyはサポートされていません。API12以降ではundefinedとnullもサポートされますが、明示的に型を指定することを推奨します。例:
@LocalStoreProp("AA") a: number | null = null; // 推奨
@LocalStoreProp("AA") a: number = null; // 非推奨

  1. 同期タイプ:LocalStoreのkeyに対応するプロパティとの単方向データ同期を確立します。LocalStoreからコンポーネント状態変数への同期のみが可能です。つまり、ArkUIフレームワークは@LocalStoreProp(key)のローカル値を変更できますが、ローカル値の変更はLocalStoreに戻りません。一方、LocalStoreのkeyに対応するプロパティが変更されると、@LocalStoreProp(key)に同期され、ローカル値が上書きされます。
  2. 初期値要件:必須です。LocalStoreインスタンスにプロパティが存在しない場合、この初期値で初期化してLocalStoreに保存されます。

(2)変数の伝達/アクセスルール

  1. 親ノードからの初期化・更新禁止:LocalStoreのkeyに対応するプロパティから初期化される必要があります。対応するkeyがない場合はローカルのデフォルト値が使用されます。
  2. 子ノードの初期化サポート:@State、@Link、@Prop、@Provideの初期化に使用できます。
  3. コンポーネント外からのアクセス不可

(3)変更監視と動作表現

  1. 変更監視タイプ
  • boolean、string、number型は値の変更を監視できます。
  • classまたはObject型はオブジェクト全体の代入とプロパティ変更を監視できます。
  • array型は要素の追加、削除、更新を監視できます。
  • Date型は全体の代入と関連インターフェースによるプロパティ更新を監視できます。
  • Map型は全体の代入とインターフェースによる値更新を監視できます。
  • Set型は全体の代入とインターフェースによる値更新を監視できます。
  1. フレームワーク動作
  • コンポーネント内の変数値の変更はLocalStoreに書き戻されません。
  • 変数の変更により関連コンポーネントが再描画されます。
  • LocalStoreの値の変更はローカルの変更を上書きします。

@LocalStoreLinkデコレータ

(1)デコレータの使用ルール

  1. パラメータ要件:@LocalStorePropと同じです。keyは定数文字列で、必須で引用符で囲み、変数型の要件も同じです。
  2. 同期タイプ:LocalStoreのkeyに対応するプロパティとの双方向データ同期を確立します。つまり、ローカルでの変更はLocalStoreに書き戻され、LocalStoreの変更もバインドされたプロパティに同期されます(単方向および双方向バインド変数を含む)。
  3. 初期値要件:必須です。LocalStoreインスタンスにプロパティが存在しない場合、この初期値で初期化してLocalStoreに保存されます。

(2)変数の伝達/アクセスルール

  1. 親ノードからの初期化・更新禁止:@LocalStorePropと同じです。
  2. 子ノードの初期化サポート:@LocalStorePropと同じです。
  3. コンポーネント外からのアクセス不可

(3)変更監視と動作表現

  1. 変更監視タイプ:@LocalStorePropと同じです。
  2. フレームワーク動作
  • コンポーネント内の値の変更はLocalStoreに同期されます。
  • LocalStoreの値の変更により、バインドされたデータ(単方向および双方向)が同期変更されます。
  • デコレートされたデータ自体が状態変数の場合、変更により所属するカスタムコンポーネントが再レンダリングされます。

利用シナリオ

(1)アプリケーションロジックにおけるLocalStoreの使用

LocalStoreインスタンスを作成し、get、link、propなどのインターフェースを使用してプロパティを操作します。例:

let para: Record<string,number> = { 'PropA': 47 };
let store: LocalStore = new LocalStore( para);
let propA: number | undefined = store.get('PropA');
let link1: SubscribedAbstractProperty<number> = store.link('PropA');
let link2: SubscribedAbstractProperty<number> = store.link('PropA');
let prop: SubscribedAbstractProperty<number> = store.prop('PropA');
link1.set(48);
prop.set(1);
link1.set(49);

(2)UI内部からのLocalStoreの使用

@LocalStorePropおよび@LocalStoreLinkを使用してUIコンポーネント内で状態変数を取得します。例:

class PropB {
 code: number;
 constructor(code: number) {
   this.code = code;
 }
}
let para: Record<string, number> = { 'PropA': 47 };
let store: LocalStore = new LocalStore( para);
store.setOrCreate('PropB', new PropB(50));
@Component
struct Child {
 @LocalStoreLink('PropA') childLinkNumber: number = 1;
 @LocalStoreLink('PropB') childLinkObject: PropB = new PropB(0);
 build() {
   Column() {
     Button(`Child from LocalStore ${this.childLinkNumber}`).onClick(() => {this.childLinkNumber += 1;})
     Button(`Child from LocalStore ${this.childLinkObject.code}`).onClick(() => {this.childLinkObject.code += 1;})
   }
 }
}
@Entry(store)
@Component
struct CompA {
 @LocalStoreLink('PropA') parentLinkNumber: number = 1;
 @LocalStoreLink('PropB') parentLinkObject: PropB = new PropB(0);
 build() {
   Column({ space: 15 }) {
     Button(`Parent from LocalStore ${this.parentLinkNumber}`).onClick(() => {this.parentLinkNumber += 1;})
     Button(`Parent from LocalStore ${this.parentLinkObject.code}`).onClick(() => {this.parentLinkObject.code += 1;})
     Child()
   }
 }
}

(3)@LocalStorePropとLocalStoreの単方向同期シナリオ

CompAコンポーネントとChildコンポーネントでstorageの'PropA'との単方向同期データを作成します。例:

let para: Record<string, number> = { 'PropA': 47 };
let store: LocalStore = new LocalStore( para);
@Entry(store)
@Component
struct CompA {
 @LocalStoreProp('PropA') storageProp1: number = 1;
 build() {
   Column({ space: 15 }) {
     Button(`Parent from LocalStore ${this.storageProp1}`).onClick(() => {this.storageProp1 += 1})
     Child()
   }
 }
}
@Component
struct Child {
 @LocalStoreProp('PropA') storageProp2: number = 2;
 build() {
   Column({ space: 15 }) {
     Text(`Parent from LocalStore ${this.storageProp2}`)
   }
 }
}

(4)@LocalStoreLinkとLocalStoreの双方向同期シナリオ

例:

let para: Record<string, number> = { 'PropA': 47 };
let store: LocalStore = new LocalStore( para);
let linkToPropA: SubscribedAbstractProperty<object> = store.link('PropA');
@Entry(store)
@Component
struct CompA {
 @LocalStoreLink('PropA') storageLink: number = 1;
 build() {
   Column() {
     Text(`incr @LocalStoreLink variable`).onClick(() => {this.storageLink += 1})
     Text(`@LocalStoreLink: ${this.storageLink} - linkToPropA: ${linkToPropA.get()}`)
   }
 }
}

(5)兄弟コンポーネント間での状態変数の同期

@LocalStoreLinkを使って兄弟コンポーネントの状態を同期します。例:

let ls: Record<string, number> = { 'countStorage': 1 }
let store: LocalStore = new LocalStore(ls);
@Component
struct Child {
 label: string = 'no name';
 @LocalStoreLink('countStorage') playCountLink: number = 0;
 build() {
   Row() {
     Text(this.label).width(50).height(60).fontSize(12)
     Text(`playCountLink ${this.playCountLink}: inc by 1`).onClick(() => {this.playCountLink += 1;})
   }.width(300).height(60)
 }
}
@Entry(store)
@Component
struct Parent {
 @LocalStoreLink('countStorage') playCount: number = 0;
 build() {
   Column() {
     Row() {
       Text('Parent').width(50).height(60).fontSize(12)
       Text(`playCount ${this.playCount} dec by 1`).onClick(() => {this.playCount -= 1;})
     }.width(300).height(60)
     Row() {
       Text('LocalStore').width(50).height(60).fontSize(12)
       Text(`countStorage ${this.playCount} incr by 1`).onClick(() => {store.set<number | undefined>('countStorage', Number(store.get<number>('countStorage')) + 1);})
     }.width(300).height(60)
     Child({ label: 'ChildA' })
     Child({ label: 'ChildB' })
     Text(`playCount in LocalStore for debug ${store.get<number>('countStorage')}`).width(300).height(60).fontSize(12)
   }
 }
}

(6)UIAbilityから複数のビューへLocalStoreインスタンスを共有

UIAbility内でLocalStoreインスタンスを作成し、windowStage.loadContentで共有します。ページではgetSharedで取得します。例:

  1. EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
para:Record<string, number> = { 'PropA': 47 };
store: LocalStore = new LocalStore(this.para);
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', this.store);
}
}

  1. index.ets
import { router } from '@kit.ArkUI';
let store = LocalStore.getShared()
@Entry(store)
@Component
struct Index {
 @LocalStoreLink('PropA') propA: number = 1;
 build() {
   Row() {
     Column() {
       Text(`${this.propA}`).fontSize(50).fontWeight(FontWeight.Bold)
       Button("To Page").onClick(() => {
         this.getUIContext().getRouter().pushUrl({
           url: 'pages/Page'
         })
       })
     }.width('100%')
   }.height('100%')
 }
}

  1. Page.ets
import { router } from '@kit.ArkUI';
let store = LocalStore.getShared()
@Entry(store)
@Component
struct Page {
 @LocalStoreLink('PropA') propA: number = 2;
 build() {
   Row() {
     Column() {
       Text(`${this.propA}`).fontSize(50).fontWeight(FontWeight.Bold)
       Button("Change propA").onClick(() => {this.propA = 100;})
       Button("Back Index").onClick(() => {this.getUIContext().getRouter().back()})
     }.width('100%')
   }
 }
}

(7)カスタムコンポーネントへのLocalStoreインスタンスの受け渡し

  1. 属性定義時の受け取り
  • インスタンスは2番目の引数として渡す必要があり、それ以外の場合はエラーになります。
  • 例:
let localStore1: LocalStore = new LocalStore();
localStore1.setOrCreate('PropA', 'PropA');
let localStore2: LocalStore = new LocalStore();
localStore2.setOrCreate('PropB', 'PropB');
@Entry(localStore1)
@Component
struct Index {
 @LocalStoreLink('PropA') PropA: string = 'Hello World';
 @State count: number = 0;
 build() {
   Row() {
     Column() {
       Text(this.PropA).fontSize(50).fontWeight(FontWeight.Bold)
       Child({ count: this.count }, localStore2)
     }.width('100%')
   }.height('100%')
 }
}
@Component
struct Child {
 @Link count: number;
 @LocalStoreLink('PropB') PropB: string = 'Hello World';
 build() {
   Text(this.PropB).fontSize(50).fontWeight(FontWeight.Bold)
 }
}

  1. 属性未定義時の受け取り:LocalStoreインスタンスを1つだけ引数として渡すことができます。例:
let localStore1: LocalStore = new LocalStore();
localStore1.setOrCreate('PropA', 'PropA');
let localStore2: LocalStore = new LocalStore();
localStore2.setOrCreate('PropB', 'PropB');
@Entry(localStore1)
@Component
struct Index {
 @LocalStoreLink('PropA') PropA: string = 'Hello World';
 @State count: number = 0;
 build() {
   Row() {
     Column() {
       Text(this.PropA).fontSize(50).fontWeight(FontWeight.Bold)
       Child(localStore2)
     }.width('100%')
   }.height('100%')
 }
}
@Component
struct Child {
 build() {
   Text("hello").fontSize(50).fontWeight(FontWeight.Bold)
 }
}

  1. 親コンポーネントからの初期化不要な属性の受け取り:最初の引数に{}を渡します。例:
let localStore1: LocalStore = new LocalStore();
localStore1.setOrCreate('PropA', 'PropA');
let localStore2: LocalStore = new LocalStore();
localStore2.setOrCreate('PropB', 'PropB');
@Entry(localStore1)
@Component
struct Index {
 @LocalStoreLink('PropA') PropA: string = 'Hello World';
 @State count: number = 0;
 build() {
   Row() {
     Column() {
       Text(this.PropA).fontSize(50).fontWeight(FontWeight.Bold)
       Child({}, localStore2)
     }.width('100%')
   }.height('100%')
 }
}
@Component
struct Child {
 @State count: number = 5;
 @LocalStoreLink('PropB') PropB: string = 'Hello World';
 build() {
   Text(this.PropB).fontSize(50).fontWeight(FontWeight.Bold)
 }
}

(8)NavigationコンポーネントとLocalStoreの連携

異なるLocalStoreインスタンスをカスタムコンポーネントに渡し、ナビゲーション遷移時にそれぞれのバインド値を表示します。例:

let localStoreA: LocalStore = new LocalStore();
localStoreA.setOrCreate('PropA', 'PropA');
let localStoreB: LocalStore = new LocalStore();
localStoreB.setOrCreate('PropB', 'PropB');
let localStoreC: LocalStore = new LocalStore();
localStoreC.setOrCreate('PropC', 'PropC');
@Entry
@Component
struct MyNavigationTestStack {
 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
 @Builder
 PageMap(name: string) {
   if (name === 'pageOne') {
     pageOneStack({}, localStoreA)
   } else if (name === 'pageTwo') {
     pageTwoStack({}, localStoreB)
   } else if (name === 'pageThree') {
     pageThreeStack({}, localStoreC)
   }
 }
 build() {
   Column({ space: 5 }) {
     Navigation(this.pageInfo) {
       Column() {
         Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }).width('80%').height(40).margin(20).onClick(() => {
           this.pageInfo.pushPath({ name: 'pageOne' });
         })
       }
     }.title('NavIndex').navDestination(this.PageMap).mode(NavigationMode.Stack).borderWidth(1)
   }
 }
}
@Component
struct pageOneStack {
 @Consume('pageInfo') pageInfo: NavPathStack;
 @LocalStoreLink('PropA') PropA: string = 'Hello World';
 build() {
   NavDestination() {
     Column() {
       NavigationContentMsgStack()
       Text(`${this.PropA}`)
       Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }).width('80%').height(40).margin(20).onClick(() => {
         this.pageInfo.pushPathByName('pageTwo', null);
       })
     }.width('100%').height('100%')
   }.title('pageOne').onBackPressed(() => {this.pageInfo.pop(); return true;})
 }
}

まとめ

HarmonyOS Nextにおいて、ページレベルストレージLocalStoreは、開発者がページレベルの状態変数を管理するための便利な方法を提供します。@LocalStorePropおよび@LocalStoreLinkデコレータを使用することで、単方向および双方向のデータ同期を実現し、さまざまなシナリオに対応できます。単一ページ内でも複数ページ間でも、LocalStoreは効果的に状態を共有できるため、アプリケーションの開発効率とユーザーエクスペリエンスを向上させます。同時に、LocalStoreの制限事項を理解し、状態変数の正しく使用・管理することが重要です。

拡張情報

  1. 実際のアプリケーションでは、他のHarmonyOS Nextの機能や特性(コンポーネント化開発、状態管理フレームワークなど)と組み合わせることで、アプリケーションのアーキテクチャとパフォーマンスをさらに最適化できます。
  2. 大規模アプリケーションでは、ReduxやVuexのようなより複雑な状態管理スキームを検討し、グローバル状態をより効果的に管理できます。
  3. LocalStoreを使用する際は、データのセキュリティとプライバシーに注意が必要です。機密情報を保存しないようにし、暗号化技術などを用いてデータ保護を行うことを推奨します。
  4. 新しい使い方やテクニックを積極的に探求・試行することで、開発効率とコード品質を高めることができます。たとえば、特定のモジュールの状態を管理するためにカスタムLocalStoreインスタンスを使用したり、アニメーション効果と組み合わせてユーザーエクスペリエンスを強化したりできます。

タグ: HarmonyOS ArkTS localstorage state management UI Development

7月27日 19:05 投稿