鸿蒙5.0 APP开发案例分析:基于StateStore的全局状态管理开发实践

往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)

✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✏️ 记录一场鸿蒙开发岗位面试经历~

✏️ 持续更新中……


概述

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

StateStore作为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解耦。

图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修饰业务数据,并生成实例对象。

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

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

  1. 创建状态管理仓库

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

  1. 组件UI的初始渲染

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

  1. 组件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;
    // ...
  }

  // ...
}
  1. 创建状态管理仓库
  • 定义状态更新事件类型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) => {
  switch (action.type) {
    case TodoListActions.getTodoList.type:
      return async () => {
        state.todoList = (await RdbUtil.getInstance(getContext())).query();
      };
    case TodoListActions.addTodoList.type:
      if (state.addTaskTextInputValue === '') {
        promptAction.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]);
  1. 在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和状态解耦的效果。

子线程同步数据库

场景描述

在HarmonyOS开发中,子线程无法直接修改或者操作UI状态。这种限制导致子线程在完成复杂任务处理后,需要额外的逻辑将任务结果同步到主线程进行状态更新。

为了解决这一问题,StateStore提供了SendableAction机制,使开发者可以在子线程中采用与主线程一致的方式分发Action,无需关注状态更新逻辑。

在本节中,我们将通过在子线程中同步数据库的场景,介绍如何在子线程中发送状态更新事件。

图4 效果图

上图效果图中,用户点击同步数据库按钮,子线程去读写数据库,同时更新进度条。

开发步骤

  1. 定义数据
@Sendable
export class ToDoItemSendable implements lang.ISendable {
  id: number;
  detail: string;
  selected: boolean;
  state: number;

  constructor(id: number, detail: string, selected: boolean = false) {
    this.id = id;
    this.selected = selected;
    this.detail = detail;
    this.state = 0;
  }
}
  1. 定义子线程操作函数并发送SendableAction
    通过StateStore.createSendableAction方法定义一个sendableAction事件,它的作用于Action的作用一致,在子线程中需要使用taskpool的sendData发送一个sendableAction事件。

createSendableAction方法接受三个参数分别是:

  • storeId: string,必选参数,用于描述当前的sendableAction事件需要操作的数据仓库。
  • type: string,必选参数,用于描述当前的sendableAction的事件类型,需要在reducer中有对应的类型逻辑事件。
  • payload?: ESObject,可选参数,事件的参数。
@Concurrent
async function concurrentUpdateProgress(context: Context, data: ToDoItemSendable[]): Promise<void> {
  try {
    let rdb = await RdbUtil.getInstance(context);
    const originalIds = rdb.getAllIds();
    const toAdd = data.filter(todo =>!originalIds.some(id => todo.id === id));
    const toUpdate = data.filter(todo => todo.state === 0 && originalIds.indexOf(todo.id) > -1);
    const toDelete = originalIds.filter(id =>!data.some(todo => todo.id === id));
    // send setTotal event to set the total number of progress bars
    taskpool.Task.sendData(StateStore.createSendableAction(TODO_LIST_STORE_ID, TodoListActions.setTotal.type,
      toAdd.length + toUpdate.length + toDelete.length));

    for (const todo of toAdd) {
      rdb.inset(todo);
      await sleep(500);
      // send the update progress bar event updateProgress
      taskpool.Task.sendData(StateStore.createSendableAction(TODO_LIST_STORE_ID, TodoListActions.updateProgress.type,
        todo.id));
    }
    // ...
  } catch (err) {
    console.error(`${err.message}\n${err.stack}`);
    return undefined;
  }
}
  1. 主线程接受后触发dispatch修改状态数据
    主线程中使用onReceiveData接收sendData发送的sendableAction事件,然后调用StateStore.receiveSendableAction来执行这个事件通知reducer修改状态。
export async function syncDatabase() {
  try {
    const todos: TodoItemData[] = TodoStore.getState().todoList;
    const ToBeSynced = todos.map(item => item.toDoItemSendable);
    let task: taskpool.Task = new taskpool.Task(concurrentUpdateProgress, getContext(), ToBeSynced);
    task.onReceiveData((data: SendableAction) => {
      // Use the receiveSendableAction method to trigger the Action sent by the child thread to refresh the state
      StateStore.receiveSendableAction(data);
    });
    await taskpool.execute(task);
    TodoStore.dispatch(TodoListActions.clearProgress);
  } catch (err) {
    console.error(`${err.message}\n${err.stack}`);
  }
}
  1. Reducer定义数据操作逻辑
case TodoListActions.updateProgress.type:
  let item = state.syncTodoList.find(item => item.id === action.payload);
  item?.updateState(1);
  state.progress.value++;
  break;
case TodoListActions.setTotal.type:
  state.syncTodoList = state.todoList.filter(item => item.state === 0);
  state.progress.total = action.payload;
  break;
  1. UI渲染
@CustomDialog
export struct AsyncProgressBuilder {
  controller: CustomDialogController;
  // ...

  build() {
    Column() {
      // ...
      Progress({
        value: TodoStore.getState().progress.value,
        total: TodoStore.getState().progress.total,
        type: ProgressType.Linear
      }).style({ enableSmoothEffect: true })
        .width('100%')
        .height(24);
    }
    // ...
  }
}

状态更新日志埋点

场景描述

在状态管理过程中,复杂业务逻辑往往需要在状态更新前后插入额外的处理逻辑,例如记录状态更新日志、请求鉴权等。这些逻辑如果直接耦合在状态管理的核心流程中,会导致代码冗杂且难以维护。

为了解决这一问题,中间件应运而生。中间件是一种灵活的扩展机制,能够在Action分发到Reducer处理的流程中插入自定义逻辑,从而解耦通用功能和核心状态管理逻辑。

在本节中,我们将通过日志埋点场景,展示如何利用中间件优雅地扩展状态管理功能。

图5 日志效果图

图6 中间件执行流程图

开发步骤

  1. 定义中间件
    开发者根据业务逻辑需要来实现beforeAction和afterAction两个钩子方法,分别在状态更新前后执行自定义逻辑。
export class MiddlewareInstance<T> extends Middleware<T> {
  beforeAction: MiddlewareFuncType<T>;
  afterAction: MiddlewareFuncType<T>;

  constructor(beforeAction: MiddlewareFuncType<T>, afterAction: MiddlewareFuncType<T>) {
    super();
    this.beforeAction = beforeAction;
    this.afterAction = afterAction;
  }
}

export const LogMiddleware = new MiddlewareInstance<TodoStoreModel>((state: TodoStoreModel, action: Action) => {
  hilog.info(0x0000, 'StateStoreSample', 'logMiddleware-before1:', JSON.stringify(state.todoList), action.type);
  return MiddlewareStatus.NEXT;
}, (state: TodoStoreModel) => {
  hilog.info(0x0000, 'StateStoreSample', 'logMiddleware-after:', JSON.stringify(state.todoList));
  return MiddlewareStatus.NEXT;
});
  1. 使用中间件
export const TODO_LIST_STORE_ID = 'todoListStore';
export const TodoStore: Store<TodoStoreModel> =
  StateStore.createStore(TODO_LIST_STORE_ID, new TodoStoreModel(), todoReducer, [LogMiddleware]);

在Store中注册LogMiddleware后,所有状态更新逻辑执行前都会触发LogMiddleware的beforeAction 逻辑打印日志,状态更新逻辑执行后也会触发afterAction 逻辑打印日志。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值