项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

目录

  1. 引言:鸿蒙ArkTS布局技术概述
  2. 技术背景与原理
    • 2.1 声明式UI编程范式
    • 2.2 组件化开发思想
    • 2.3 响应式状态管理机制
  3. @Builder装饰器详解
    • 3.1 @Builder装饰器定义与作用
    • 3.2 @Builder的核心特点
    • 3.3 @Builder的基本使用方式
    • 3.4 @Builder参数化设计
  4. @State状态管理详解
    • 4.1 @State装饰器定义与作用
    • 4.2 @State的工作原理
    • 4.3 @State的响应式刷新机制
    • 4.4 @State与其他状态装饰器的对比
  5. 参数传递机制
    • 5.1 @Builder参数传递方式
    • 5.2 状态联动与数据流
    • 5.3 参数类型与约束
  6. 完整实战案例:参数化卡片列表布局
    • 6.1 需求分析与设计
    • 6.2 完整代码实现
    • 6.3 代码逐段解析
    • 6.4 运行效果展示
  7. 常见踩坑与解决方案
    • 7.1 Card组件不存在问题
    • 7.2 borderTop属性不存在问题
    • 7.3 transparent颜色值无效问题
    • 7.4 @State数组浅监听问题
    • 7.5 链式调用语法错误问题
  8. 性能优化建议
    • 8.1 状态管理最佳实践
    • 8.2 布局复用优化策略
    • 8.3 渲染性能优化
  9. 与其他布局复用方案的对比
    • 9.1 @Builder vs @Component
    • 9.2 @Builder vs @BuilderParam
    • 9.3 选择策略与场景推荐
  10. 总结与展望

1. 引言:鸿蒙ArkTS布局技术概述

在鸿蒙HarmonyOS NEXT开发中,ArkTS作为核心开发语言,为开发者提供了强大的声明式UI编程能力。布局复用是UI开发中的核心需求,它能显著提升代码的可维护性、可复用性和开发效率。

@Builder+@State参数化复用布局是鸿蒙ArkTS中一种高效的布局复用方案,它结合了:

  • @Builder装饰器:用于定义可复用的UI布局片段
  • @State装饰器:用于管理组件内部状态,实现响应式UI刷新
  • 参数化设计:通过参数传递实现同一布局的不同样式和内容

这种布局方式特别适用于以下场景:

  • 列表项布局的复用(如商品列表、消息列表、菜单列表)
  • 卡片布局的复用(如功能卡片、信息卡片、统计卡片)
  • 表单元素布局的复用(如输入框、选择器、开关)
  • 通用UI组件的封装(如按钮组、标签页、导航栏)

本文将深入探讨@Builder+@State参数化复用布局的技术原理、使用方法、实战案例和最佳实践,帮助开发者掌握这一核心布局技术。


2. 技术背景与原理

2.1 声明式UI编程范式

声明式UI是一种现代UI编程范式,它与传统的命令式UI编程有着本质区别:

命令式UI编程:开发者需要手动操作DOM元素,通过一系列命令来实现UI的创建、更新和销毁。

// 命令式示例(伪代码)
const button = document.createElement('button');
button.textContent = '点击我';
button.style.color = 'blue';
button.addEventListener('click', () => {
  button.textContent = '已点击';
});
document.body.appendChild(button);

声明式UI编程:开发者只需描述UI的"目标状态",框架自动处理UI的创建、更新和销毁。

// 声明式示例(ArkTS)
@Entry
@Component
struct MyPage {
  @State isClicked: boolean = false;
  
  build() {
    Button(this.isClicked ? '已点击' : '点击我')
      .fontColor('#0000FF')
      .onClick(() => {
        this.isClicked = true;
      });
  }
}

声明式UI的核心优势:

  1. 代码简洁:只需描述UI的状态,无需关心DOM操作细节
  2. 可维护性高:UI与状态绑定,状态变化自动触发UI更新
  3. 可预测性强:UI始终反映当前状态,避免状态不同步问题
  4. 易于调试:状态变化可追踪,问题定位更加简单

2.2 组件化开发思想

组件化开发是将UI拆分为独立、可复用的组件单元的开发方式。在鸿蒙ArkTS中,组件化开发具有以下特点:

  1. 组件独立性:每个组件拥有独立的状态和布局
  2. 组件复用性:同一组件可在多个页面中复用
  3. 组件组合性:通过组件嵌套实现复杂UI
  4. 组件通信:通过参数传递和状态管理实现组件间通信

组件化开发的优势:

  • 提高开发效率:复用已有组件,减少重复代码
  • 降低维护成本:修改一处,多处生效
  • 提升代码质量:组件职责单一,易于测试
  • 支持团队协作:不同开发者可独立开发不同组件

2.3 响应式状态管理机制

响应式状态管理是声明式UI的核心机制,它确保UI始终与数据状态保持同步。在鸿蒙ArkTS中,状态管理通过装饰器实现:

装饰器 作用范围 数据流向 适用场景
@State 组件内部 单向 组件私有状态
@Prop 父子组件 单向 父传子,子组件不修改
@Link 父子组件 双向 父子共享状态
@Provide/@Consume 跨层级组件 双向 祖先与后代共享状态
@ObjectLink 对象属性 双向 对象属性级别的状态同步

响应式状态管理的工作原理:

  1. 状态注册:使用装饰器标记状态变量,框架自动注册
  2. 依赖收集:框架追踪哪些UI组件依赖于哪些状态变量
  3. 状态变更:当状态变量的值发生变化时,触发状态变更通知
  4. UI刷新:框架重新渲染依赖于该状态的UI组件

3. @Builder装饰器详解

3.1 @Builder装饰器定义与作用

@Builder是鸿蒙ArkTS中的装饰器,用于定义可复用的UI布局片段。它可以将一组UI组件封装为一个独立的布局函数,在多个地方重复使用。

核心作用

  1. 布局复用:将重复的UI布局封装为@Builder函数,避免代码重复
  2. 参数化设计:通过参数传递实现同一布局的不同样式和内容
  3. 代码组织:将复杂的UI拆分为多个@Builder函数,提高代码可读性
  4. 逻辑封装:将布局相关的逻辑封装在@Builder函数内部

3.2 @Builder的核心特点

3.2.1 可以接受参数

@Builder函数可以定义参数,通过参数传递实现布局的参数化:

@Builder
CustomText(text: string, color: string, fontSize: number) {
  Text(text)
    .fontColor(color)
    .fontSize(fontSize);
}
3.2.2 可以访问组件的@State状态

@Builder函数定义在组件内部时,可以直接访问组件的@State状态变量:

@Component
struct MyComponent {
  @State count: number = 0;
  
  @Builder
  CounterDisplay() {
    Text(`当前计数: ${this.count}`)
      .fontSize(20);
  }
  
  build() {
    Column() {
      this.CounterDisplay();
      Button('增加')
        .onClick(() => {
          this.count++;
        });
    }
  }
}
3.2.3 可以在build()方法中多次调用

同一个@Builder函数可以在build()方法中多次调用,实现布局复用:

@Entry
@Component
struct MyPage {
  @Builder
  SectionHeader(title: string) {
    Text(title)
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .margin({ top: 16, bottom: 8 });
  }
  
  build() {
    Column() {
      this.SectionHeader('热门推荐');
      // ... 推荐内容
      
      this.SectionHeader('最新动态');
      // ... 动态内容
      
      this.SectionHeader('我的收藏');
      // ... 收藏内容
    }
  }
}
3.2.4 支持条件渲染和循环渲染

@Builder函数内部支持条件渲染和循环渲染:

@Builder
ListBuilder(items: string[], showIndex: boolean) {
  Column() {
    ForEach(
      items,
      (item: string, index: number) => {
        if (showIndex) {
          Text(`${index + 1}. ${item}`)
            .fontSize(16);
        } else {
          Text(item)
            .fontSize(16);
        }
      }
    );
  }
}

3.3 @Builder的基本使用方式

3.3.1 定义@Builder函数
@Builder
FunctionName(parameter1: Type1, parameter2: Type2, ...) {
  // UI布局代码
}
3.3.2 调用@Builder函数
build() {
  Column() {
    this.FunctionName(value1, value2, ...);
  }
}
3.3.3 完整示例
@Entry
@Component
struct MyPage {
  @Builder
  InfoCard(title: string, content: string, icon: Resource) {
    Column({ space: 8 }) {
      Image(icon)
        .width(48)
        .height(48);
      Text(title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold);
      Text(content)
        .fontSize(14)
        .fontColor('#666666');
    }
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12);
  }
  
  build() {
    Column({ space: 12 }) {
      this.InfoCard('鸿蒙生态', '分布式能力,跨设备协同', $r('app.media.icon1'));
      this.InfoCard('ArkTS开发', 'TypeScript语法,声明式UI', $r('app.media.icon2'));
      this.InfoCard('原生体验', '高性能渲染,流畅动画', $r('app.media.icon3'));
    }
    .padding(16)
    .backgroundColor('#F5F5F5');
  }
}

3.4 @Builder参数化设计

参数化设计是@Builder的核心能力,通过参数传递实现同一布局的不同样式和内容。

3.4.1 参数类型设计

@Builder函数支持多种参数类型:

@Builder
StyledText(
  text: string,                    // 字符串参数
  fontSize: number = 16,           // 数字参数,带默认值
  color: string = '#333333',       // 字符串参数,带默认值
  fontWeight: number = FontWeight.Normal, // 枚举参数
  isItalic: boolean = false        // 布尔参数
) {
  Text(text)
    .fontSize(fontSize)
    .fontColor(color)
    .fontWeight(fontWeight)
    .fontStyle(isItalic ? FontStyle.Italic : FontStyle.Normal);
}
3.4.2 对象参数设计

当参数较多时,可以使用对象参数:

interface TextStyle {
  fontSize: number;
  color: string;
  fontWeight: number;
  isItalic: boolean;
}

@Builder
StyledTextWithObject(text: string, style: TextStyle) {
  Text(text)
    .fontSize(style.fontSize)
    .fontColor(style.color)
    .fontWeight(style.fontWeight)
    .fontStyle(style.isItalic ? FontStyle.Italic : FontStyle.Normal);
}

// 调用
this.StyledTextWithObject('Hello World', {
  fontSize: 20,
  color: '#FF5252',
  fontWeight: FontWeight.Bold,
  isItalic: true
});
3.4.3 状态参数设计

可以将@State状态作为参数传递给@Builder函数:

@Component
struct MyComponent {
  @State isSelected: boolean = false;
  @State count: number = 0;
  
  @Builder
  StatusIndicator(isActive: boolean, value: number) {
    Row({ space: 8 }) {
      Text(isActive ? '激活' : '未激活')
        .fontColor(isActive ? '#4CAF50' : '#9E9E9E');
      Text(`数值: ${value}`)
        .fontSize(14);
    }
  }
  
  build() {
    Column() {
      this.StatusIndicator(this.isSelected, this.count);
      Button('切换状态')
        .onClick(() => {
          this.isSelected = !this.isSelected;
          this.count++;
        });
    }
  }
}

4. @State状态管理详解

4.1 @State装饰器定义与作用

@State是鸿蒙ArkTS中最常用的状态管理装饰器,用于标记组件内部的状态变量。

核心作用

  1. 状态标记:标记组件内部的响应式状态变量
  2. UI刷新触发:当状态变量的值发生变化时,自动触发UI刷新
  3. 状态封装:将状态封装在组件内部,实现组件的独立性

4.2 @State的工作原理

@State的工作原理基于观察者模式:

  1. 状态注册:组件初始化时,@State装饰器将状态变量注册到状态管理系统
  2. 依赖收集:框架分析build()方法,追踪哪些UI组件依赖于该状态变量
  3. 变更监听:状态管理系统监听状态变量的变化
  4. UI刷新:当状态变量变化时,框架重新渲染依赖于该状态的UI组件
@Component
struct Counter {
  // 1. 状态注册:count被注册到状态管理系统
  @State count: number = 0;
  
  build() {
    Column() {
      // 2. 依赖收集:Text组件依赖于count
      Text(`当前计数: ${this.count}`)
        .fontSize(24);
        
      Button('增加')
        .onClick(() => {
          // 3. 变更监听:count的值发生变化
          this.count++;
          // 4. UI刷新:框架重新渲染Text组件
        });
    }
  }
}

4.3 @State的响应式刷新机制

@State的响应式刷新机制具有以下特点:

4.3.1 细粒度刷新

框架只会刷新依赖于变化状态的UI组件,而不是整个页面:

@Component
struct MultiState {
  @State count1: number = 0;
  @State count2: number = 0;
  
  build() {
    Column() {
      Text(`计数1: ${this.count1}`)  // 仅依赖count1
        .fontSize(20);
        
      Text(`计数2: ${this.count2}`)  // 仅依赖count2
        .fontSize(20);
        
      Button('增加计数1')
        .onClick(() => {
          this.count1++;  // 只刷新第一个Text
        });
        
      Button('增加计数2')
        .onClick(() => {
          this.count2++;  // 只刷新第二个Text
        });
    }
  }
}
4.3.2 数组和对象的刷新机制

@State对数组和对象的刷新机制是浅监听

  • 数组:只有当数组引用发生变化时才会触发刷新(如重新赋值、push、pop等)
  • 对象:只有当对象引用发生变化时才会触发刷新
@Component
struct ArrayState {
  @State items: string[] = ['Item 1', 'Item 2'];
  
  build() {
    Column() {
      ForEach(this.items, (item: string) => {
        Text(item);
      });
      
      Button('添加项')
        .onClick(() => {
          // 方法1:直接修改数组元素(不会触发刷新)
          this.items.push('Item 3');  // ❌ 不会刷新
          
          // 方法2:重新赋值数组(会触发刷新)
          this.items = [...this.items, 'Item 3'];  // ✅ 会刷新
        });
    }
  }
}
4.3.3 嵌套对象的刷新机制

对于嵌套对象,@State同样采用浅监听:

interface User {
  name: string;
  age: number;
  address: {
    city: string;
    street: string;
  };
}

@Component
struct ObjectState {
  @State user: User = {
    name: '张三',
    age: 25,
    address: {
      city: '北京',
      street: '朝阳区'
    }
  };
  
  build() {
    Column() {
      Text(`姓名: ${this.user.name}`);
      Text(`年龄: ${this.user.age}`);
      Text(`城市: ${this.user.address.city}`);
      
      Button('修改城市')
        .onClick(() => {
          // 方法1:直接修改嵌套属性(不会触发刷新)
          this.user.address.city = '上海';  // ❌ 不会刷新
          
          // 方法2:重新赋值对象(会触发刷新)
          this.user = {
            ...this.user,
            address: {
              ...this.user.address,
              city: '上海'
            }
          };  // ✅ 会刷新
        });
    }
  }
}

4.4 @State与其他状态装饰器的对比

装饰器 作用范围 数据流向 更新机制 适用场景
@State 组件内部 单向 浅监听 组件私有状态
@Prop 父子组件 单向 浅监听 父传子,子组件不修改
@Link 父子组件 双向 浅监听 父子共享状态
@ObjectLink 对象属性 双向 属性级监听 对象属性级别的状态同步
@Provide/@Consume 跨层级组件 双向 浅监听 祖先与后代共享状态
4.4.1 @State vs @Prop
// 父组件
@Component
struct Parent {
  @State parentCount: number = 0;
  
  build() {
    Column() {
      Child({ childCount: this.parentCount });
      Button('父组件增加')
        .onClick(() => {
          this.parentCount++;  // 子组件会刷新
        });
    }
  }
}

// 子组件
@Component
struct Child {
  @Prop childCount: number = 0;
  
  build() {
    Column() {
      Text(`子组件计数: ${this.childCount}`);
      Button('子组件增加')
        .onClick(() => {
          this.childCount++;  // 仅修改子组件内部,不会影响父组件
        });
    }
  }
}
4.4.2 @State vs @Link
// 父组件
@Component
struct Parent {
  @State sharedCount: number = 0;
  
  build() {
    Column() {
      Child({ sharedCount: $this.sharedCount });
      Button('父组件增加')
        .onClick(() => {
          this.sharedCount++;  // 子组件会刷新
        });
    }
  }
}

// 子组件
@Component
struct Child {
  @Link sharedCount: number = 0;
  
  build() {
    Column() {
      Text(`子组件计数: ${this.sharedCount}`);
      Button('子组件增加')
        .onClick(() => {
          this.sharedCount++;  // 父组件也会刷新
        });
    }
  }
}
4.4.3 @State vs @ObjectLink
@Observed
interface User {
  name: string;
  age: number;
}

// 父组件
@Component
struct Parent {
  @State user: User = { name: '张三', age: 25 };
  
  build() {
    Column() {
      Child({ user: $this.user });
    }
  }
}

// 子组件
@Component
struct Child {
  @ObjectLink user: User;
  
  build() {
    Column() {
      Text(`姓名: ${this.user.name}`);
      Button('修改姓名')
        .onClick(() => {
          this.user.name = '李四';  // 父组件会刷新(属性级监听)
        });
    }
  }
}

5. 参数传递机制

5.1 @Builder参数传递方式

@Builder函数支持多种参数传递方式:

5.1.1 基本类型参数
@Builder
CardBuilder(title: string, count: number, isActive: boolean) {
  Column() {
    Text(title)
      .fontSize(isActive ? 20 : 16)
      .fontWeight(isActive ? FontWeight.Bold : FontWeight.Normal);
    Text(`数量: ${count}`)
      .fontSize(14);
  }
}
5.1.2 资源类型参数
@Builder
ImageCard(icon: Resource, bgColor: string) {
  Column() {
    Image(icon)
      .width(64)
      .height(64);
  }
  .backgroundColor(bgColor)
  .padding(16)
  .borderRadius(12);
}

// 调用
this.ImageCard($r('app.media.icon'), '#FFF3E0');
5.1.3 对象类型参数
interface CardData {
  title: string;
  description: string;
  icon: Resource;
  color: string;
}

@Builder
DataCard(data: CardData) {
  Column() {
    Image(data.icon)
      .width(48)
      .height(48);
    Text(data.title)
      .fontSize(18)
      .fontColor(data.color);
    Text(data.description)
      .fontSize(14);
  }
}
5.1.4 函数类型参数
@Builder
ActionButton(label: string, onClick: () => void) {
  Button(label)
    .onClick(onClick);
}

// 调用
this.ActionButton('确认', () => {
  console.info('确认按钮被点击');
});

5.2 状态联动与数据流

5.2.1 @Builder内部访问@State

@Builder函数可以直接访问组件的@State状态:

@Component
struct MyComponent {
  @State isExpanded: boolean = false;
  
  @Builder
  ExpandableContent() {
    Column() {
      Text('点击展开/收起');
      if (this.isExpanded) {
        Text('展开的内容...');
      }
    }
    .onClick(() => {
      this.isExpanded = !this.isExpanded;
    });
  }
  
  build() {
    this.ExpandableContent();
  }
}
5.2.2 通过参数传递状态

可以将@State状态作为参数传递给@Builder函数,实现状态联动:

@Component
struct MyComponent {
  @State selectedId: number = -1;
  
  @Builder
  SelectableItem(itemId: number, name: string, isSelected: boolean) {
    Text(name)
      .padding(12)
      .backgroundColor(isSelected ? '#E3F2FD' : '#FFFFFF')
      .border({ width: isSelected ? 2 : 0, color: '#1976D2' })
      .onClick(() => {
        this.selectedId = isSelected ? -1 : itemId;
      });
  }
  
  build() {
    Column() {
      this.SelectableItem(1, '选项1', this.selectedId === 1);
      this.SelectableItem(2, '选项2', this.selectedId === 2);
      this.SelectableItem(3, '选项3', this.selectedId === 3);
    }
  }
}
5.2.3 数据流方向

在@Builder+@State参数化复用布局中,数据流方向如下:

  1. 父组件→@Builder:通过参数传递数据和状态
  2. @Builder→父组件:通过修改父组件的@State状态实现反向通信
  3. @Builder内部:可以直接访问父组件的@State状态

5.3 参数类型与约束

5.3.1 参数类型定义

@Builder函数的参数需要明确指定类型:

@Builder
StyledButton(
  label: string,                    // 字符串类型
  fontSize: number = 16,            // 数字类型,带默认值
  bgColor: string = '#1976D2',      // 字符串类型,带默认值
  textColor: string = '#FFFFFF',    // 字符串类型,带默认值
  width: string | number = '100%',  // 联合类型
  onClick?: () => void              // 可选函数类型
) {
  Button(label)
    .fontSize(fontSize)
    .backgroundColor(bgColor)
    .fontColor(textColor)
    .width(width)
    .onClick(onClick || (() => {}));
}
5.3.2 参数约束

可以使用接口或类型别名对参数进行约束:

interface ButtonStyle {
  fontSize: number;
  bgColor: string;
  textColor: string;
  borderRadius: number;
}

@Builder
StyledButtonWithStyle(label: string, style: ButtonStyle) {
  Button(label)
    .fontSize(style.fontSize)
    .backgroundColor(style.bgColor)
    .fontColor(style.textColor)
    .borderRadius(style.borderRadius);
}
5.3.3 泛型参数

@Builder函数支持泛型参数:

@Builder
ListBuilder<T>(items: T[], renderItem: (item: T) => void) {
  Column() {
    ForEach(items, (item: T) => {
      renderItem(item);
    });
  }
}

// 调用
this.ListBuilder<string>(['A', 'B', 'C'], (item: string) => {
  Text(item);
});

6. 完整实战案例:参数化卡片列表布局

6.1 需求分析与设计

6.1.1 需求描述

实现一个参数可变的卡片列表布局,每个卡片具有以下特性:

  • 显示图标、标题和描述
  • 支持选中状态(选中时显示边框)
  • 支持展开/收起状态(展开时显示详细信息)
  • 每个卡片的颜色、图标、内容都可以通过参数配置
6.1.2 设计思路
  1. 使用@Builder定义可复用的卡片布局:通过参数控制卡片的样式和内容
  2. 使用@State管理状态:管理选中状态和展开状态
  3. 使用ForEach渲染列表:遍历卡片数据,动态生成卡片
  4. 参数化设计:通过参数传递实现卡片的个性化配置
6.1.3 数据结构设计
interface CardItem {
  id: number;           // 卡片唯一标识
  title: string;        // 卡片标题
  description: string;  // 卡片描述
  icon: Resource;       // 卡片图标
  bgColor: string;      // 背景颜色
  titleColor: string;   // 标题颜色
  isExpanded: boolean;  // 展开状态
}

6.2 完整代码实现

/**
 * 鸿蒙ArkTS示例:@Builder + @State 参数化复用布局
 * API Level: 24
 * 
 * 核心技术要点:
 * 1. @Builder - 定义可复用的UI布局片段,支持参数传递
 * 2. @State - 管理组件内部状态,状态变化自动触发UI刷新
 * 3. 参数化复用 - 通过@Builder的参数实现同一布局的不同样式和内容
 */

@Entry
@Component
struct Index {
  /**
   * @State - 当前选中的卡片索引
   * 当状态值变化时,ArkUI自动刷新关联的UI组件
   */
  @State selectedCardIndex: number = -1;
  
  /**
   * @State - 卡片数据列表
   * 使用浅监听机制,数组引用变化时触发刷新
   */
  @State cardList: CardItem[] = [
    {
      id: 1,
      title: '鸿蒙生态',
      description: '分布式能力,跨设备协同',
      icon: $r('app.media.foreground'),
      bgColor: '#FFF3E0',
      titleColor: '#E65100',
      isExpanded: false
    },
    {
      id: 2,
      title: 'ArkTS开发',
      description: 'TypeScript语法,声明式UI',
      icon: $r('app.media.background'),
      bgColor: '#E3F2FD',
      titleColor: '#1565C0',
      isExpanded: false
    },
    {
      id: 3,
      title: '原生体验',
      description: '高性能渲染,流畅动画',
      icon: $r('app.media.startIcon'),
      bgColor: '#E8F5E9',
      titleColor: '#2E7D32',
      isExpanded: false
    },
    {
      id: 4,
      title: '跨端兼容',
      description: '一套代码,多端运行',
      icon: $r('app.media.layered_image'),
      bgColor: '#F3E5F5',
      titleColor: '#7B1FA2',
      isExpanded: false
    }
  ];

  /**
   * @Builder - 卡片布局构建器
   * 通过参数实现卡片的个性化配置
   * 
   * @param item - 卡片数据对象
   * @param isSelected - 是否选中状态
   */
  @Builder
  CardBuilder(item: CardItem, isSelected: boolean) {
    /**
     * 使用Column作为卡片容器
     * 设置space属性控制子组件间距
     */
    Column({ space: 12 }) {
      /**
       * 图标和标题区域
       * 使用Row实现水平布局
       */
      Row({ space: 16 }) {
        /**
         * 图标组件
         * 通过参数动态设置图标资源
         */
        Image(item.icon)
          .width(64)
          .height(64)
          .borderRadius(16)
          .objectFit(ImageFit.Cover)
        
        /**
         * 标题区域
         * 使用Column实现垂直布局
         */
        Column({ space: 4 }) {
          /**
           * 标题文本
           * 通过参数动态设置标题内容和颜色
           */
          Text(item.title)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(item.titleColor)
          
          /**
           * 描述文本
           * 通过参数动态设置描述内容
           */
          Text(item.description)
            .fontSize(14)
            .fontColor('#666666')
        }
        .flexGrow(1)
      }
      
      /**
       * 条件渲染:展开时显示详细信息
       * 使用if语句实现条件渲染
       */
      if (item.isExpanded) {
        Column({ space: 8 }) {
          /**
           * 分隔线组件
           * 使用Divider实现视觉分隔
           */
          Divider()
            .color('#EEEEEE')
            .strokeWidth(1)
          
          Text('详细信息')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#333333')
          
          /**
           * 详细信息文本
           * 显示卡片ID、选中状态和展开状态
           */
          Text(`卡片ID: ${item.id}\n状态: ${isSelected ? '已选中' : '未选中'}\n展开状态: ${item.isExpanded ? '展开' : '折叠'}`)
            .fontSize(12)
            .fontColor('#888888')
            .textAlign(TextAlign.Start)
        }
        .width('100%')
        .padding({ top: 8 })
      }
      
      /**
       * 操作按钮区域
       * 使用Row实现水平布局
       */
      Row({ space: 12 }) {
        /**
         * 切换选中按钮
         * 根据选中状态动态设置样式
         */
        Button('切换选中')
          .width('45%')
          .height(36)
          .fontSize(14)
          .backgroundColor(isSelected ? '#FF5252' : '#E0E0E0')
          .fontColor(isSelected ? '#FFFFFF' : '#333333')
          .borderRadius(8)
          .onClick(() => {
            /**
             * 修改@State状态
             * 状态变化触发UI刷新
             */
            this.selectedCardIndex = isSelected ? -1 : item.id;
          })
        
        /**
         * 展开/收起按钮
         * 根据展开状态动态设置按钮文本
         */
        Button(item.isExpanded ? '收起' : '展开')
          .width('45%')
          .height(36)
          .fontSize(14)
          .backgroundColor(item.titleColor)
          .fontColor('#FFFFFF')
          .borderRadius(8)
          .onClick(() => {
            /**
             * 修改数组元素的属性
             * 需要重新赋值数组触发刷新(浅监听机制)
             */
            item.isExpanded = !item.isExpanded;
            this.cardList = [...this.cardList];
          })
      }
    }
    /**
     * 卡片容器样式
     * 通过参数动态设置背景色和边框
     */
    .width('100%')
    .padding(20)
    .backgroundColor(item.bgColor)
    .border({
      width: isSelected ? 2 : 0,
      color: isSelected ? item.titleColor : '#00000000'
    })
    .borderRadius(16)
    .margin({ bottom: 16 })
    /**
     * 点击卡片切换选中状态
     */
    .onClick(() => {
      this.selectedCardIndex = isSelected ? -1 : item.id;
    })
  }

  /**
   * @Builder - 页面头部布局构建器
   * 展示标题和当前选中状态
   * 
   * @param title - 页面标题
   * @param subtitle - 页面副标题
   */
  @Builder
  HeaderBuilder(title: string, subtitle: string) {
    Column({ space: 8 }) {
      Text(title)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      
      Text(subtitle)
        .fontSize(16)
        .fontColor('#666666')
    }
    .width('100%')
    .padding({ top: 24, bottom: 24 })
  }

  /**
   * build()方法 - 组件的UI构建入口
   * 所有UI组件都必须放在build()方法内部
   */
  build() {
    /**
     * 根布局:垂直滚动容器
     * 使用Scroll实现内容滚动
     */
    Scroll() {
      Column({ space: 0 }) {
        /**
         * 调用@Builder方法构建头部
         * 动态传入标题和选中状态信息
         */
        this.HeaderBuilder(
          '@Builder + @State 参数化复用布局',
          `当前选中卡片: ${this.selectedCardIndex === -1 ? '无' : `ID ${this.selectedCardIndex}`}`
        )
        
        /**
         * 卡片列表区域
         * 使用Column作为容器
         */
        Column({ space: 0 }) {
          /**
           * 使用ForEach遍历卡片数据
           * 动态生成卡片组件
           */
          ForEach(
            this.cardList,
            (item: CardItem) => {
              /**
               * 调用@Builder方法构建卡片
               * 通过参数实现同一布局的不同样式
               */
              this.CardBuilder(item, this.selectedCardIndex === item.id);
            },
            (item: CardItem) => item.id.toString() // 唯一标识
          )
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 24 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAFAFA')
  }
}

/**
 * 卡片数据接口定义
 * 用于类型约束,确保数据结构一致性
 */
interface CardItem {
  id: number;
  title: string;
  description: string;
  icon: Resource;
  bgColor: string;
  titleColor: string;
  isExpanded: boolean;
}

6.3 代码逐段解析

6.3.1 组件声明与状态定义
@Entry
@Component
struct Index {
  @State selectedCardIndex: number = -1;
  @State cardList: CardItem[] = [...];
}
  • @Entry:标记这是应用的入口页面
  • @Component:标记这是一个可复用的UI组件
  • @State selectedCardIndex:管理当前选中卡片的索引
  • @State cardList:管理卡片数据列表
6.3.2 @Builder卡片布局构建器
@Builder
CardBuilder(item: CardItem, isSelected: boolean) {
  Column({ space: 12 }) {
    // 图标和标题区域
    Row({ space: 16 }) {
      Image(item.icon)...
      Column({ space: 4 }) {
        Text(item.title)...
        Text(item.description)...
      }...
    }
    
    // 条件渲染:展开时显示详细信息
    if (item.isExpanded) {
      Column({ space: 8 }) {
        Divider()...
        Text('详细信息')...
        Text(`卡片ID: ${item.id}...`)...
      }...
    }
    
    // 操作按钮区域
    Row({ space: 12 }) {
      Button('切换选中')...
      Button(item.isExpanded ? '收起' : '展开')...
    }
  }
  .width('100%')
  .padding(20)
  .backgroundColor(item.bgColor)
  .border({...})
  .borderRadius(16)
  .margin({ bottom: 16 })
  .onClick(() => {...})
}
  • @Builder:标记这是一个可复用的布局函数
  • item: CardItem:卡片数据对象参数
  • isSelected: boolean:选中状态参数
  • Column({ space: 12 }):垂直布局,子组件间距12
  • Row({ space: 16 }):水平布局,子组件间距16
  • if (item.isExpanded):条件渲染,展开时显示详细信息
  • .backgroundColor(item.bgColor):通过参数动态设置背景色
  • .border({...}):根据选中状态动态设置边框
6.3.3 @Builder头部布局构建器
@Builder
HeaderBuilder(title: string, subtitle: string) {
  Column({ space: 8 }) {
    Text(title)...
    Text(subtitle)...
  }
  .width('100%')
  .padding({ top: 24, bottom: 24 })
}
  • title: string:页面标题参数
  • subtitle: string:页面副标题参数
6.3.4 build()方法
build() {
  Scroll() {
    Column({ space: 0 }) {
      this.HeaderBuilder(
        '@Builder + @State 参数化复用布局',
        `当前选中卡片: ${this.selectedCardIndex === -1 ? '无' : `ID ${this.selectedCardIndex}`}`
      )
      
      Column({ space: 0 }) {
        ForEach(
          this.cardList,
          (item: CardItem) => {
            this.CardBuilder(item, this.selectedCardIndex === item.id);
          },
          (item: CardItem) => item.id.toString()
        )
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 24 })
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#FAFAFA')
}
  • Scroll():滚动容器
  • ForEach():遍历卡片数据,动态生成卡片
  • this.CardBuilder(item, this.selectedCardIndex === item.id):调用@Builder方法,传递数据和状态

6.4 运行效果展示

6.4.1 默认状态
  • 页面显示标题和4张卡片
  • 每张卡片显示图标、标题和描述
  • 卡片具有不同的背景色和标题色
  • 头部显示"当前选中卡片: 无"
6.4.2 选中状态
  • 点击卡片或"切换选中"按钮
  • 选中的卡片显示2px边框,颜色与标题色一致
  • 头部显示"当前选中卡片: ID X"
  • "切换选中"按钮变为红色
6.4.3 展开状态
  • 点击"展开"按钮
  • 卡片下方显示分隔线和详细信息
  • 显示卡片ID、选中状态和展开状态
  • "展开"按钮变为"收起"按钮
6.4.4 收起状态
  • 点击"收起"按钮
  • 详细信息区域隐藏
  • "收起"按钮变为"展开"按钮

7. 常见踩坑与解决方案

7.1 Card组件不存在问题

问题描述

在HarmonyOS NEXT(API 24)中,Card组件已被移除,使用Card()会导致编译错误:

Cannot find name 'Card'.
解决方案

使用ColumnRow组件替代Card组件,通过设置backgroundColorborderRadiusborder等属性实现卡片效果:

// ❌ 错误:Card组件不存在
Card() {
  // 卡片内容
}

// ✅ 正确:使用Column替代
Column() {
  // 卡片内容
}
.backgroundColor('#FFFFFF')
.borderRadius(16)
.border({ width: 1, color: '#EEEEEE' })
原理分析

HarmonyOS NEXT对组件体系进行了重构,移除了部分旧版组件。Card组件的功能可以通过基础布局组件(Column、Row)配合样式属性实现。

7.2 borderTop属性不存在问题

问题描述

在ArkUI中,ColumnRow等布局组件不支持.borderTop().borderBottom()等单独设置某一边边框的方法:

Property 'borderTop' does not exist on type 'ColumnAttribute'.
解决方案

使用.border()方法并通过对象参数设置特定边的边框,或使用Divider组件实现分隔线效果:

// ❌ 错误:borderTop不存在
Column() {...}
.borderTop({ width: 1, color: '#EEEEEE' })

// ✅ 方案1:使用border方法
Column() {...}
.border({ width: { top: 1 }, color: { top: '#EEEEEE' } })

// ✅ 方案2:使用Divider组件
Column({ space: 8 }) {
  Divider()
    .color('#EEEEEE')
    .strokeWidth(1)
  // 其他内容
}
原理分析

ArkUI的边框设置采用统一的.border()方法,通过对象参数可以精确控制每一边的边框样式。对于简单的分隔线场景,Divider组件是更简洁的选择。

7.3 transparent颜色值无效问题

问题描述

在ArkUI中,字符串'transparent'不是有效的颜色值,会导致颜色设置无效或编译错误:

// 无效的颜色值
.border({ color: 'transparent' })
解决方案

使用十六进制透明色'#00000000'Color.Transparent枚举值:

// ❌ 错误:transparent不是有效颜色
.border({ color: 'transparent' })

// ✅ 方案1:使用十六进制透明色
.border({ color: '#00000000' })

// ✅ 方案2:使用Color枚举
.border({ color: Color.Transparent })
原理分析

ArkUI只支持以下颜色格式:

  • 十六进制颜色:#RRGGBB#RRGGBBAA
  • RGB/RGBA函数:rgb(255, 0, 0)rgba(255, 0, 0, 0.5)
  • Color枚举:Color.RedColor.Transparent

7.4 @State数组浅监听问题

问题描述

@State对数组采用浅监听机制,直接修改数组元素的属性不会触发UI刷新:

@State cardList: CardItem[] = [...];

// ❌ 不会触发刷新
this.cardList[0].isExpanded = true;

// ❌ 不会触发刷新
this.cardList.push(newItem);
解决方案

修改数组元素后,需要重新赋值数组引用触发刷新:

// ✅ 方案1:使用展开运算符重新赋值
this.cardList[0].isExpanded = true;
this.cardList = [...this.cardList];

// ✅ 方案2:使用数组方法后重新赋值
this.cardList.push(newItem);
this.cardList = [...this.cardList];

// ✅ 方案3:直接重新赋值
this.cardList = this.cardList.map((item, index) => {
  if (index === 0) {
    return { ...item, isExpanded: true };
  }
  return item;
});
原理分析

@State的浅监听机制只检测数组引用的变化,不检测数组内部元素的变化。因此,修改数组元素后需要通过重新赋值来触发UI刷新。

7.5 链式调用语法错误问题

问题描述

在ArkUI中,组件的链式调用需要注意语法结构,错误的换行或缩进可能导致编译错误:

// ❌ 错误:链式调用断裂
Column() {
  // 内容
}
.backgroundColor('#FFFFFF')  // 这行是独立语句,会报错

// ❌ 错误:括号不匹配
Column() {
  Text('Hello')
}
  .fontSize(20)  // fontSize是Text的属性,不是Column的属性
解决方案

确保链式调用属于同一个组件,并且括号正确配对:

// ✅ 正确:链式调用连续
Column() {
  Text('Hello')
    .fontSize(20)  // fontSize是Text的属性
}
.backgroundColor('#FFFFFF')  // backgroundColor是Column的属性

// ✅ 正确:括号正确配对
Column({ space: 8 }) {
  Row() {
    Text('A')
    Text('B')
  }
  .width('100%')
}
.width('100%')
原理分析

ArkUI的组件调用和属性设置必须属于同一条语句。属性设置方法(如.width().backgroundColor())必须紧跟在组件定义之后,形成连续的链式调用。


8. 性能优化建议

8.1 状态管理最佳实践

8.1.1 最小化状态范围

将状态定义在最小的作用范围内,避免不必要的UI刷新:

// ❌ 不推荐:状态定义在父组件,子组件不需要
@Component
struct Parent {
  @State count: number = 0;
  
  build() {
    Column() {
      Child();  // 子组件不使用count,但父组件刷新时子组件也会刷新
      Text(`计数: ${this.count}`);
    }
  }
}

// ✅ 推荐:状态定义在需要的组件内
@Component
struct Parent {
  build() {
    Column() {
      Child();  // 子组件独立,不受影响
      Counter();  // 状态在Counter组件内
    }
  }
}
8.1.2 使用@Prop替代@State传递状态

当子组件不需要修改父组件状态时,使用@Prop避免不必要的双向绑定:

// ✅ 推荐:子组件只读取状态,不修改
@Component
struct Child {
  @Prop count: number = 0;
  
  build() {
    Text(`计数: ${this.count}`);
  }
}
8.1.3 使用@ObjectLink优化对象状态管理

对于复杂对象,使用@ObjectLink实现属性级别的状态同步:

@Observed
interface User {
  name: string;
  age: number;
}

@Component
struct Child {
  @ObjectLink user: User;
  
  build() {
    Text(`姓名: ${this.user.name}`);
  }
}

8.2 布局复用优化策略

8.2.1 合理使用@Builder

将重复的布局封装为@Builder函数,但不要过度拆分:

// ✅ 推荐:合理封装
@Builder
CardHeader(title: string, icon: Resource) {
  Row({ space: 8 }) {
    Image(icon).width(24).height(24);
    Text(title).fontSize(18).fontWeight(FontWeight.Bold);
  }
}

// ❌ 不推荐:过度拆分,增加调用开销
@Builder
CardIcon(icon: Resource) {
  Image(icon).width(24).height(24);
}

@Builder
CardTitle(title: string) {
  Text(title).fontSize(18).fontWeight(FontWeight.Bold);
}
8.2.2 使用ForEach的key参数

为ForEach提供唯一的key参数,帮助框架优化渲染性能:

// ✅ 推荐:提供key参数
ForEach(
  this.cardList,
  (item: CardItem) => {
    this.CardBuilder(item);
  },
  (item: CardItem) => item.id.toString()  // 唯一标识
)

// ❌ 不推荐:缺少key参数
ForEach(
  this.cardList,
  (item: CardItem) => {
    this.CardBuilder(item);
  }
)
8.2.3 避免在@Builder中定义复杂逻辑

将复杂逻辑移到组件外部或使用工具函数:

// ✅ 推荐:逻辑在外部处理
@Builder
CardBuilder(item: CardItem, isSelected: boolean) {
  Column() {
    Text(item.title)
      .fontColor(isSelected ? item.titleColor : '#333333');
  }
}

// ❌ 不推荐:@Builder中包含复杂逻辑
@Builder
CardBuilder(item: CardItem, isSelected: boolean) {
  Column() {
    if (isSelected) {
      Text(item.title).fontColor(item.titleColor);
    } else {
      Text(item.title).fontColor('#333333');
    }
  }
}

8.3 渲染性能优化

8.3.1 使用懒加载

对于长列表,使用LazyForEach实现懒加载:

// ✅ 推荐:懒加载长列表
LazyForEach(
  this.dataSource,
  (item: CardItem) => {
    this.CardBuilder(item);
  },
  (item: CardItem) => item.id.toString()
)
8.3.2 减少不必要的条件渲染

避免频繁的条件渲染切换,使用状态控制:

// ✅ 推荐:使用状态控制
@State showContent: boolean = false;

build() {
  Column() {
    if (this.showContent) {
      Text('内容');
    }
  }
}

// ❌ 不推荐:频繁切换
build() {
  Column() {
    if (Math.random() > 0.5) {
      Text('内容A');
    } else {
      Text('内容B');
    }
  }
}
8.3.3 优化图片加载

使用合适的图片尺寸和格式,避免大图加载:

// ✅ 推荐:设置合适的尺寸
Image(item.icon)
  .width(64)
  .height(64)
  .objectFit(ImageFit.Cover)

// ❌ 不推荐:未设置尺寸,可能加载大图
Image(item.icon)

9. 与其他布局复用方案的对比

9.1 @Builder vs @Component

9.1.1 相似点
  • 都可以实现布局复用
  • 都支持参数传递
  • 都可以访问组件状态
9.1.2 差异点
特性 @Builder @Component
组件类型 布局片段 完整组件
状态管理 只能访问外部状态 可以定义自己的状态
生命周期 有(aboutToAppear、aboutToDisappear)
复杂度
性能开销
9.1.3 选择策略
  • 使用@Builder:布局简单、无需独立状态、追求高性能
  • 使用@Component:布局复杂、需要独立状态、需要生命周期回调
// @Builder:简单布局复用
@Builder
SimpleCard(title: string) {
  Text(title)
    .padding(12)
    .backgroundColor('#FFFFFF');
}

// @Component:复杂组件,需要独立状态
@Component
struct ComplexCard {
  @State isExpanded: boolean = false;
  
  build() {
    Column() {
      Text('标题')
        .onClick(() => {
          this.isExpanded = !this.isExpanded;
        });
      if (this.isExpanded) {
        Text('展开的内容');
      }
    }
  }
}

9.2 @Builder vs @BuilderParam

9.2.1 相似点
  • 都用于布局复用
  • 都支持参数传递
9.2.2 差异点
特性 @Builder @BuilderParam
定义位置 组件内部或外部 组件内部
调用方式 直接调用 通过组件属性传递
灵活性 更高(支持外部传入布局)
适用场景 固定布局复用 可定制布局的组件
9.2.3 选择策略
  • 使用@Builder:布局固定,在组件内部复用
  • 使用@BuilderParam:布局需要外部定制的组件
// @Builder:固定布局
@Component
struct MyComponent {
  @Builder
  DefaultHeader() {
    Text('默认标题');
  }
  
  build() {
    Column() {
      this.DefaultHeader();
    }
  }
}

// @BuilderParam:可定制布局
@Component
struct CustomizableComponent {
  @BuilderParam headerBuilder?: () => void;
  
  build() {
    Column() {
      if (this.headerBuilder) {
        this.headerBuilder();
      } else {
        Text('默认标题');
      }
    }
  }
}

// 使用时传入自定义布局
CustomizableComponent({
  headerBuilder: () => {
    Text('自定义标题').fontSize(20);
  }
});

9.3 选择策略与场景推荐

9.3.1 场景一:列表项布局

推荐方案:@Builder + @State

@Builder
ListItem(item: ListItemData) {
  Row() {
    Image(item.icon);
    Column() {
      Text(item.title);
      Text(item.description);
    }
  }
}
9.3.2 场景二:功能卡片

推荐方案:@Builder + @State + 参数化

@Builder
FunctionCard(title: string, icon: Resource, color: string) {
  Column() {
    Image(icon).backgroundColor(color);
    Text(title);
  }
}
9.3.3 场景三:可定制组件

推荐方案:@Component + @BuilderParam

@Component
struct CardContainer {
  @BuilderParam contentBuilder: () => void;
  
  build() {
    Column() {
      this.contentBuilder();
    }
    .backgroundColor('#FFFFFF')
    .borderRadius(16);
  }
}
9.3.4 场景四:复杂业务组件

推荐方案:@Component + @State + @Link

@Component
struct FormInput {
  @State value: string = '';
  @Link error: string = '';
  
  build() {
    Column() {
      TextInput({ placeholder: '请输入' })
        .onChange((value: string) => {
          this.value = value;
        });
      if (this.error) {
        Text(this.error).fontColor('#FF5252');
      }
    }
  }
}

10. 总结与展望

10.1 技术总结

@Builder+@State参数化复用布局是鸿蒙ArkTS中一种高效的布局复用方案,具有以下核心优势:

  1. 布局复用:通过@Builder定义可复用的UI布局片段,避免代码重复
  2. 参数化设计:通过参数传递实现同一布局的不同样式和内容
  3. 响应式刷新:通过@State管理状态,状态变化自动触发UI刷新
  4. 性能优化:@Builder轻量级,性能开销低

10.2 实践要点

  1. 合理划分@Builder函数:将重复的布局封装为@Builder函数,但不要过度拆分
  2. 正确使用@State:理解@State的浅监听机制,正确触发UI刷新
  3. 注意API兼容性:HarmonyOS NEXT移除了部分旧版组件,使用替代方案
  4. 遵循ArkUI语法规范:注意链式调用的语法结构,避免语法错误

10.3 未来展望

随着鸿蒙生态的不断发展,ArkTS布局技术将继续演进:

  1. 更强大的状态管理:可能会引入更高级的状态管理方案
  2. 更丰富的组件库:官方会提供更多开箱即用的组件
  3. 更好的性能优化:框架会持续优化渲染性能
  4. 更完善的开发工具:DevEco Studio会提供更好的开发体验

掌握@Builder+@State参数化复用布局技术,对于构建高质量的鸿蒙应用至关重要。希望本文能帮助开发者深入理解这一核心布局技术,并在实际项目中灵活应用。


参考文献

Logo

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

更多推荐