鸿蒙6.0应用开发——基于StateStore的全局状态管理

概述

使用ArkUI开发页面时,多组件状态共享是我们经常会遇到的场景;ArkUI通过装饰器,例如@State+@Prop/@Link、@Provide+@Consume实现父子组件状态共享,但是这样会造成状态数据耦合。

作为ArkUI状态与UI解耦的解决方案,支持全局维护状态,优雅地解决状态共享的问题。让开发者在开发过程中实现状态与UI解耦,多个组件可以方便地共享和更新全局状态,将状态管理逻辑从组件逻辑中分离出来,简化维护。

StateStore提供了下列功能特性:

  • 状态对象与UI解耦,支持状态全局化操作。
  • 支持在子线程中进行状态对象更新。
  • 支持状态更新执行的预处理和后处理。

ArkUI状态管理现状

在多组件状态共享的场景中,我们常常遇到的问题是,当多个组件需要共享相同的状态时,必须通过它们的共同父组件来传递和维护这些状态数据。

例如,我们实现如下图待办列表的新增与删除功能。

图1 待办列表效果图

在这里插入图片描述

新增和删除功能按钮分别位于两个兄弟组件中。在开发时,父组件需要维护一个listDatas列表,并通过@Link装饰器实现数据的双向同步,从而实现兄弟组件之间的状态同步。删除功能和新增功能逻辑分别由两个子组件处理,但是这两个组件都需要引入与UI渲染无关的listDatas数据,造成了状态与UI的高耦合。使得状态管理变得复杂,难以维护和扩展。组件结构图如下:

在这里插入图片描述

引入StateStore库后,开发者可以将listDatas数据存储在全局仓库(Store)中,组件从Store中获取数据进行UI渲染,并通过向Store发送事件来更新数据。这样,状态更新逻辑被集中管理,组件无需额外引入状态进行逻辑处理,从而实现了状态与UI的低耦合。组件结构图如下:

在这里插入图片描述

实现原理

StateStore基于ArkUI的状态管理特性(@Observed@ObservedV2)实现全局状态管理。统一由UI分发事件指令,状态管理仓库触发对应的状态更新逻辑,实现状态与UI解耦。

具体步骤如下

  1. @ObservedV2装饰类实现数据监听
  2. Store集中管理被观察对象
  3. 状态变更通过装饰器触发UI更新

图2 运行原理图

在这里插入图片描述

核心概念解释

  • View:视图层

    View构成了用户界面,它包含了丰富的页面UI组件,并响应用户操作。当用户和UI进行交互时,View会通过dispatch方法分发Action事件,从而触发状态更新的流程。

  • Store:状态管理仓库

    Store作为状态管理的核心,主要向外部提供两个关键方法:getState()和dispatch(action)。getState()方法允许外部获取当前的状态信息,而dispatch(action)方法则用于接收并处理来自UI的Action事件。

  • Reducer:状态刷新逻辑处理函数

    Reducer是一个专门负责状态刷新逻辑的函数。它会根据传入的Action事件指令,对状态进行更新。每一个Action事件都携带着特定的指令,Reducer会根据这些指令来精确地修改状态。

  • Dispatch:事件分发方法

    Dispatch是UI侧与Store进行交互的桥梁,也是UI侧触发状态更新的唯一途径。UI侧通过调用Dispatch方法,将封装了事件类型的Action对象发送到Store,从而触发后续的状态更新流程。

  • Action:事件描述对象

    Action是一个用于描述指示发生了何种事件的对象。它包含了两个重要的属性:type和payload。type属性用于标识事件的类型,而payload属性则携带了事件相关的具体数据。通过这两个属性,Action能够完整地描述一个事件,并引导Reducer进行状态更新。

UI刷新原理

数据改变刷新UI的能力依赖系统侧@Observed/@ObservedV2对数据的观测能力,StateStore不接管数据驱动UI更新。

说明

开发步骤

  1. 定义业务数据

    开发者使用@Observed或@ObservedV2修饰业务数据,并生成实例对象。

  2. 定义状态更新逻辑函数

    开发者需要定义状态处理函数,该函数类型为Reducer。该函数负责根据业务逻辑来更新数据。

  3. 创建状态管理仓库

    为了集中管理状态更新,开发者使用StateStore.createStore方法来创建一个状态管理仓库(即Store对象)。在调用createStore方法时,需要传入业务数据的对象实例和业务逻辑函数,以便为Store对象绑定相应的初始状态Reducer

  4. 组件UI的初始渲染

    在组件内部,开发者通过调用Store对象的getState()方法来获取业务数据,并据此编写UI结构。这样,组件即可根据获取到的数据进行渲染。

  5. 组件UI的刷新

    1. 创建Action事件对象

      这一步的目的是告诉store对象,你要做什么;例如,我们需要添加某个数据,则创建一个添加事件——AddAction。

    2. UI触发事件

      事件定义好后,需要某个操作来触发事件,即我们在UI中通过dispatch(AddAction)发送该添加事件给store,接收到事件后会通知实际处理者——Reducer,根据接收到的AddAction事件处理对应的添加逻辑,修改状态数据。

    3. UI刷新

      Reducer类型函数的逻辑被触发后,状态会随之更新。借助系统提供的@Observed或@ObservedV2装饰器的监听能力,UI能够与状态保持同步刷新。

使用StateStore实现状态与UI解耦

场景描述

在开发复杂应用时,状态与UI的强耦合常常导致代码臃肿、难以维护,尤其是在多个组件需要共享状态时,这种问题尤为突出。使用StateStore,开发者可以将状态管理逻辑完全从UI中抽离,实现状态的集中式管理和更新,进而简化代码结构、提高可维护性。

本节以备忘录应用为例,演示如何通过StateStore实现多个组件的状态共享与更新,同时保持UI层的纯粹性。

图3 效果图

在这里插入图片描述

开发步骤

  1. 定义页面数据

    使用@ObservedV2定义页面需要的数据TodoList、TodoItem。

    @ObservedV2
    export class TodoStoreModel {
      @Trace todoList: TodoItemData[] = [];
      @Trace isShow: boolean = false;
      addTaskTextInputValue: string = '';
      // ...
    
      @Computed
      get uncompletedTodoList(): TodoItemData[] {
        return this.todoList.filter(item => !item.selected);
      }
    
      @Computed
      get completedTodoList(): TodoItemData[] {
        return this.todoList.filter(item => item.selected);
      }
    }
    
    @ObservedV2
    export class TodoItemData {
      id: number = 0;
      @Trace taskDetail: string = '';
      @Trace selected?: boolean;
      // ...
    
      constructor(taskDetail: string, selected?: boolean, id?: number) {
        this.id = id ? id : Date.now();
        this.taskDetail = taskDetail;
        this.selected = selected;
        // ...
      }
    
      // ...
    }
    
  2. 创建状态管理仓库

    • 定义状态更新事件类型Action

      export default class TodoListActions {
        static getTodoList: Action = StateStore.createAction('getTodoList');
        static addTodoList: Action = StateStore.createAction('addTodoList');
        static deleteTodoItem: Action = StateStore.createAction('deleteTodoItem');
        static updateTaskDetail: Action = StateStore.createAction('updateTaskDetail');
        static completeTodoItem: Action = StateStore.createAction('completeTodoItem');
        // ...
      };
      
    • 定义状态处理函数TodoReducer

      export const todoReducer: Reducer<TodoStoreModel> = (state: TodoStoreModel, action: Action) => {
        let GlobalContent = GlobalContext.getInstance();
        uiContext = GlobalContent.getUIContext()
        switch (action.type) {
          case TodoListActions.getTodoList.type:
            return async () => {
              state.todoList = (await RdbUtil.getInstance(uiContext?.getHostContext()!)).query();
            };
          case TodoListActions.addTodoList.type:
            if (state.addTaskTextInputValue === '') {
              uiContext!.getPromptAction().showToast({ message: $r('app.string.empty') });
              return null;
            }
            state.todoList.push(new TodoItemData(state.addTaskTextInputValue));
            state.isShow = false;
            state.addTaskTextInputValue = '';
            break;
          case TodoListActions.deleteTodoItem.type:
            // ...
            break;
          case TodoListActions.updateTaskDetail.type:
            // ...
            break;
          case TodoListActions.completeTodoItem.type:
            // ...
            break;
          // ...
        }
        return null;
      };
      
    • 创建状态管理仓库

      export const TODO_LIST_STORE_ID = 'todoListStore';
      
      export const TodoStore: Store<TodoStoreModel> =
        StateStore.createStore(TODO_LIST_STORE_ID, new TodoStoreModel(), todoReducer, [LogMiddleware]);
      
  3. 在UI中使用

    • 通过getState()拿到Store中的状态数据。
    • 使用dispatch()派发一个状态更新事件来刷新UI。

    如下例子中:Index组件内,通过getState()方法获取状态数据并绑定UI,通过dispatch触发GetTodoList事件获取全量数据并更新状态;TodoItem子组件中通过dispatch方法派发一个CompleteTodoItem事件来改变全局状态,将当前项设置为已完成。

    @Entry
    @ComponentV2
    struct Index {
      @Local viewModel: TodoStoreModel = TodoStore.getState();
      // ...
      aboutToAppear(): void {
        // The dispatch triggers a GetTodoList event to get the full data and update the status
        TodoStore.dispatch(TodoListActions.getTodoList);
      }
    
      // ...
      build() {
        Column() {
          // ...
          if (this.viewModel.todoList.length > 0) {
            List({ space: 12 }) {
              if (this.viewModel.uncompletedTodoList.length > 0) {
                ListItemGroup({ header: this.todayGroupHeader(), space: 12 }) {
                  ForEach(this.viewModel.uncompletedTodoList, (item: TodoItemData) => {
                    ListItem() {
                      TodoItem({ itemData: item });
                    };
                  }, (item: TodoItemData) => item.id.toString());
                };
              }
              // ...
            }.width('100%')
            .height('100%')
            .layoutWeight(1);
    
            // ...
      }
    }
    
    @ComponentV2
    export struct TodoItem {
      @Param @Require itemData: TodoItemData;
      // ...
    
      build() {
        Row({ space: 8 }) {
          Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
            .select(this.itemData.selected)
            .shape(CheckBoxShape.CIRCLE)
            .onChange((_value) => {
              // The child component changes the global state by sending a CompleteTodoItem event through the dispatch method, setting the current item to complete
              TodoStore.dispatch(TodoListActions.completeTodoItem.setPayload({ id: this.itemData.id, value: _value }));
            });
          // ...
        }
        // ...
      }
    }
    

通过StateStore库的使用,在UI上就没有任何状态更新逻辑,UI层面只需要关注界面描述和事件分发,保持了UI层的纯粹性。UI界面通过事件触发dispatch操作发送Action给Store来执行具体的逻辑,达到UI和状态解耦的效果。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐