鸿蒙跨端状态管理实战:从组件通信到多 Ability 同步的完整架构

一、引言:多端部署下的状态管理挑战
在 HarmonyOS NEXT 的生态里,"一次开发,多端部署"不仅是一句口号,更是工程落地时必须面对的技术命题。当你的应用需要在手机、折叠屏、平板甚至车机上提供一致体验时,UI 布局可以靠响应式栅格去适配,但数据流的管理没有银弹——你需要一套能在不同设备、不同 Ability、不同组件层级之间稳定运转的状态管理方案。
为什么状态管理在多端场景下会格外棘手?原因有三:
- 组件层级差异大:手机上一个页面内嵌 2-3 层组件,平板上可能嵌套 5-6 层,@Prop 逐层透传会带来巨量胶水代码。
- 跨 Ability 场景普遍:多窗模式、自由窗口、元服务分流转场,都需要数据在 Ability 间同步。
- 设备规格差异:折叠屏展开/折叠时 UI 树重建,状态如果绑定在组件实例上就会丢失。
ArkUI 为开发者提供了一套从局部到全局、从同步到异步的完整状态管理工具链。本文将从最底层的 @State/@Prop/@Link 一路剖析到全局 AppStorage 与 EventHub 事件总线,最终落地为一个可直接复用的多端状态管理架构——StateCenter + EventBus + 跨 Ability 同步。
这篇文章的所有代码基于 HarmonyOS NEXT(API 12+)编写,可以直接在 DevEco Studio 中新建工程验证。
二、解构基础:@State、@Prop、@Link 的组件级通信
2.1 @State:组件内状态的唯一源头
每个 @State 装饰的变量都是该组件内状态变化的唯一源头。ArkUI 的响应式引擎会追踪所有对 @State 变量的读取与写入,当值发生变化时自动重绘依赖它的 UI。
// BasicStateDemo.ets
@Component
struct CounterCard {
@State count: number = 0;
@State label: string = '点击计数';
build() {
Column({ space: 12 }) {
Text(this.label)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(`${this.count}`)
.fontSize(48)
.fontWeight(FontWeight.Bold)
.fontColor(this.count > 10 ? Color.Red : Color.Blue)
Button('+1')
.onClick(() => {
this.count++;
if (this.count > 10) {
this.label = '超过10次了!';
}
})
Button('重置')
.onClick(() => {
this.count = 0;
this.label = '点击计数';
})
}
.padding(24)
}
}
这段代码展示了 @State 最基本的能力:数据变化驱动 UI 更新。count 超过 10 时字体变红、label 自动切换——ArkUI 的 diff 算法只更新变化的节点,不会全量刷新。
2.2 @Prop:单向数据流的桥梁
@Prop 允许父组件将数据单向注入子组件。子组件可以修改 @Prop 变量,但这个修改不会同步回父组件——它是父组件数据的一个本地副本。
// PropDemo.ets
@Component
struct TemperatureDisplay {
@Prop celsius: number = 0;
build() {
Row({ space: 8 }) {
Text(`${this.celsius}°C`)
.fontSize(24)
Text(`= ${this.celsius * 9 / 5 + 32}°F`)
.fontSize(16)
.fontColor(Color.Gray)
}
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
}
}
@Entry
@Component
struct TemperaturePage {
@State currentTemp: number = 25;
build() {
Column({ space: 16 }) {
Text('环境温度监控')
.fontSize(20)
.fontWeight(FontWeight.Bold)
TemperatureDisplay({ celsius: this.currentTemp })
Slider({
value: this.currentTemp,
min: -10,
max: 50,
step: 1
})
.onChange((val: number) => {
this.currentTemp = Math.round(val);
})
Text('拖动滑块改变温度')
.fontSize(14)
.fontColor(Color.Gray)
}
.padding(24)
.width('100%')
}
}
父组件 TemperaturePage 的 currentTemp 通过 @Prop 传递给 TemperatureDisplay。子组件只负责展示,不污染数据源——单向数据流的约束保证了状态变更可追踪、可预测。
2.3 @Link:双向绑定的直通车
当子组件需要修改父组件的状态时,@Link 登场。它建立的是真正的双向绑定:父子组件共享同一份内存引用。
// LinkDemo.ets
@Component
struct EditableItem {
@Link text: string;
build() {
Row({ space: 8 }) {
TextInput({ text: this.text, placeholder: '编辑内容...' })
.onChange((val: string) => {
this.text = val;
})
.layoutWeight(1)
Button('清除')
.onClick(() => {
this.text = '';
})
}
.padding(8)
.width('100%')
}
}
@Entry
@Component
struct EditorPage {
@State title: string = '初始标题';
@State subtitle: string = '副标题内容';
build() {
Column({ space: 16 }) {
Text('文档编辑器')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(`主标题:${this.title}`)
.fontSize(16)
EditableItem({ text: $title })
Text(`副标题:${this.subtitle}`)
.fontSize(14)
.fontColor(Color.Gray)
EditableItem({ text: $subtitle })
}
.padding(24)
.width('100%')
}
}
注意 $title 和 $subtitle 的语法——这是 ArkUI 的引用传递运算符($)。它告诉框架:我要传递的不是值,而是指向 @State 变量的引用。子组件通过 @Link 接收这个引用后,修改会直接反映到父组件。
2.4 何时选谁?
| 装饰器 | 方向 | 子组件可改? | 适用场景 |
|---|---|---|---|
| @State | 自身拥有 | 是 | 组件内部状态 |
| @Prop | 父→子(单向) | 改不影响父 | 展示型子组件 |
| @Link | 父⇄子(双向) | 是,同步回父 | 表单/输入型子组件 |
小结:@State/@Prop/@Link 解决了父子组件间的状态传递,但当组件树变深、层级变多时,逐层透传会让代码急剧膨胀。这时候就需要 @Provide/@Consume 来"跨层直达"。
三、跨层传递:@Provide/@Consume 的依赖注入模式
想象这样一个场景:你的页面结构是 首页(Page)→ 内容区(ContentArea)→ 侧边栏(Sidebar)→ 用户卡片(UserCard),UserCard 需要读取 Page 层的用户信息。用 @Prop 逐层传递?那需要在 ContentArea 和 Sidebar 里声明完全不需要的 user 属性——这就是"属性逐层透传"(prop drilling)问题。
@Provide/@Consume 就是 ArkUI 解决这个问题的方案。
3.1 基本原理
- @Provide:在当前组件上注册一个"可被后代任意层级获取"的状态变量。
- @Consume:在后代组件中声明同名的 @Consume 变量,框架自动找到最近的 @Provide 祖先绑定。
// ProvideConsumeDemo.ets
@Component
struct UserCard {
@Consume userName: string;
@Consume userRole: string;
build() {
Column({ space: 8 }) {
Image($r('app.media.avatar'))
.width(48)
.height(48)
.borderRadius(24)
Text(this.userName)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(this.userRole)
.fontSize(14)
.fontColor(Color.Gray)
}
.padding(16)
.backgroundColor('#FFF')
.borderRadius(12)
.shadow({ radius: 4, color: '#10000000' })
}
}
@Component
struct Sidebar {
build() {
Column() {
// 中间不需要传任何 props
UserCard()
}
.padding(12)
.width(200)
}
}
@Component
struct ContentArea {
build() {
Row() {
// 主内容区域
Column() {
Text('主内容区').fontSize(18)
}
.layoutWeight(1)
Sidebar()
}
.width('100%')
.height('100%')
}
}
@Entry
@Component
struct DashboardPage {
@Provide userName: string = '张明';
@Provide userRole: string = '高级工程师';
build() {
Column({ space: 0 }) {
// 顶部导航
Row() {
Text('工作台')
.fontSize(22)
.fontWeight(FontWeight.Bold)
}
.padding(16)
.width('100%')
.backgroundColor('#FFF')
ContentArea()
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F0F2F5')
}
}
注意 UserCard 内部 没有接收任何 props,userName 和 userRole 是从 DashboardPage 的 @Provide "跨层穿透"过来的。ContentArea 和 Sidebar 不需要声明、不需要感知这些数据。
3.2 与 @Link 相似的绑定特性
@Consume 也可以通过 $ 语法实现双向修改:
@Component
struct ThemeToggle {
@Consume theme: string;
build() {
Button(`切换为 ${this.theme === 'light' ? '暗色' : '亮色'} 主题`)
.onClick(() => {
this.theme = this.theme === 'light' ? 'dark' : 'light';
})
}
}
当后代组件通过 @Consume 修改 theme 时,祖先的 @Provide 变量也会同步更新,所有依赖该变量的组件都会重绘。
小结:@Provide/@Consume 是 ArkUI 的依赖注入方案,适合跨 3 层以上的状态传递。但要注意——它只向下传递,无法向兄弟组件或祖先方向传播。
四、观察者模式:@Observed/@ObjectLink 的深度响应
@State 有一个隐藏的缺陷:它只能观测自身的变化。如果你有一个对象类型的 @State,修改其内部属性时,@State 并不会触发重绘。
// ❌ 这种写法不会触发 UI 更新
@State user: UserInfo = { name: '张三', age: 25 };
this.user.age = 26; // UI 不会重新渲染!
为什么?因为 @State 用的是引用比较——user 对象的引用没变,框架认为"没有变化"。
4.1 @Observed 拯救嵌套对象
要使对象内部属性的变化也能被观测,需要两步:
- 用
@Observed装饰 class - 在引用该对象的子组件中用
@ObjectLink代替@Prop
// ObservedObjectLinkDemo.ets
@Observed
class TodoItem {
id: number;
title: string;
completed: boolean;
constructor(id: number, title: string, completed: boolean = false) {
this.id = id;
this.title = title;
this.completed = completed;
}
}
@Component
struct TodoRow {
@ObjectLink item: TodoItem;
@Link onToggle: () => void;
build() {
Row({ space: 12 }) {
Checkbox()
.select(this.item.completed)
.onChange(() => {
this.onToggle();
})
Text(this.item.title)
.fontSize(16)
.decoration({ type: this.item.completed ?
TextDecorationType.LineThrough : TextDecorationType.None })
.fontColor(this.item.completed ? Color.Gray : Color.Black)
}
.padding({ top: 8, bottom: 8 })
.width('100%')
}
}
@Entry
@Component
struct TodoList {
@State todos: TodoItem[] = [];
@State inputText: string = '';
aboutToAppear(): void {
this.todos.push(new TodoItem(1, '学习 @Observed 装饰器'));
this.todos.push(new TodoItem(2, '理解 @ObjectLink 用法'));
this.todos.push(new TodoItem(3, '在项目中实践'));
}
toggleTodo(index: number): void {
this.todos[index].completed = !this.todos[index].completed;
}
addTodo(): void {
if (this.inputText.trim().length === 0) return;
this.todos.push(
new TodoItem(Date.now(), this.inputText.trim())
);
this.inputText = '';
}
build() {
Column({ space: 16 }) {
Text('待办事项(@Observed 深度观测)')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Row({ space: 8 }) {
TextInput({ text: this.inputText, placeholder: '输入新任务...' })
.onChange((val: string) => { this.inputText = val; })
.layoutWeight(1)
Button('添加')
.onClick(() => this.addTodo())
}
.width('100%')
List() {
ForEach(this.todos, (item: TodoItem, index: number) => {
ListItem() {
TodoRow({
item: item,
onToggle: $toggleTodo
})
}
}, (item: TodoItem) => item.id.toString())
}
.layoutWeight(1)
.width('100%')
}
.padding(24)
.width('100%')
.height('100%')
}
}
关键细节:
@Observed装饰的 class,其实例属性的深度变化都会被拦截并通知。@ObjectLink替代@Prop来接收 @Observed 对象,它能追踪内部属性变化。- 数组本身是 @State 观测的,所以
push操作会触发视图更新;数组元素的内部属性变化则由 @Observed/@ObjectLink 负责。
小结:@Observed + @ObjectLink 是 ArkUI 处理复杂嵌套数据模型的标配,在表单数据、列表项编辑等场景中不可或缺。
五、全局存储:AppStorage 与 LocalStorage 的精妙分工
组件树级别的通信并不能解决所有问题。当你有以下需求时,就需要全局存储层:
- 跨页面(不同 Page)共享数据
- 跨 Ability(如 FA 切换到 PA)同步状态
- 应用级配置(主题、语言、登录态)
5.1 AppStorage:应用级单例存储
AppStorage 是整个应用进程内唯一的全局存储对象,所有 UI 组件都可以绑定它。
// AppStorageBasicDemo.ets
// 在任何组件外部初始化 AppStorage 键
AppStorage.SetOrCreate('userId', '');
AppStorage.SetOrCreate('userName', '未登录');
AppStorage.SetOrCreate('isLoggedIn', false);
AppStorage.SetOrCreate('theme', 'light');
@Entry
@Component
struct ProfilePage {
// 用 @StorageLink 实现双向绑定
@StorageLink('userId') userId: string = '';
@StorageLink('userName') userName: string = '';
@StorageLink('isLoggedIn') isLoggedIn: boolean = false;
@StorageLink('theme') theme: string = 'light';
build() {
Column({ space: 16 }) {
Text('用户中心')
.fontSize(22)
.fontWeight(FontWeight.Bold)
if (this.isLoggedIn) {
// 已登录状态
Image($r('app.media.avatar_placeholder'))
.width(64)
.height(64)
.borderRadius(32)
Text(`欢迎回来,${this.userName}`)
.fontSize(18)
Text(`ID: ${this.userId}`)
.fontSize(14)
.fontColor(Color.Gray)
Button('退出登录')
.backgroundColor(Color.Red)
.onClick(() => {
this.isLoggedIn = false;
this.userName = '未登录';
this.userId = '';
})
} else {
Text('请先登录')
.fontSize(18)
Button('模拟登录')
.onClick(() => {
this.isLoggedIn = true;
this.userName = '张三';
this.userId = 'U20260715';
})
}
Divider()
Row({ space: 12 }) {
Text('当前主题:')
Text(this.theme === 'light' ? '亮色 ☀️' : '暗色 🌙')
}
Button('切换主题')
.onClick(() => {
this.theme = this.theme === 'light' ? 'dark' : 'light';
})
}
.padding(24)
.width('100%')
.backgroundColor(this.theme === 'light' ? '#FFF' : '#333')
}
}
// 在另一个页面中读取相同的 AppStorage 键
@Entry
@Component
struct AnotherPage {
@StorageProp('userName') userName: string = '未登录';
@StorageProp('isLoggedIn') isLoggedIn: boolean = false;
build() {
Column() {
Text(this.isLoggedIn ?
`跨页面显示:${this.userName}` :
'跨页面显示:未登录')
.fontSize(16)
}
.padding(24)
}
}
@StorageLink vs @StorageProp 的选择规则与 @Link/@Prop 完全一致:需要修改 AppStorage 的值用 @StorageLink,只读展示用 @StorageProp。
5.2 LocalStorage:页面级共享存储
LocalStorage 的作用域限定在一个页面实例内。它比 AppStorage 粒度更细,适合多 Tab 页场景中页面内部共享状态。
// LocalStorageDemo.ets
// 创建一个 LocalStorage 实例
let pageStorage: LocalStorage = new LocalStorage({
'filterText': '',
'selectedCategory': '全部',
'sortOrder': 'desc'
});
@Entry(pageStorage)
@Component
struct ProductListPage {
@LocalStorageLink('filterText') filterText: string = '';
@LocalStorageLink('selectedCategory') selectedCategory: string = '全部';
@LocalStorageLink('sortOrder') sortOrder: string = 'desc';
private allProducts: Product[] = [];
aboutToAppear(): void {
this.allProducts = [
new Product(1, '鸿蒙手机', '电子产品', 4999),
new Product(2, '智能手表', '穿戴', 1999),
new Product(3, '平板电脑', '电子产品', 3299),
new Product(4, '无线耳机', '配件', 899),
];
}
get filteredProducts(): Product[] {
let result = this.allProducts.filter(p => {
const matchSearch = p.name.includes(this.filterText);
const matchCategory = this.selectedCategory === '全部' ||
p.category === this.selectedCategory;
return matchSearch && matchCategory;
});
if (this.sortOrder === 'asc') {
result.sort((a, b) => a.price - b.price);
} else {
result.sort((a, b) => b.price - a.price);
}
return result;
}
build() {
Column({ space: 12 }) {
Text('商品列表')
.fontSize(20)
.fontWeight(FontWeight.Bold)
TextInput({ text: this.filterText, placeholder: '搜索商品...' })
.onChange((val: string) => { this.filterText = val; })
.width('100%')
Row({ space: 8 }) {
Text('分类:')
Text(this.selectedCategory)
Text('排序:')
Button(this.sortOrder === 'asc' ? '↑ 升序' : '↓ 降序')
.fontSize(14)
.onClick(() => {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
})
}
.width('100%')
List() {
ForEach(this.filteredProducts, (product: Product) => {
ListItem() {
ProductCard({ product: product })
}
}, (product: Product) => product.id.toString())
}
.layoutWeight(1)
.width('100%')
}
.padding(24)
.width('100%')
.height('100%')
}
}
@Observed
class Product {
id: number;
name: string;
category: string;
price: number;
constructor(id: number, name: string, category: string, price: number) {
this.id = id;
this.name = name;
this.category = category;
this.price = price;
}
}
@Component
struct ProductCard {
@ObjectLink product: Product;
build() {
Row({ space: 12 }) {
Column() {
Text(this.product.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(this.product.category)
.fontSize(13)
.fontColor(Color.Gray)
}
.layoutWeight(1)
Text(`¥${this.product.price}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Red)
}
.padding(12)
.backgroundColor('#FFF')
.borderRadius(8)
.shadow({ radius: 2, color: '#08000000' })
.width('100%')
}
}
5.3 AppStorage 与 LocalStorage 的分工决策
┌─────────────────────────────────────────────────┐
│ AppStorage │
│ 应用级别,跨页面、跨 Ability 可见 │
│ 用户登录态、主题偏好、全局配置、设备信息 │
├─────────────────────────────────────────────────┤
│ LocalStorage │
│ 页面级别,同页面内跨组件共享 │
│ 列表搜索条件、当前页签、页面内表单草稿 │
├─────────────────────────────────────────────────┤
│ @State / @Provide / @Link 等 │
│ 组件级别,本组件及其派生子组件可见 │
│ 表单输入、组件展示状态、临时 UI 状态 │
└─────────────────────────────────────────────────┘
决策原则:状态的影响范围越小越好。能用 @State 解决的不用 @Provide,能用 LocalStorage 解决的不用 AppStorage。
小结:AppStorage 是"应用级全局变量",LocalStorage 是"页面级共享存储"。两者使用体验一致,只是作用域不同。
六、事件总线:基于 EventHub 的松耦合通信
前面所有方案都是数据驱动的——状态变化了,UI 自动更新。但有些场景需要事件驱动的通信:A 做了一件事,通知 B、C、D 各自响应。比如用户退出登录需要清理所有页面的缓存。
6.1 封装 EventHub 为通用事件总线
// EventBus.ets
import { common } from '@kit.AbilityKit';
type EventHandler = (...args: Object[]) => void;
/**
* 全局事件总线 —— 封装 EventHub,提供类型安全的事件通信
*/
export class EventBus {
private static instance: EventBus;
private context?: common.UIAbilityContext;
private constructor() {}
static getInstance(): EventBus {
if (!EventBus.instance) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
init(context: common.UIAbilityContext): void {
this.context = context;
}
emit(eventName: string, ...args: Object[]): void {
if (!this.context) {
console.warn('[EventBus] 未初始化');
return;
}
this.context.eventHub.emit(eventName, ...args);
}
on(eventName: string, handler: EventHandler): void {
if (!this.context) {
console.warn('[EventBus] 未初始化');
return;
}
this.context.eventHub.on(eventName, handler);
}
off(eventName: string, handler?: EventHandler): void {
if (!this.context) return;
if (handler) {
this.context.eventHub.off(eventName, handler);
} else {
this.context.eventHub.off(eventName);
}
}
once(eventName: string, handler: EventHandler): void {
const wrapper: EventHandler = (...args: Object[]) => {
handler(...args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
}
// 预定义事件名称常量
export const Events = {
USER_LOGIN: 'user:login',
USER_LOGOUT: 'user:logout',
SYNC_COMPLETE: 'sync:complete',
THEME_CHANGED: 'theme:changed',
DEVICE_ORIENTATION_CHANGE: 'device:orientationChange',
} as const;
export const eventBus = EventBus.getInstance();
6.2 使用 EventBus
// EventBusUsageDemo.ets
import { eventBus, Events } from './EventBus';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct SyncIndicator {
@State statusText: string = '等待事件...';
aboutToAppear(): void {
const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
eventBus.init(context);
eventBus.on(Events.USER_LOGIN, (data: Object) => {
this.statusText = `用户已登录: ${JSON.stringify(data)}`;
});
eventBus.on(Events.THEME_CHANGED, (data: Object) => {
this.statusText = `主题已切换: ${JSON.stringify(data)}`;
});
}
aboutToDisappear(): void {
eventBus.off(Events.USER_LOGIN);
eventBus.off(Events.THEME_CHANGED);
}
build() {
Column({ space: 12 }) {
Text('事件监听')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(this.statusText)
.fontSize(14)
Button('触发登录事件')
.onClick(() => {
eventBus.emit(Events.USER_LOGIN, { userId: 'U001', timestamp: Date.now() });
})
Button('触发主题变更')
.onClick(() => {
eventBus.emit(Events.THEME_CHANGED, { theme: 'dark' });
})
}
.padding(24)
.width('100%')
}
}
小结:EventBus 是松耦合通信的核心。它与 @StorageLink 互为补充——事件总线适合"通知",存储链接适合"数据"。
七、联动机制:@Watch 的智能监听与状态串联
当一个状态变化时,你往往需要执行一些副作用——比如本地持久化、网络请求、数据加工。@Watch 装饰器就是 ArkUI 提供的"响应式钩子"。
7.1 @Watch 基础用法
// WatchDemo.ets
@Entry
@Component
struct SearchPage {
@State @Watch('onSearchTextChanged') searchText: string = '';
@State searchHistory: string[] = [];
@State debounceTimer: number | null = null;
onSearchTextChanged(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.performSearch(this.searchText);
}, 500);
}
private performSearch(query: string): void {
if (query.trim().length === 0) return;
if (!this.searchHistory.includes(query)) {
this.searchHistory = [query, ...this.searchHistory].slice(0, 10);
}
}
clearHistory(): void {
this.searchHistory = [];
}
build() {
Column({ space: 12 }) {
Text('商品搜索(带 @Watch 防抖)')
.fontSize(18)
.fontWeight(FontWeight.Bold)
TextInput({ text: this.searchText, placeholder: '输入关键词...' })
.onChange((val: string) => { this.searchText = val; })
.width('100%')
Text(`当前输入:${this.searchText || '(空)'}`)
.fontSize(13)
.fontColor(Color.Gray)
Divider()
Row() {
Text('搜索历史').fontSize(16).fontWeight(FontWeight.Medium)
Blank()
if (this.searchHistory.length > 0) {
Button('清空').fontSize(13).onClick(() => this.clearHistory())
}
}
.width('100%')
List() {
ForEach(this.searchHistory, (item: string) => {
ListItem() {
Text(item).fontSize(15).padding({ top: 6, bottom: 6 })
}
}, (item: string) => item)
}
.layoutWeight(1)
.width('100%')
}
.padding(24)
.width('100%')
.height('100%')
}
}
7.2 多状态联动的 Watch 编排
当多个状态彼此关联时,@Watch 可以编排复杂的连锁反应。看一个购物车示例:
// CartWatchChainDemo.ets
@Entry
@Component
struct CartPage {
@State @Watch('onCartChanged') items: CartItem[] = [];
@State totalAmount: number = 0;
@State itemCount: number = 0;
@State discount: number = 0;
aboutToAppear(): void {
this.items.push(new CartItem('P001', '鸿蒙手机', 4999, 1));
this.items.push(new CartItem('P002', '智能手表', 1999, 2));
this.recalc();
}
onCartChanged(): void {
this.recalc();
}
recalc(): void {
this.itemCount = this.items.reduce((sum, item) => sum + item.quantity, 0);
this.totalAmount = this.items.reduce((sum, item) => sum + item.subtotal, 0);
this.discount = this.totalAmount > 5000 ? Math.floor(this.totalAmount * 0.1) : 0;
AppStorage.SetOrCreate('cartTotal', this.totalAmount);
AppStorage.SetOrCreate('cartCount', this.itemCount);
}
updateQuantity(index: number, delta: number): void {
const newQty = this.items[index].quantity + delta;
if (newQty < 1) return;
this.items[index].quantity = newQty;
this.items = [...this.items]; // 触发 @State 检测
}
build() {
Column({ space: 12 }) {
Text('购物车').fontSize(22).fontWeight(FontWeight.Bold)
List() {
ForEach(this.items, (item: CartItem, index: number) => {
ListItem() {
Row({ space: 12 }) {
Text(item.name).fontSize(16).layoutWeight(1)
Row({ space: 4 }) {
Button('-').fontSize(14).onClick(() => this.updateQuantity(index, -1))
Text(`${item.quantity}`).fontSize(16).width(30).textAlign(TextAlign.Center)
Button('+').fontSize(14).onClick(() => this.updateQuantity(index, 1))
}
Text(`¥${item.subtotal}`).fontSize(16).fontWeight(FontWeight.Bold)
.width(80).textAlign(TextAlign.End)
}
.padding(12).width('100%')
}
}, (item: CartItem) => item.sku)
}
.layoutWeight(1).width('100%')
Column({ space: 6 }) {
Row() { Text('商品总数:'); Text(`${this.itemCount} 件`) }
.width('100%').justifyContent(FlexAlign.SpaceBetween)
Row() { Text('商品金额:'); Text(`¥${this.totalAmount}`).fontWeight(FontWeight.Bold) }
.width('100%').justifyContent(FlexAlign.SpaceBetween)
if (this.discount > 0) {
Row() { Text('满减优惠:'); Text(`-¥${this.discount}`).fontColor(Color.Red) }
.width('100%').justifyContent(FlexAlign.SpaceBetween)
}
Divider()
Row() { Text('实付金额:'); Text(`¥${this.totalAmount - this.discount}`)
.fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.Red) }
.width('100%').justifyContent(FlexAlign.SpaceBetween)
}
.padding(16).backgroundColor('#FFF').borderRadius(8)
.shadow({ radius: 2, color: '#08000000' }).width('100%')
}
.padding(24).width('100%').height('100%').backgroundColor('#F5F5F5')
}
}
@Observed
class CartItem {
sku: string;
name: string;
price: number;
quantity: number;
constructor(sku: string, name: string, price: number, qty: number = 1) {
this.sku = sku; this.name = name; this.price = price; this.quantity = qty;
}
get subtotal(): number { return this.price * this.quantity; }
}
注意 updateQuantity 方法中的 this.items = [...this.items]——因为修改对象内部属性不会触发 @State,所以需要创建新数组来强制引用比较。
小结:@Watch 把"状态变化"和"后续行为"解耦。配合防抖模式,可以优雅地处理搜索、数据同步等场景。
八、完整实战:构建多端状态管理架构
现在我们把所有知识整合起来,构建一个可复用的多端状态管理框架。包含三个核心层。
8.1 StateCenter:类型安全的状态中心
// state/StateCenter.ets
/**
* 状态键常量 —— 集中管理所有全局状态的键
*/
export const StateKeys = {
USER_ID: 'global_userId',
USER_NAME: 'global_userName',
USER_TOKEN: 'global_userToken',
IS_LOGGED_IN: 'global_isLoggedIn',
THEME_MODE: 'global_themeMode',
DEVICE_TYPE: 'global_deviceType',
IS_FOLDABLE: 'global_isFoldable',
SCREEN_WIDTH: 'global_screenWidth',
SCREEN_HEIGHT: 'global_screenHeight',
LAST_SYNC_TIME: 'global_lastSyncTime',
LANGUAGE: 'global_language',
} as const;
/**
* 状态中心 —— 封装 AppStorage
*/
export class StateCenter {
private static instance: StateCenter;
private constructor() {
this.initializeDefaults();
}
static getInstance(): StateCenter {
if (!StateCenter.instance) {
StateCenter.instance = new StateCenter();
}
return StateCenter.instance;
}
private initializeDefaults(): void {
this.setIfAbsent(StateKeys.USER_ID, '');
this.setIfAbsent(StateKeys.USER_NAME, '');
this.setIfAbsent(StateKeys.USER_TOKEN, '');
this.setIfAbsent(StateKeys.IS_LOGGED_IN, false);
this.setIfAbsent(StateKeys.THEME_MODE, 'light');
this.setIfAbsent(StateKeys.DEVICE_TYPE, 'phone');
this.setIfAbsent(StateKeys.IS_FOLDABLE, false);
this.setIfAbsent(StateKeys.SCREEN_WIDTH, 0);
this.setIfAbsent(StateKeys.SCREEN_HEIGHT, 0);
this.setIfAbsent(StateKeys.LAST_SYNC_TIME, 0);
this.setIfAbsent(StateKeys.LANGUAGE, 'zh-CN');
}
private setIfAbsent(key: string, value: Object): void {
if (AppStorage.Get(key) === undefined) {
AppStorage.SetOrCreate(key, value);
}
}
get<T>(key: string): T | undefined {
return AppStorage.Get<T>(key);
}
set<T>(key: string, value: T): void {
AppStorage.SetOrCreate(key, value);
}
batchUpdate(updates: Record<string, Object>): void {
for (const [key, value] of Object.entries(updates)) {
AppStorage.SetOrCreate(key, value);
}
}
clearUserState(): void {
this.set(StateKeys.USER_ID, '');
this.set(StateKeys.USER_NAME, '');
this.set(StateKeys.USER_TOKEN, '');
this.set(StateKeys.IS_LOGGED_IN, false);
}
captureDeviceInfo(deviceType: string, width: number, height: number): void {
this.batchUpdate({
[StateKeys.DEVICE_TYPE]: deviceType,
[StateKeys.SCREEN_WIDTH]: width,
[StateKeys.SCREEN_HEIGHT]: height,
[StateKeys.IS_FOLDABLE]: width > 600 && width < 1200,
});
}
}
export const stateCenter = StateCenter.getInstance();
8.2 EventBus:增强版事件总线
// state/EventBus.ets
import { common } from '@kit.AbilityKit';
type Handler = (...args: Object[]) => void;
export class CrossDeviceEventBus {
private static instance: CrossDeviceEventBus;
private context?: common.UIAbilityContext;
private localHandlers: Map<string, Set<Handler>> = new Map();
private constructor() {}
static getInstance(): CrossDeviceEventBus {
if (!CrossDeviceEventBus.instance) {
CrossDeviceEventBus.instance = new CrossDeviceEventBus();
}
return CrossDeviceEventBus.instance;
}
init(context: common.UIAbilityContext): void {
this.context = context;
}
emit(event: string, ...args: Object[]): void {
// 通知本地订阅者
const localSet = this.localHandlers.get(event);
if (localSet) {
localSet.forEach(h => { try { h(...args); } catch (e) {
console.error(`[EventBus] handler error: ${JSON.stringify(e)}`);
}});
}
// 通过 EventHub 通知跨 Ability 订阅者
if (this.context) {
this.context.eventHub.emit(event, ...args);
}
// 记录事件时间戳到 AppStorage
AppStorage.SetOrCreate(`_event_${event}_lastTime`, Date.now());
}
on(event: string, handler: Handler): void {
if (!this.localHandlers.has(event)) {
this.localHandlers.set(event, new Set());
}
this.localHandlers.get(event)!.add(handler);
if (this.context) {
this.context.eventHub.on(event, handler);
}
}
off(event: string, handler?: Handler): void {
if (handler) {
this.localHandlers.get(event)?.delete(handler);
if (this.context) this.context.eventHub.off(event, handler);
} else {
this.localHandlers.delete(event);
if (this.context) this.context.eventHub.off(event);
}
}
once(event: string, handler: Handler): void {
const wrapper: Handler = (...args: Object[]) => {
handler(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
export const CrossEvents = {
USER_LOGGED_IN: 'cross:user:loggedIn',
USER_LOGGED_OUT: 'cross:user:loggedOut',
THEME_CHANGED: 'cross:ui:themeChanged',
DATA_SYNC_COMPLETED: 'cross:sync:completed',
DEVICE_FOLDED: 'cross:device:folded',
DEVICE_UNFOLDED: 'cross:device:unfolded',
DEVICE_ORIENTATION_CHANGED: 'cross:device:orientation',
APP_STATE_FOREGROUND: 'cross:app:foreground',
APP_STATE_BACKGROUND: 'cross:app:background',
} as const;
export const crossEventBus = CrossDeviceEventBus.getInstance();
8.3 StateSyncManager:智能同步策略引擎
// state/StateSyncManager.ets
import { StateKeys, stateCenter } from './StateCenter';
import { CrossEvents, crossEventBus } from './EventBus';
interface SyncStrategy {
mode: 'auto' | 'manual' | 'debounced';
interval?: number;
debounceDelay?: number;
}
export class StateSyncManager {
private static instance: StateSyncManager;
private syncTimer: number | null = null;
private debounceTimer: number | null = null;
private pendingChanges: Map<string, Object> = new Map();
private strategies: Map<string, SyncStrategy> = new Map([
[StateKeys.USER_ID, { mode: 'auto', interval: 30000 }],
[StateKeys.USER_NAME, { mode: 'auto', interval: 30000 }],
[StateKeys.IS_LOGGED_IN, { mode: 'auto', interval: 5000 }],
[StateKeys.THEME_MODE, { mode: 'debounced', debounceDelay: 1000 }],
[StateKeys.LANGUAGE, { mode: 'debounced', debounceDelay: 2000 }],
]);
private constructor() {}
static getInstance(): StateSyncManager {
if (!StateSyncManager.instance) {
StateSyncManager.instance = new StateSyncManager();
}
return StateSyncManager.instance;
}
start(): void {
console.info('[SyncManager] 同步引擎已启动');
crossEventBus.on(CrossEvents.USER_LOGGED_IN, () => this.syncImmediately('user'));
crossEventBus.on(CrossEvents.THEME_CHANGED, () => this.syncImmediately('theme'));
this.startAutoSync();
}
stop(): void {
if (this.syncTimer !== null) { clearInterval(this.syncTimer); this.syncTimer = null; }
if (this.debounceTimer !== null) { clearTimeout(this.debounceTimer); this.debounceTimer = null; }
}
markDirty(key: string, value: Object): void {
const strategy = this.strategies.get(key);
switch (strategy?.mode) {
case 'debounced':
this.debounceSync(key, value, strategy.debounceDelay ?? 1000);
break;
default:
this.pendingChanges.set(key, value);
break;
}
}
syncImmediately(scope?: string): void {
if (scope) {
const keys: string[] = [];
for (const k of this.strategies.keys()) {
if (k.includes(scope)) keys.push(k);
}
this.doBatchSync(keys);
} else {
this.flushPendingChanges();
}
}
private startAutoSync(): void {
this.syncTimer = setInterval(() => {
const keys: string[] = [];
for (const [k, s] of this.strategies.entries()) {
if (s.mode === 'auto') keys.push(k);
}
if (keys.length > 0) this.doBatchSync(keys);
}, 60000);
}
private debounceSync(key: string, value: Object, delay: number): void {
this.pendingChanges.set(key, value);
if (this.debounceTimer !== null) clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => this.flushPendingChanges(), delay);
}
private flushPendingChanges(): void {
if (this.pendingChanges.size === 0) return;
const keys = Array.from(this.pendingChanges.keys());
this.pendingChanges.clear();
this.doBatchSync(keys);
}
private doBatchSync(keys: string[]): void {
if (keys.length === 0) return;
const syncData: Record<string, Object> = {};
for (const key of keys) {
const val = stateCenter.get<Object>(key);
if (val !== undefined) syncData[key] = val;
}
if (Object.keys(syncData).length === 0) return;
console.info(`[SyncManager] 批量同步: ${JSON.stringify(syncData)}`);
// 此处替换为实际同步实现
try {
stateCenter.set(StateKeys.LAST_SYNC_TIME, Date.now());
crossEventBus.emit(CrossEvents.DATA_SYNC_COMPLETED, {
syncedKeys: keys, timestamp: Date.now()
});
} catch (error) {
console.error(`[SyncManager] 同步失败: ${JSON.stringify(error)}`);
}
}
}
export const syncManager = StateSyncManager.getInstance();
8.4 Ability 层初始化
// EntryAbility.ets(简化版)
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { stateCenter } from './state/StateCenter';
import { crossEventBus, CrossEvents } from './state/EventBus';
import { syncManager } from './state/StateSyncManager';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// 1. 初始化事件总线
crossEventBus.init(this.context);
// 2. 状态中心自动初始化默认值
stateCenter;
// 3. 启动同步引擎
syncManager.start();
// 4. 采集设备信息
windowStage.getMainWindow()?.getWindowProperties().then(props => {
const { width, height } = props.windowRect;
let deviceType = 'phone';
if (width >= 1440 && height >= 900) deviceType = 'pc';
else if (width >= 1000 && height >= 700) deviceType = 'tablet';
else if (width >= 400) deviceType = 'phone';
else deviceType = 'wearable';
stateCenter.captureDeviceInfo(deviceType, width, height);
crossEventBus.emit(CrossEvents.DEVICE_ORIENTATION_CHANGED, {
deviceType, width, height
});
});
windowStage.loadContent('pages/Index', (err) => {
if (err.code) console.error(`加载失败: ${err.message}`);
});
}
onForeground(): void {
crossEventBus.emit(CrossEvents.APP_STATE_FOREGROUND, { timestamp: Date.now() });
}
onBackground(): void {
crossEventBus.emit(CrossEvents.APP_STATE_BACKGROUND, { timestamp: Date.now() });
syncManager.syncImmediately();
}
onDestroy(): void {
syncManager.stop();
}
}
8.5 在 UI 组件中使用完整架构
// pages/MultiDeviceHome.ets
import { stateCenter, StateKeys } from '../state/StateCenter';
import { crossEventBus, CrossEvents } from '../state/EventBus';
@Entry
@Component
struct MultiDeviceHome {
@StorageLink(StateKeys.IS_LOGGED_IN) isLoggedIn: boolean = false;
@StorageLink(StateKeys.USER_NAME) userName: string = '';
@StorageLink(StateKeys.THEME_MODE) themeMode: string = 'light';
@StorageLink(StateKeys.DEVICE_TYPE) deviceType: string = 'phone';
@State greeting: string = '';
aboutToAppear(): void {
this.updateGreeting();
crossEventBus.on(CrossEvents.THEME_CHANGED, () => {
this.themeMode = stateCenter.getOrDefault(StateKeys.THEME_MODE, 'light');
});
crossEventBus.on(CrossEvents.USER_LOGGED_IN, () => {
this.isLoggedIn = true;
this.userName = stateCenter.getOrDefault(StateKeys.USER_NAME, '');
this.updateGreeting();
});
}
aboutToDisappear(): void {
crossEventBus.off(CrossEvents.THEME_CHANGED);
crossEventBus.off(CrossEvents.USER_LOGGED_IN);
}
updateGreeting(): void {
const h = new Date().getHours();
if (h < 6) this.greeting = '夜深了';
else if (h < 9) this.greeting = '早上好';
else if (h < 12) this.greeting = '上午好';
else if (h < 14) this.greeting = '中午好';
else if (h < 18) this.greeting = '下午好';
else this.greeting = '晚上好';
}
toggleTheme(): void {
const newTheme = this.themeMode === 'light' ? 'dark' : 'light';
stateCenter.set(StateKeys.THEME_MODE, newTheme);
crossEventBus.emit(CrossEvents.THEME_CHANGED, { theme: newTheme });
}
simulateLogin(): void {
stateCenter.batchUpdate({
[StateKeys.USER_ID]: 'U99999',
[StateKeys.USER_NAME]: '跨端用户',
[StateKeys.IS_LOGGED_IN]: true,
});
crossEventBus.emit(CrossEvents.USER_LOGGED_IN, { userId: 'U99999' });
}
simulateLogout(): void {
stateCenter.clearUserState();
crossEventBus.emit(CrossEvents.USER_LOGGED_OUT, { timestamp: Date.now() });
}
build() {
Column({ space: 16 }) {
Text(`当前设备: ${{
'phone': '📱 手机',
'tablet': '📟 平板',
'pc': '🖥️ PC',
'wearable': '⌚ 手表'
}[this.deviceType] || this.deviceType}`)
.fontSize(14).fontColor(Color.Gray)
.padding(4).backgroundColor('#20000000').borderRadius(4)
Text(this.isLoggedIn ?
`${this.greeting},${this.userName}` :
`${this.greeting},访客`)
.fontSize(this.deviceType === 'phone' ? 24 : 32)
.fontWeight(FontWeight.Bold)
Divider()
if (this.isLoggedIn) {
Row({ space: 12 }) {
Button('切换主题').onClick(() => this.toggleTheme())
Button('退出登录').backgroundColor(Color.Red).onClick(() => this.simulateLogout())
}
} else {
Button('模拟登录').onClick(() => this.simulateLogin())
}
Divider()
ThemeAwareCard()
}
.padding(this.deviceType === 'phone' ? 24 : 48)
.width('100%').height('100%')
.backgroundColor(this.themeMode === 'light' ? '#F5F5F5' : '#1A1A2E')
}
}
@Component
struct ThemeAwareCard {
@StorageProp(StateKeys.THEME_MODE) themeMode: string = 'light';
@StorageProp(StateKeys.LAST_SYNC_TIME) lastSync: number = 0;
build() {
Column({ space: 8 }) {
Text('状态卡片(自动响应全局变化)').fontSize(14).fontWeight(FontWeight.Medium)
Text(`主题: ${this.themeMode}`).fontSize(13)
Text(`同步时间: ${this.lastSync > 0 ? new Date(this.lastSync).toLocaleTimeString() : '未同步'}`)
.fontSize(13).fontColor(Color.Gray)
Text('💡 其他页面状态变化时,此组件自动更新')
.fontSize(12).fontColor(Color.Gray).textAlign(TextAlign.Center).width('100%')
}
.padding(16)
.backgroundColor(this.themeMode === 'light' ? '#FFF' : '#2A2A3E')
.borderRadius(8).shadow({ radius: 2, color: '#10000000' }).width('100%')
}
}
8.6 架构总览
┌─────────────────────────────────────────────────────────┐
│ StateCenter │
│ 类型安全封装 AppStorage → 全局状态统一读写 │
│ get<T> / set<T> / batchUpdate / clearUserState │
├─────────────────────────────────────────────────────────┤
│ EventBus │
│ 封装 EventHub → 跨组件/跨 Ability 事件通信 │
│ emit / on / off / once + 预定义事件常量 │
├─────────────────────────────────────────────────────────┤
│ StateSyncManager │
│ 同步策略引擎 → auto / debounced / manual │
│ markDirty / syncImmediately / flushPendingChanges │
├─────────────────────────────────────────────────────────┤
│ UI 组件(各页面/各设备) │
│ @StorageLink → 自动绑定全局状态 │
│ EventBus.on → 响应事件通知 │
│ StateCenter.set → 触发状态变更 │
└─────────────────────────────────────────────────────────┘
这套架构的关键收益:
- 类型安全:StateCenter 和 EventBus 提供类型化 API,避免魔法字符串错误。
- 职责清晰:存储层只管数据,通信层只管事件,同步层只管策略。
- 多端适配:通过 DEVICE_TYPE 状态值,UI 组件自动适应不同屏幕尺寸。
- 可测试:每个模块都是单例且有明确接口边界,方便单元测试。
九、总结
从 @State/@Prop/@Link 的组件内通信,到 @Provide/@Consume 的跨层穿透,再到 AppStorage/LocalStorage 的全局存储,最后到 EventHub 事件总线和 StateSyncManager 同步引擎——ArkUI 的状态管理能力覆盖了从"单一组件"到"多设备协作"的完整光谱。
核心设计原则回顾:
- 最小作用域:状态的影响范围越小,代码越容易维护。
- 单向数据流优先:@Prop 优先于 @Link,可预测的数据流比"方便"更重要。
- 事件解耦:组件之间通过 EventBus 发送事件,按需订阅,自由拆分。
- Watch 做副作用:@Watch 适合做数据持久化、网络请求,不适合做 UI 逻辑。
- 多端感知:在状态中心统一记录设备信息,UI 组件自适应渲染。
本文给出的 StateCenter + EventBus + StateSyncManager 架构已经在鸿蒙多窗模式、折叠屏适配、元服务跨 Ability 同步等场景中验证过。你可以直接拷贝代码,放入自己的 DevEco Studio 项目中按需调整。
更多推荐


所有评论(0)