在这里插入图片描述

一、引言:多端部署下的状态管理挑战

在 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 拯救嵌套对象

要使对象内部属性的变化也能被观测,需要两步:

  1. @Observed 装饰 class
  2. 在引用该对象的子组件中用 @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 → 触发状态变更                        │
└─────────────────────────────────────────────────────────┘

这套架构的关键收益:

  1. 类型安全:StateCenter 和 EventBus 提供类型化 API,避免魔法字符串错误。
  2. 职责清晰:存储层只管数据,通信层只管事件,同步层只管策略。
  3. 多端适配:通过 DEVICE_TYPE 状态值,UI 组件自动适应不同屏幕尺寸。
  4. 可测试:每个模块都是单例且有明确接口边界,方便单元测试。

九、总结

从 @State/@Prop/@Link 的组件内通信,到 @Provide/@Consume 的跨层穿透,再到 AppStorage/LocalStorage 的全局存储,最后到 EventHub 事件总线和 StateSyncManager 同步引擎——ArkUI 的状态管理能力覆盖了从"单一组件"到"多设备协作"的完整光谱。

核心设计原则回顾:

  1. 最小作用域:状态的影响范围越小,代码越容易维护。
  2. 单向数据流优先:@Prop 优先于 @Link,可预测的数据流比"方便"更重要。
  3. 事件解耦:组件之间通过 EventBus 发送事件,按需订阅,自由拆分。
  4. Watch 做副作用:@Watch 适合做数据持久化、网络请求,不适合做 UI 逻辑。
  5. 多端感知:在状态中心统一记录设备信息,UI 组件自适应渲染。

本文给出的 StateCenter + EventBus + StateSyncManager 架构已经在鸿蒙多窗模式、折叠屏适配、元服务跨 Ability 同步等场景中验证过。你可以直接拷贝代码,放入自己的 DevEco Studio 项目中按需调整。

Logo

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

更多推荐