一、详解声明式UI

1、概念

声明式UI是一种用“描述结果”而不是“写步骤”的方式来构建用户界面的方法。

比如做菜🍳

命令式

  • 1、开火
  • 2、倒油
  • 3、等油热
  • 4、放菜
  • 5、翻炒5分钟
  • 6、放盐
  • 7、盛出来

需要一步步指挥每个动作

声明式

  • 我要一份炒好的青菜(盐适中,熟了但没焦)

只告诉厨房最终状态,具体步骤由厨房自己搞定

2、举个例子

需要实现的功能是:点击按钮数字+1,当数字≥10时按钮变红并禁用

命令式UI(Android Java)实现

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="20dp">

    <TextView
        android:id="@+id/tvCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="40sp"
        android:layout_marginBottom="30dp"/>

    <Button
        android:id="@+id/btnAdd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击+1"
        android:textSize="18sp"
        android:padding="15dp"/>

</LinearLayout>

Activity文件

public class MainActivity extends AppCompatActivity {
    private TextView tvCount;
    private Button btnAdd;
    private int count = 0;  // 状态变量
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 1. 手动找到视图组件
        tvCount = findViewById(R.id.tvCount);
        btnAdd = findViewById(R.id.btnAdd);
        
        // 2. 设置初始状态
        tvCount.setText("0");
        btnAdd.setEnabled(true);
        btnAdd.setBackgroundColor(Color.BLUE);
        
        // 3. 手动设置点击事件
        btnAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 4. 更新数据
                count++;
                
                // 5. 手动更新UI(必须记住所有要修改的地方)
                // 更新数字显示
                tvCount.setText(String.valueOf(count));
                
                // 根据条件修改按钮状态
                if (count >= 10) {
                    // 手动禁用按钮
                    btnAdd.setEnabled(false);
                    // 手动改变按钮颜色
                    btnAdd.setBackgroundColor(Color.RED);
                    // 手动改变按钮文字
                    btnAdd.setText("已达上限");
                } else {
                    // 手动保持按钮可用
                    btnAdd.setEnabled(true);
                    btnAdd.setBackgroundColor(Color.BLUE);
                    btnAdd.setText("点击+1");
                }
                
                // 6. 可能需要手动重绘
                tvCount.invalidate();
                btnAdd.invalidate();
            }
        });
    }
}

声明式UI(鸿蒙 ArkTS)实现

@Entry
@Component
struct CounterPage {
  // 1. 定义状态变量(数据源)
  @State count: number = 0
  
  build() {
    Column({ space: 20 }) {
      // 2. 数字显示 - 直接绑定状态
      Text(this.count.toString())
        .fontSize(40)
        .fontColor(Color.Black)
        .margin({ bottom: 30 })
      
      // 3. 按钮 - 根据状态自动更新
      Button(this.getButtonText())  // 按钮文字自动随状态变化
        .fontSize(18)
        .padding(15)
        .backgroundColor(this.getButtonColor())  // 颜色自动随状态变化
        .enabled(this.count < 10)  // 是否可用自动随状态变化
        .onClick(() => {
          // 4. 只需要更新状态,UI会自动更新
          this.count++
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
  
  // 5. 计算属性:根据状态返回按钮文字
  private getButtonText(): string {
    return this.count >= 10 ? '已达上限' : '点击+1'
  }
  
  // 6. 计算属性:根据状态返回按钮颜色
  private getButtonColor(): ResourceColor {
    return this.count >= 10 ? Color.Red : Color.Blue
  }
}

核心区别

  • 命令式(Android Java):像厨师,要自己控制每个步骤
// 你像厨师,要自己控制每个步骤:
btnAdd.setOnClickListener(v -> {
    // 1. 改数据
    count++;
    
    // 2. 改显示
    tvCount.setText(String.valueOf(count));
    
    // 3. 改按钮可用性
    if (count >= 10) {
        btnAdd.setEnabled(false);
    }
    
    // 4. 改按钮颜色
    if (count >= 10) {
        btnAdd.setBackgroundColor(Color.RED);
    } else {
        btnAdd.setBackgroundColor(Color.BLUE);
    }
    
    // 5. 改按钮文字
    btnAdd.setText(count >= 10 ? "已达上限" : "点击+1");
});
  • 声明式(鸿蒙 ArkTs):点菜就好
// 你像点餐,只要描述想要什么:
Button(this.getButtonText())  // "我要显示这个文字"
  .backgroundColor(this.getButtonColor())  // "我要这个颜色"
  .enabled(this.count < 10)  // "我要这个可用状态"
  .onClick(() => {
    // 只改数据,UI会自动更新!
    this.count++
  })

// UI会根据count值自动变成对应的样子

3、差异总结

方面 命令式( Android Java) 声明式(鸿蒙 ArkTS)
思维方式 “怎么做”:一步步操作UI “是什么”:描述UI应有的状态
数据与UI关系 数据改变 → 手动更新UI 数据改变 → UI自动重新渲染
代码量 多,每次改状态都要操作UI 少,只需改数据
维护性 容易出错,可能遗漏更新 不容易出错,状态与UI自动同步
典型代码 findViewById() + setText() @State + 数据绑定

二、装饰器深度解析

ArkTs的基本组成如下图:
在这里插入图片描述

  • 装饰器:用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述:@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新
  • UI描述:以声明式的方式来描述UI的结构,例如build()方法中的代码块。
  • 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
  • 系统组件:ArkUI框架中默认内置的基础和容器组件,可以直接调用,例如示例中的Column、Text、Divider、Button
  • 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()。
  • 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。

除此之外,ArkTS扩展了多种语法范式来使开发更加便捷,都会在接下来的文章中详细介绍。

  • @Builder/@BuilderParam:特殊的封装UI描述的方法,细粒度的封装和复用UI描述。

  • @Extend/@Styles:扩展系统组件和封装属性样式,更灵活地组合系统组件。

  • stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。

1、装饰器基础概念

在鸿蒙(HarmonyOS)ArkTS开发中,装饰器是一种特殊语法,用于修饰类、属性或方法,以扩展其功能或赋予特定行为。
对,就是上篇博文说的:装饰器就是 “不拆房子,只搞装修” 🏠 → 🏡

2、内置装饰器分类

1)类装饰器

  • @Entry - 入口组件,标记为页面入口组件,一个模块只能有一个@Entry
@Entry  // 标记为应用入口
@Component
struct IndexPage {
  build() {
    // ...
  }
}
  • @Component:用于标记自定义组件,允许开发者通过声明式UI描述页面布局,用于定义可复用的UI组件
@Component
struct MyComponent {
  // 组件内部状态
  @State count: number = 0
  
  build() {
    // 构建UI
  }
}
  • @Observed:装饰class。需要放在class的定义前,使用new创建类对象。

2)状态装饰器V1

  • @State:组件内部私有状态,变化时触发当前组件重新渲染,支持:number、string、boolean、class、Array等
@Component
struct Counter {
  @State count: number = 0  // 变化时触发UI更新
  
  build() {
    Text(this.count.toString())  // 自动同步
      .onClick(() => {
        this.count++  // 只需改数据,UI自动更新!
      })
  }
}
  • @Prop - 单向数据流,父 → 子的单向传递,子组件不能修改@Prop变量,父组件@State变化时,@Prop自动更新
// 父组件
@Component
struct Parent {
  @State parentCount: number = 0
  
  build() {
    Column() {
      Child({ count: this.parentCount })  // 传递数据
    }
  }
}

// 子组件
@Component
struct Child {
  @Prop count: number  // 接收父组件数据
  
  build() {
    Text(`来自父组件: ${this.count}`)
    // this.count = 10  // ❌ 错误!@Prop不可修改
  }
}
  • @Link - 双向数据绑定:父子组件数据同步,子组件可以修改,父组件自动更新
// 父组件
@Component
struct Parent {
  @State parentCount: number = 0
  
  build() {
    Column() {
      Text(`父组件: ${this.parentCount}`)
      Child({ count: this.parentCount })
    }
  }
}

// 子组件
@Component
struct Child {
  @Link count: number  // 双向绑定

  build() {
    Button(`子组件+1 count = ${this.count}`)
      .onClick(() => {
        this.count++  // ✅ 可以修改!会同步到父组件
      })
  }
}
  • @Provide/@Consume - 跨层级传递:避免逐层传递
// 祖先组件
@Component
struct Ancestor {
  @Provide name: string = '夏小鱼'  // 提供数据
  
  build() {
    Column() {
      Parent()
    }
  }
}

// 中间组件(不需要接收)
@Component
struct Parent {
  build() {
    Column() {
      Child()
    }
  }
}

// 后代组件
@Component
struct Child {
  @Consume name: string  // 直接获取祖先数据
  
  build() {
    Text(this.name)
  }
}
  • @Watch - 状态监听:状态变化时执行回调
@Entry
@Component
struct WatchDemo {
  @State @Watch('onCountChange')count: number = 0
  @State total: number = 0

  // 监听count变化
  onCountChange() {
    this.total += this.count
    console.log(`count变为: ${this.count}, total: ${this.total}`)
  }

  build() {
    Button(`点击 ${this.count},total ${this.total}`)
      .onClick(() => {
        this.count++
      })
  }
}
  • @ObjectLink - 对象属性监听
@Observed
class User {
  name: string = '';
  age: number = 0;

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

@Component
struct UserComponent{
  @ObjectLink user: User  // 监听对象内部属性
  build() {
    Text(this.user.name)
  }
}

@Component
struct UserInfo {
  @State user:User = new User('夏小鱼',18);

  build() {
    Column() {
      UserComponent({user:this.user})
      Button('改名')
        .onClick(() => {
          this.user.name = '夏小七'  // 触发UI更新
        })
    }
  }
}

3)方法装饰器

  • @Builder装饰器:自定义构建函数:用于封装可复用的UI结构,通过提取重复的布局代码提高开发效率
 @Builder
  buildContent(){
    Text('动态内容').width('100%')
    Button('按钮').width('100%')
  }
  • @BuilderParam - 动态UI:@BuilderParam装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化。
@Component
struct Container {
  @BuilderParam content: () => void // 接收UI片段

  build() {
    Column({space:10}) {
      this.content() // 渲染传入的UI
    }.width('100%').height('120')
  }
}

@Entry
@Component
struct IndexPage {
  build() {
    Column() {
      Container({
        content: () => {
         this.buildContent();
        }
      })
    }
  }

  @Builder
  buildContent(){
    Text('动态内容').width('100%')
    Button('按钮').width('100%')
  }
}
  • @Styles装饰器:定义组件重用样式
@Entry
@Component
struct IndexPage {

  @State heightValue: number = 50;

  @Styles
  fancy() {
    .height(this.heightValue)
    .backgroundColor(Color.Blue)
    .onClick(() => {
      this.heightValue = 100;
    })
  }

  build() {
    Column() {
      Button('change height')
        .fancy()
    }
    .height('100%')
    .width('100%')
  }
}
  • @Extend装饰器:定义扩展组件样式,支持封装指定组件的私有属性、私有事件和自身定义的全局方法。
// superFancyText可以调用预定义的fancy
@Extend(Text)
function superFancyText(size: number) {
  .fontSize(size)
  .fancy()
}

@Entry
@Component
struct IndexPage {
  build() {
    Row({ space: 10 }) {
      Text('Fancy')
        .superFancyText(16)
      Text('Fancy')
        .superFancyText(24)
    }
  }
}
  • stateStyles:多态样式
 Button('Button1')
        .focusOnTouch(true)
        .stateStyles({
          focused: {
            .backgroundColor('#ffffeef0')
          },
          pressed: {
            .backgroundColor('#ff707070')
          },
          normal: {
            .backgroundColor('#ff2787d9')
          }
        })
        .margin(20)

三、异步编程全面掌握

1、并发

并发是指系统在同一时间内处理多个任务的能力。在多核设备上,不同任务可以真正并行地在多个CPU上执行;而在单核设备上,CPU会通过任务切换机制,在任务处于等待状态(如I/O操作)时执行其他任务,从而最大化CPU利用率。

为保障应用流畅性,避免耗时任务阻塞UI主线程,ArkTS提供了两种并发处理方案:

异步并发

  • 采用非阻塞执行模式:任务可暂停并在适当时机恢复
  • 单线程执行:同一时刻仅运行一个任务片段
  • 实现方式:基于Promise和async/await语法
  • 适用场景:单次I/O密集型操作

多线程并发

  • 真正并行执行:多个任务片段同步运行
  • 线程分工:主线程负责UI响应,后台线程处理耗时操作
  • 实现方式:通过TaskPool和Worker机制
  • 适用场景:计算密集型或长时间运行任务

2、异步并发 (Promise和async/await)

Promise和async/await是标准的JS异步语法,提供异步并发能力。异步代码执行时会被挂起,稍后继续执行,确保同一时间只有一段代码在运行

典型的异步并发使用场景:

  • I/O 非阻塞操作:网络请求、文件读写、定时器等。
  • 任务轻量且无 CPU 阻塞:单次任务执行时间短。
  • 逻辑依赖清晰:任务有明确的顺序或并行关系。

异步并发是一种编程语言的特性,允许程序在执行某些操作时不必等待其完成,可以继续执行其他异步代码。

1)Promise

简单来说,Promise 就是一个表示“将来可能完成的事情”的对象,它有三种最终状态:成功、失败、无论成功失败都要做的事

我们可以用三个方法来安排对应的事情:

  1. .then(成功时做什么)

    • 只传一个函数 → 处理成功的情况。
    • 传两个函数 .then(成功函数, 失败函数) → 同时处理成功和失败。
  2. .catch(失败时做什么)

    • 专门处理失败或出错的情况(比如网络错误、数据错误等)。
  3. .finally(最后总是做什么)

    • 不管成功还是失败,最后都会执行,适合做清理工作(比如关闭加载动画)。

比喻
就像你点外卖:

  • .then = 外卖到了,开心吃饭。
  • .catch = 外卖送错了或丢了,联系客服处理。
  • .finally = 不管外卖到没到,你都把桌上的碗筷摆好/收走。

举个例子

const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
  setTimeout(() => {
    const randomNumber: number = Math.random();
    if (randomNumber > 0.5) {
      resolve(randomNumber);
    } else {
      reject(new Error('Random number is too small'));
    }
  }, 1000);
})
  • const promise: Promise
    创建一个名为 promise 的常量,它的类型是 Promise,意味着这个 Promise 在成功时会得到一个数字(number)。

  • new Promise((resolve, reject) => { … })
    创建 Promise 对象,需要传入一个函数(执行器)。这个函数会立即执行,它有两个参数:

resolve:调用后会把 Promise 状态变为 成功(fulfilled)
reject:调用后会把 Promise 状态变为 失败(rejected)

  • setTimeout(() => { … }, 1000)
    延迟 1 秒执行里面的代码,模拟异步操作(比如网络请求)
  • const randomNumber = Math.random()
    生成一个 0 到 1 之间的随机数。
  • if (randomNumber > 0.5)

如果随机数大于 0.5 → 调用 resolve(randomNumber),Promise 成功,把随机数传递出去。
如果随机数小于等于 0.5 → 调用 reject(new Error(…)),Promise 失败,抛出一个错误。

使用:

promise
  .then(num => {
    console.log("成功,数字是:", num);
  })
  .catch(err => {
    console.error("失败了:", err.message);
  })
  .finally(() => {
    console.log("无论成功失败,我都执行");
  });

2)async/await

简单来说,async/await 是 Promise 的“舒适版写法”,让你写异步代码像写同步代码一样简单清晰。

比喻说明

想象一下,以前用 .then 处理异步就像是:

  1. 点外卖
  2. 然后(等外卖到了)吃饭
  3. 然后(吃完后)洗碗

async/await 后,你可以这样写代码:

async function 吃饭流程() {
  let 外卖 = await 点外卖();   // 等外卖到了才继续
  (外卖);                     // 吃饭
  await 洗碗();                 // 等洗完碗
}

代码看起来就像是一步一步执行的,但其实 await 后面的等待(比如等外卖、等洗碗)并不会阻塞整个程序。

关键点

  1. async 函数

    • 函数前面加 async,这个函数永远返回 Promise
  2. await 关键字

    • 只能在 async 函数里面用。
    • await 后面跟一个 Promise,它会“暂停”在这里,等到这个 Promise 出结果(成功或失败)后才继续往下执行
    • 如果 Promise 成功 → await 返回成功的值。
    • 如果 Promise 失败 → await 会抛出错误,需要用 try...catch 捕获。
  3. 错误处理

    • 可以用 try { ... } catch (err) { ... } 包裹 await,来捕获错误,就像处理同步代码错误一样。

例子对比

Promise 写法

function 获取数据() {
  return fetch('url')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.error('出错:', err));
}

async/await 写法

async function 获取数据() {
  try {
    const response = await fetch('url');
    const data = await response.json();
    console.log(data);
  } catch (err) {
    console.error('出错:', err);
  }
}

一句话总结

async/await 就是:

  • 用同步的写法,写异步的逻辑
  • 等 Promise 结果时,不阻塞程序其他部分
  • try-catch 捕获错误,更直观

3、多线程并发

简单来说,ArkTS(鸿蒙应用开发语言)提供了两种处理“耗时任务不卡界面”的方式:TaskPool 和 Worker,它们都是为了让你的App更流畅。


比喻说明

想象你的App是一个餐厅

  • 主线程 = 前台服务员(直接跟顾客打交道)
  • 耗时任务 = 做复杂菜品、处理大量食材

问题:如果服务员自己去做菜,前台就没人接待顾客了,餐厅会显得“卡住”。

解决方案

  1. 请专门的厨师(Worker)

    • 固定雇佣一个厨师专门做某类菜
    • 适合:长期稳定的后台任务(如持续采集传感器数据)
  2. 用临时帮厨团队(TaskPool)

    • 有个帮厨池,需要时叫一个临时帮厨来处理零活
    • 适合:临时的大量计算(如图片处理、文件压缩)

两种方式对比

特点 Worker(固定厨师) TaskPool(帮厨团队)
创建成本 较高(每个Worker都是独立线程) 较低(线程池共享)
适合场景 长期运行的任务
需要保持状态的任务
短期大量计算
频繁的临时任务
通信方式 通过消息传递(不会直接共享内存) 通过消息传递
生命周期 需要手动创建和关闭 系统管理,自动调度

为什么用这种模型?

  • 避免“锁”的麻烦:传统多线程要小心数据冲突,而Actor模型(消息传递)就像厨师和服务员通过“菜单纸条”沟通,不会互相干扰。
  • 不卡界面:耗时任务都交给后台线程,前台操作依然流畅。

实际应用场景

  1. 图片处理(用TaskPool)

    // 比如用户选择10张照片要加滤镜
    // 用TaskPool同时处理多张,处理完一张更新一张预览
    
  2. 持续定位(用Worker)

    // 导航App需要持续获取GPS位置
    // 用一个Worker专门监听位置变化,不干扰主界面操作
    
  3. 游戏逻辑(Worker或TaskPool)

    // 游戏中的AI计算、物理模拟等
    // 可以放在后台线程,保证画面渲染流畅
    

一句话总结

ArkTS的多线程:

  • Worker = 长期固定的后台助手
  • TaskPool = 临时的任务处理小组
  • 共同目标 = 让耗时任务在后台跑,主界面永远流畅响应

四、实践任务

1、装饰器实战

// 实现一个简单的状态管理装饰器
function MyDescriptor(target: Object, key: string, descriptor: PropertyDescriptor) {
  const originalMethod: Function = descriptor.value
  descriptor.value = (...args: Object[]) => {
    // Get the name, input parameters, and return value of the decorated method
    console.log(`Calling ${target.constructor?.name} method ${key} with argument: ${args}`)
    const result: Object = originalMethod(...args)
    console.log(`Method ${key} returned: ${result}`)
    return result
  }
  return descriptor
}

@Entry
@Component
export struct MyDescriptorCom {
  @State message: string = 'Hello World';

  @MyDescriptor
  demoFunc(str: string) {
    return str
  }

  aboutToAppear(): void {
    this.demoFunc('夏小鱼的装饰器')
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

// 运行后日志:
// Calling MyDescriptorCom method demoFunc with argument: 夏小鱼的装饰器
// Method demoFunc returned: 夏小鱼的装饰器
Logo

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

更多推荐