【入门到精通】鸿蒙next开发:基于StateStore的全局状态管理开发实践

往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)


基于StateStore的全局状态管理开发实践

概述

多组件状态共享是我们经常会遇到的场景;ArkUI通过装饰器,例如@State+@Prop/@Link、@Provide+@Consume实现父子组件状态共享,但是这样会造成状态数据耦合。StateStore作为ArkUI状态与UI解耦的解决方案,支持全局维护状态,优雅地解决状态共享的问题。让开发者在开发过程中实现状态与UI解耦,多个组件可以方便地共享和更新全局状态,将状态管理逻辑从组件逻辑中分离出来,简化维护。

StateStore提供了下列功能特性:

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

原理介绍

StateStore运行原理是基于状态管理特性(@Observed@ObservedV2),通过集中管理状态和统一的Action-Reducer模型,实现状态的全局更新和同步。

运行原理可概括为以下几个核心流程:

1、状态定义与监听。

使用@Observed或@ObservedV2修饰需要管理的状态数据,依赖系统对数据改变监听的能力,驱动UI刷新。

2、状态管理仓库创建。

使用StateStore.createStore方法创建状态管理仓库(Store),注册初始状态和Reducer,集中管理状态和逻辑。

3、Action事件派发。

主线程通过dispatch(Action)、子线程通过receiveSendableAction(SendableAction)派发事件,驱动状态更新。

4、Reducer状态处理逻辑。

接收到的Action由Reducer处理,生成新的状态。

5、UI 刷新与反馈状态更新后,StateStore通过监听器通知UI框架刷新界面或执行其他处理逻辑,完成状态和UI的同步。

交互流程

1、用户在UI层触发操作,通过 dispatch(Action) 分发事件。

2、事件到达Store后,传递给Reducer处理,计算新的状态。

3、状态更新后,StateStore自动触发监听器通知UI层,完成数据驱动的界面更新。

系统核心概念

  • View:用户界面,包含页面UI组件,并响应用户操作;当用户和UI交互时,通过dispatch分发Action事件来进行状态更新。
  • Store:状态管理仓库;主要对外提供 getState()、 dispatch(action) 方法。
  • Dispatch:事件分发方法,UI侧用其将Action对象发送到Store,是UI侧触发更新的唯一方式。
  • Action:描述指示发生了的事件类型,提供type和payload属性。
  • Reducer:状态刷新逻辑处理函数。根据传入的Action事件指令,刷新状态。​​​​​​​
  • MiddleWare:介于dispatch和Reducer之间的软件层,允许在发送Action到Reducer执行之前或执行完成之后插入额外的处理逻辑。可以用来处理日志记录、请求鉴权等,用于扩展功能,使框架能够处理更复杂的场景。用户可根据业务逻辑自行选择是否使用。

使用StateStore实现状态与UI解耦

场景描述

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

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

实现效果图如下:​​​​​​​

1.png

开发步骤

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

// TodoListModel.ets 
@ObservedV2 
export class TodoItem { 
  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 
  } 
} 
 
@ObservedV2 
export class TodoList { 
  @Trace todoList: TodoItem[] = [] 
  @Trace isShow: boolean = false 
  addTaskTextInputValue: string = '' 
 
  @Computed 
  get uncompletedTodoList(): ITodoItemData[] { 
    return this.todoList.filter(item =>!item.selected) 
  } 
 
  @Computed 
  get completedTodoList(): ITodoItemData[] { 
    return this.todoList.filter(item => item.selected) 
  } 
}

2、创建状态管理仓库。

  a、定义状态更新事件类型Action。

export const GetTodoList: Action = StateStore.createAction('getTodoList') 
 
export const AddTodoList: Action = StateStore.createAction('addTodoList') 
 
export const DeleteTodoItem: Action = StateStore.createAction('deleteTodoItem') 
 
export const UpdateTaskDetail: Action = StateStore.createAction('updateTaskDetail') 
 
export const CompleteTodoItem: Action = StateStore.createAction('completeTodoItem') 
// ...

  b、定义状态处理函数TodoReducer。

const TodoReducer: Reducer<TodoStoreModel> = (_state: TodoStoreModel, action: Action) => { 
  switch (action.type) { 
    // 获取待办列表 
    case GetTodoList.type: 
      // 从数据库获取全量列表并赋值给state中的todoList 
      return async () => { 
        _state.todoList = (await RdbUtil.getInstance(getContext())).query() 
      }; 
    // 新增待办任务 
    case AddTodoList.type: 
      if (_state.addTaskTextInputValue === '') { 
        promptAction.showToast({ message: $r('app.string.empty') }) 
        return null 
      } 
      // 插入一条待办任务到state的todoList中 
      _state.todoList.push(new TodoItemData(_state.addTaskTextInputValue)) 
      _state.isShow = false 
      _state.addTaskTextInputValue = '' 
      break 
    // 删除待办任务 
    case DeleteTodoItem.type: 
      // 删除逻辑 
      break 
    // 修改待办任务 
    case UpdateTaskDetail.type: 
      // 修改逻辑 
      break 
    // 待办任务完成 
    case CompleteTodoItem.type: 
      // 已完成逻辑 
      break 
  } 
  return null 
}

  c、创建状态管理仓库。

const TODO_LIST_STORE = "toDoListStore" 
export const TodoStore: Store<TodoStoreModel> = 
  StateStore.createStore(TODO_LIST_STORE, new TodoStoreModel(), TodoReducer)

3、在UI中使用。

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

Index.ets

@Entry 
@ComponentV2 
struct Index { 
  // 通过getState()获取状态数据 
  @Local viewModel: ITodoStore = TodoStore.getState() 
 
  aboutToAppear(): void { 
    // 通过dispatch触发GetTodoList事件获取全量数据并更新状态 
    TodoStore.dispatch(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: ITodoItemData) => { 
                ListItem() { 
                  TodoItem({ itemData: item }) 
                } 
              }, (item: ITodoItemData) => item.id.toString()) 
            } 
          } 
        }.width('100%').height('100%').layoutWeight(1) 
      } 
      // ... 
    } 
  } 
}

TodoItem.ets

@ComponentV2 
export struct TodoItem { 
  @Param @Require itemData: ITodoItemData 
 
  build() { 
    Row({ space: 8 }) { 
      Checkbox({ name: 'checkbox1', group: 'checkboxGroup' }) 
        .select(this.itemData.selected) 
        .shape(CheckBoxShape.CIRCLE) 
        .onChange((_value) => { 
          TodoStore.dispatch(CompleteTodoItem.setPayload({ id: this.itemData.id, value: _value })) 
        }) 
      Row() { 
        // ... 
      }.layoutWeight(1) 
    } 
  } 
}

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

子线程同步数据库

场景描述

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

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

在本节中,我们将通过在子线程中同步数据库的场景,介绍如何在子线程中发送状态更新事件,需要实现效果图如下:

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

开发步骤

1、定义数据。

// 主线程使用 
@ObservedV2 
export class TodoItemData implements ITodoItemData { 
  id: number = 0 
  @Trace taskDetail: string = '' 
  @Trace selected?: boolean 
  toDoItemSendable: ToDoItemSendable 
 
  constructor(taskDetail: string, selected?: boolean, id?: number) { 
    this.id = id ? id : Date.now() 
    this.taskDetail = taskDetail 
    this.selected = selected 
    this.toDoItemSendable = new ToDoItemSendable(this.id, this.taskDetail, this.selected) 
  } 
} 
// 子线程使用 
@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 
  } 
}

2、定义子线程操作函数并发送SendableAction。

@Concurrent 
async function UpdateProgress_1(context: Context, data: ToDoItemSendable[]): Promise<void> { 
  try { 
    // 与远程数据对比整理出那些是inset的那些是update的那些是delete的 
    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)); 
    // 发送设置进度条总数事件SetTotal 
    taskpool.Task.sendData(StateStore.createSendableAction(TODO_LIST_STORE, SetTotal.type, 
      toAdd.length + toUpdate.length + toDelete.length)) 
 
    for (const todo of toAdd) { 
      rdb.inset(todo) 
      // 发送更新进度条事件UpdateProgress 
      taskpool.Task.sendData(StateStore.createSendableAction(TODO_LIST_STORE, UpdateProgress.type, null)) 
    } 
    //... 
  } catch (err) { 
    console.error(err) 
    return undefined; 
  } 
}

3、主线程接受后触发dispatch修改状态数据。

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(UpdateProgress_1, getContext(), ToBeSynced); 
    // 接受子线程发送的SendableAction 
    task.onReceiveData((data: SendableAction) => { 
      // 使用receiveSendableAction方法触发子线程发送的Action来刷新状态 
      StateStore.receiveSendableAction(data) 
    }) 
    await taskpool.execute(task) 
  } catch (err) { 
    console.error(err) 
  } 
}

4、Reducer定义数据操作逻辑。

export const UpdateProgress: Action = StateStore.createAction('updateProgress') 
export const SetTotal: Action = StateStore.createAction('setTotal') 
const TodoReducer: Reducer<TodoStoreModel> = (_state: TodoStoreModel, action: Action) => { 
  switch (action.type) { 
  // ... 
    case UpdateProgress.type: 
      _state.progress.value++ 
      break 
    case SetTotal.type: 
      _state.progress.total = action.payload 
      break 
  } 
  return null 
}

5、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处理的流程中插入自定义逻辑,从而解耦通用功能和核心状态管理逻辑。

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

3.png

开发步骤

1、定义中间件。

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) => { 
  // 操作前埋点 
  console.log('logMiddleware-before:', JSON.stringify(state.todoList), action.type); 
  return MiddlewareStatus.NEXT 
}, (state: TodoStoreModel, action: Action) => { 
  // 操作后埋点 
  console.log('logMiddleware-after:', JSON.stringify(state.todoList)); 
  return MiddlewareStatus.NEXT 
})

2、使用中间件。

export const TodoStore: Store<TodoStoreModel> = 
  StateStore.createStore(TODO_LIST_STORE, new TodoStoreModel(), TodoReducer, [LogMiddleware])

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值