《鸿蒙APP开发从入门到精通》第3篇:自定义组件与数据双向绑定 🔄

在这里插入图片描述


内容承接与核心价值

这是《鸿蒙APP开发从入门到精通》的第3篇——架构深化篇,承接第2篇的「电商首页组件」,100%复用项目架构,为后续第6-12篇的电商购物车全栈项目铺垫自定义组件复用数据驱动UI更新的核心技术。

学习目标

  • 掌握自定义组件的高级特性(@Prop、@State、@Link、@Provide/@Consume、@Observed/@ObjectLink);
  • 实现商品搜索购物车两个核心业务场景的自定义组件;
  • 理解数据双向绑定的原理与实现方式;
  • 优化组件代码结构,提高代码复用率。

学习重点

  • 父子组件间的数据传递(@Prop、@Link、@Provide/@Consume);
  • 复杂对象的双向绑定(@Observed/@ObjectLink);
  • 自定义组件的事件处理与动画效果;
  • 组件样式的统一管理(主题化)。

一、 自定义组件高级特性 🎯

1.1 状态装饰器分类

ArkTS提供了6种核心状态装饰器,用于实现数据驱动UI更新

装饰器 功能 适用场景
@State 组件内部状态,数据变化自动更新UI 组件自身状态管理
@Prop 父子组件单向绑定(父→子),子组件不可修改 组件属性传递
@Link 父子组件双向绑定(父↔子),子组件可修改 表单输入、状态共享
@Provide/@Consume 跨组件层级状态共享(祖先→后代),支持双向绑定 主题切换、用户信息共享
@Observed/@ObjectLink 复杂对象的双向绑定,对象属性变化自动更新UI 商品详情、用户信息等复杂数据
@StorageProp/@StorageLink 应用级存储状态绑定,数据持久化 用户设置、购物车数据等持久化数据

1.2 实战:自定义组件状态管理

1. 搜索框组件(@Link双向绑定)

⌨️ entry/src/main/ets/components/SearchBarComponent.ets

import router from '@ohos.router';

@Component
export struct SearchBarComponent {
  @Link searchText: string;

  build() {
    Row({ space: 16 }) {
      // 搜索图标
      Image($r('app.media.search'))
        .width(20)
        .height(20)
        .objectFit(ImageFit.Contain);

      // 搜索输入框
      Input({
        text: this.searchText,
        placeholder: '搜索商品'
      })
        .width('100%')
        .height(40)
        .fontSize(14)
        .backgroundColor('transparent')
        .onChange((value: string) => {
          this.searchText = value;
        });

      // 清空按钮
      if (this.searchText.length > 0) {
        Image($r('app.media.clear'))
          .width(20)
          .height(20)
          .objectFit(ImageFit.Contain)
          .onClick(() => {
            this.searchText = '';
          });
      }
    }
    .width('100%')
    .height(56)
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(28)
    .onClick(() => {
      router.pushUrl({ url: 'pages/SearchPage' });
    });
  }
}
2. 购物车组件(@Observed/@ObjectLink复杂对象绑定)

⌨️ entry/src/main/ets/components/CartComponent.ets

import { CartItemModel } from '../models/CartModel';
import router from '@ohos.router';

@Observed
export class CartItemModel {
  id: number;
  name: string;
  imageUrl: string;
  price: number;
  count: number;
  isChecked: boolean;

  constructor(id: number, name: string, imageUrl: string, price: number, count: number, isChecked: boolean) {
    this.id = id;
    this.name = name;
    this.imageUrl = imageUrl;
    this.price = price;
    this.count = count;
    this.isChecked = isChecked;
  }
}

@Component
export struct CartComponent {
  @ObjectLink cartItems: Array<CartItemModel> = [];

  // 计算选中商品总价
  @Computed
  get totalPrice(): number {
    return this.cartItems.filter(item => item.isChecked).reduce((total, item) => total + item.price * item.count, 0);
  }

  // 计算选中商品数量
  @Computed
  get checkedCount(): number {
    return this.cartItems.filter(item => item.isChecked).length;
  }

  // 全选/取消全选
  private toggleAllChecked(): void {
    const isAllChecked = this.checkedCount === this.cartItems.length;
    this.cartItems.forEach(item => {
      item.isChecked = !isAllChecked;
    });
  }

  // 增加商品数量
  private addCount(index: number): void {
    this.cartItems[index].count++;
  }

  // 减少商品数量
  private subtractCount(index: number): void {
    if (this.cartItems[index].count > 1) {
      this.cartItems[index].count--;
    }
  }

  // 删除购物车商品
  private deleteItem(index: number): void {
    this.cartItems.splice(index, 1);
  }

  build() {
    Column({ space: 0 }) {
      // 购物车商品列表
      List({ space: 16 }) {
        ForEach(this.cartItems, (item: CartItemModel, index: number) => {
          ListItem() {
            CartItemComponent({
              cartItem: item,
              onAddCount: () => this.addCount(index),
              onSubtractCount: () => this.subtractCount(index),
              onDeleteItem: () => this.deleteItem(index)
            });
          }
          .width('100%')
          .height('auto');
        }, (item: CartItemModel) => item.id.toString());
      }
      .width('100%')
      .height('auto')
      .padding(0, 16, 0, 16)
      .layoutWeight(1);

      // 购物车底部栏
      Row({ space: 16 }) {
        // 全选按钮
        Checkbox()
          .checked(this.checkedCount === this.cartItems.length)
          .onChange((isChecked: boolean) => {
            this.toggleAllChecked();
          });
        Text('全选')
          .fontSize(14)
          .textColor('#666666');

        // 总价与结算按钮
        Row({ space: 16 }) {
          Text(`¥${this.totalPrice.toFixed(2)}`)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .textColor('#FF0000');
          Button('结算')
            .width(120)
            .height(48)
            .backgroundColor('#007DFF')
            .onClick(() => {
              router.pushUrl({ url: 'pages/OrderPage' });
            });
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.End);
      }
      .width('100%')
      .height(56)
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(16, 16, 0, 0);
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5');
  }
}

@Component
struct CartItemComponent {
  @ObjectLink cartItem: CartItemModel;
  onAddCount: () => void;
  onSubtractCount: () => void;
  onDeleteItem: () => void;

  build() {
    Row({ space: 16 }) {
      // 选中状态
      Checkbox()
        .checked(this.cartItem.isChecked)
        .onChange((isChecked: boolean) => {
          this.cartItem.isChecked = isChecked;
        });

      // 商品图片
      Image(this.cartItem.imageUrl)
        .width(80)
        .height(80)
        .objectFit(ImageFit.Cover)
        .borderRadius(8);

      // 商品信息
      Column({ space: 8 }) {
        Text(this.cartItem.name)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .textColor('#000000')
          .maxLines(2)
          .ellipsis({ overflow: TextOverflow.Ellipsis });
        Text(`¥${this.cartItem.price}`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .textColor('#FF0000');

        // 数量控制
        Row({ space: 8 }) {
          Image($r('app.media.minus'))
            .width(24)
            .height(24)
            .objectFit(ImageFit.Contain)
            .onClick(() => this.onSubtractCount());
          Text(`${this.cartItem.count}`)
            .fontSize(14)
            .textColor('#666666');
          Image($r('app.media.plus'))
            .width(24)
            .height(24)
            .objectFit(ImageFit.Contain)
            .onClick(() => this.onAddCount());
        }
        .width('auto')
        .justifyContent(FlexAlign.Center);
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start);

      // 删除按钮
      Image($r('app.media.delete'))
        .width(24)
        .height(24)
        .objectFit(ImageFit.Contain)
        .onClick(() => this.onDeleteItem());
    }
    .width('100%')
    .height('auto')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12);
  }
}

二、 数据双向绑定实战 🛠️

2.1 实战目标

基于第2篇的「MyFirstHarmonyApp」项目架构,实现以下功能:

  • 搜索功能:在首页添加搜索框组件,支持实时搜索;
  • 购物车功能:实现购物车组件的商品添加、删除、数量修改、选中状态切换;
  • 主题化:统一管理组件样式,支持夜间模式切换。

2.2 🔧 项目结构调整

在「entry/src/main/ets」目录下创建以下文件夹:

  • themes:存放主题化相关代码;
  • services:存放业务服务相关代码(如搜索服务、购物车服务)。

2.3 🔧 主题化实现

1. 主题配置文件

⌨️ entry/src/main/ets/themes/ThemeConfig.ets

// 主题类型
export type ThemeType = 'light' | 'dark';

// 主题颜色配置
export interface ThemeColors {
  primary: string;
  primaryText: string;
  secondaryText: string;
  backgroundColor: string;
  surfaceColor: string;
  borderColor: string;
  errorColor: string;
  successColor: string;
}

// 主题配置
export const ThemeConfig: Record<ThemeType, ThemeColors> = {
  light: {
    primary: '#007DFF',
    primaryText: '#000000',
    secondaryText: '#666666',
    backgroundColor: '#F5F5F5',
    surfaceColor: '#FFFFFF',
    borderColor: '#E0E0E0',
    errorColor: '#FF0000',
    successColor: '#00C853'
  },
  dark: {
    primary: '#007DFF',
    primaryText: '#FFFFFF',
    secondaryText: '#B0B0B0',
    backgroundColor: '#121212',
    surfaceColor: '#1E1E1E',
    borderColor: '#303030',
    errorColor: '#FF0000',
    successColor: '#00C853'
  }
};
2. 主题服务

⌨️ entry/src/main/ets/services/ThemeService.ets

import { ThemeType, ThemeColors, ThemeConfig } from '../themes/ThemeConfig';

// 主题状态管理
export class ThemeService {
  private static instance: ThemeService | null = null;
  private currentTheme: ThemeType = 'light';

  // 单例模式
  static getInstance(): ThemeService {
    if (!ThemeService.instance) {
      ThemeService.instance = new ThemeService();
    }
    return ThemeService.instance;
  }

  // 获取当前主题
  getCurrentTheme(): ThemeType {
    return this.currentTheme;
  }

  // 获取当前主题颜色
  getCurrentColors(): ThemeColors {
    return ThemeConfig[this.currentTheme];
  }

  // 切换主题
  toggleTheme(): void {
    this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
    // 保存主题到应用存储
    AppStorage.set('theme', this.currentTheme);
  }

  // 初始化主题
  initTheme(): void {
    const savedTheme = AppStorage.get<string>('theme') as ThemeType;
    if (savedTheme) {
      this.currentTheme = savedTheme;
    }
  }
}
3. 主题组件

⌨️ entry/src/main/ets/components/ThemeComponent.ets

import { ThemeService } from '../services/ThemeService';
import { ThemeColors } from '../themes/ThemeConfig';

@Component
export struct ThemeComponent {
  @Provide themeColors: ThemeColors = ThemeService.getInstance().getCurrentColors();

  build() {
    Column({ space: 0 }) {
      // 子组件内容
      BuilderContainer();
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.themeColors.backgroundColor);
  }

  // 响应主题变化
  aboutToAppear() {
    ThemeService.getInstance().initTheme();
    // 监听主题变化
    AppStorage.watch('theme', () => {
      this.themeColors = ThemeService.getInstance().getCurrentColors();
    });
  }
}

2.4 🔧 搜索功能实现

1. 搜索服务

⌨️ entry/src/main/ets/services/SearchService.ets

import { GoodsModel } from '../models/HomeModel';
import { goodsData } from '../models/HomeData';

// 搜索服务
export class SearchService {
  private static instance: SearchService | null = null;

  // 单例模式
  static getInstance(): SearchService {
    if (!SearchService.instance) {
      SearchService.instance = new SearchService();
    }
    return SearchService.instance;
  }

  // 搜索商品
  searchGoods(keyword: string): Array<GoodsModel> {
    if (!keyword) {
      return [];
    }
    return goodsData.filter(item => item.name.toLowerCase().includes(keyword.toLowerCase()));
  }
}
2. 搜索页面

⌨️ entry/src/main/ets/pages/SearchPage.ets

import { SearchBarComponent } from '../components/SearchBarComponent';
import { GoodsListComponent } from '../components/GoodsListComponent';
import { SearchService } from '../services/SearchService';
import { GoodsModel } from '../models/HomeModel';

@Entry
@Component
struct SearchPage {
  @State searchText: string = '';
  @State searchResult: Array<GoodsModel> = [];

  build() {
    Column({ space: 0 }) {
      // 搜索框
      SearchBarComponent({ searchText: $searchText });

      // 搜索结果
      if (this.searchText.length > 0) {
        GoodsListComponent({ data: this.searchResult });
      } else {
        Text('请输入搜索关键词')
          .fontSize(14)
          .textColor('#666666')
          .width('100%')
          .height('100%')
          .textAlign(TextAlign.Center);
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5');
  }

  // 监听搜索关键词变化
  aboutToAppear() {
    AppStorage.watch('searchText', () => {
      this.searchResult = SearchService.getInstance().searchGoods(this.searchText);
    });
  }
}

2.5 🔧 购物车功能实现

1. 购物车服务

⌨️ entry/src/main/ets/services/CartService.ets

import { CartItemModel } from '../components/CartComponent';
import { goodsData } from '../models/HomeData';

// 购物车服务
export class CartService {
  private static instance: CartService | null = null;
  private cartItems: Array<CartItemModel> = [];

  // 单例模式
  static getInstance(): CartService {
    if (!CartService.instance) {
      CartService.instance = new CartService();
    }
    return CartService.instance;
  }

  // 获取购物车商品列表
  getCartItems(): Array<CartItemModel> {
    return this.cartItems;
  }

  // 添加商品到购物车
  addToCart(id: number, count: number = 1): void {
    const existingItem = this.cartItems.find(item => item.id === id);
    if (existingItem) {
      existingItem.count += count;
    } else {
      const goods = goodsData.find(item => item.id === id);
      if (goods) {
        this.cartItems.push(new CartItemModel(
          goods.id,
          goods.name,
          goods.imageUrl,
          goods.price,
          count,
          false
        ));
      }
    }
  }

  // 删除购物车商品
  deleteFromCart(id: number): void {
    const index = this.cartItems.findIndex(item => item.id === id);
    if (index !== -1) {
      this.cartItems.splice(index, 1);
    }
  }

  // 修改购物车商品数量
  updateCount(id: number, count: number): void {
    const existingItem = this.cartItems.find(item => item.id === id);
    if (existingItem && count > 0) {
      existingItem.count = count;
    }
  }

  // 切换购物车商品选中状态
  toggleChecked(id: number): void {
    const existingItem = this.cartItems.find(item => item.id === id);
    if (existingItem) {
      existingItem.isChecked = !existingItem.isChecked;
    }
  }

  // 清空购物车
  clearCart(): void {
    this.cartItems = [];
  }
}
2. 购物车页面

⌨️ entry/src/main/ets/pages/CartPage.ets

import { CartComponent } from '../components/CartComponent';
import { CartService } from '../services/CartService';

@Entry
@Component
struct CartPage {
  @State cartItems: Array<CartComponent.CartItemModel> = CartService.getInstance().getCartItems();

  build() {
    Column({ space: 0 }) {
      if (this.cartItems.length > 0) {
        CartComponent({ cartItems: $cartItems });
      } else {
        Text('购物车是空的')
          .fontSize(14)
          .textColor('#666666')
          .width('100%')
          .height('100%')
          .textAlign(TextAlign.Center);
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5');
  }

  // 监听购物车数据变化
  aboutToAppear() {
    AppStorage.watch('cartItems', () => {
      this.cartItems = CartService.getInstance().getCartItems();
    });
  }
}

2.6 🔧 商品详情页面

⌨️ entry/src/main/ets/pages/ProductDetailPage.ets

import { CartService } from '../services/CartService';
import { GoodsModel } from '../models/HomeModel';
import { goodsData } from '../models/HomeData';

@Entry
@Component
struct ProductDetailPage {
  @State goods: GoodsModel = goodsData[0];
  @State count: number = 1;

  build() {
    Scroll() {
      Column({ space: 24 }) {
        // 商品图片
        Image(this.goods.imageUrl)
          .width('100%')
          .height(300)
          .objectFit(ImageFit.Cover);

        // 商品信息
        Column({ space: 16 }) {
          Text(this.goods.name)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .textColor('#000000')
            .maxLines(2)
            .ellipsis({ overflow: TextOverflow.Ellipsis });

          // 价格与销量
          Row({ space: 16 }) {
            Column({ space: 4 }) {
              Text(`¥${this.goods.price}`)
                .fontSize(24)
                .fontWeight(FontWeight.Bold)
                .textColor('#FF0000');
              Text(`¥${this.goods.originalPrice}`)
                .fontSize(14)
                .textColor('#999999')
                .decoration({ type: TextDecorationType.LineThrough });
            }
            .alignItems(HorizontalAlign.Start);

            Text(`已售${this.goods.sales}`)
              .fontSize(14)
              .textColor('#666666');
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween);

          // 数量控制
          Row({ space: 16 }) {
            Text('数量')
              .fontSize(14)
              .textColor('#666666');
            Row({ space: 8 }) {
              Image($r('app.media.minus'))
                .width(24)
                .height(24)
                .objectFit(ImageFit.Contain)
                .onClick(() => {
                  if (this.count > 1) {
                    this.count--;
                  }
                });
              Text(`${this.count}`)
                .fontSize(14)
                .textColor('#666666');
              Image($r('app.media.plus'))
                .width(24)
                .height(24)
                .objectFit(ImageFit.Contain)
                .onClick(() => {
                  this.count++;
                });
            }
            .width('auto')
            .justifyContent(FlexAlign.Center);
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween);

          // 加入购物车与立即购买按钮
          Row({ space: 16 }) {
            Button('加入购物车')
              .width('50%')
              .height(48)
              .backgroundColor('#FFFFFF')
              .textColor('#007DFF')
              .border({ width: 1, color: '#007DFF' })
              .onClick(() => {
                CartService.getInstance().addToCart(this.goods.id, this.count);
              });
            Button('立即购买')
              .width('50%')
              .height(48)
              .backgroundColor('#007DFF')
              .onClick(() => {
                console.log('点击了立即购买按钮');
              });
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween);
        }
        .width('100%')
        .padding(24);
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5');
  }
}

三、 项目运行与效果验证 📱

3.1 图片资源准备

在「entry/src/main/resources/base/media」目录下添加以下图片资源(图片格式为.png,尺寸符合要求):

  • search.pngclear.png(搜索框);
  • minus.pngplus.png(数量控制);
  • delete.png(删除按钮)。

3.2 🔧 项目运行

① 在DevEco Studio中点击「Run」按钮;
② 选择调试设备,点击「OK」;
③ 等待编译安装完成,应用会自动在设备上启动。

效果验证

搜索功能:在首页顶部显示搜索框,输入关键词可实时搜索;
购物车功能:在购物车页面显示商品列表,支持添加、删除、数量修改、选中状态切换;
主题化:在设置页面支持夜间模式切换;
响应式布局:自动适配不同设备尺寸。


四、 总结与未来学习路径 🚀

4.1 总结

本文作为《鸿蒙APP开发从入门到精通》的第3篇,完成了:

  • 对ArkTS核心状态装饰器的理解;
  • 搜索框、购物车等核心业务场景的自定义组件实现;
  • 数据双向绑定的实战应用(@Link、@Observed/@ObjectLink);
  • 主题化功能的实现,支持夜间模式切换。

4.2 未来学习路径

  • 第4篇:网络请求与数据持久化;
  • 第5篇:页面路由与组件跳转;
  • 第6篇:原子化服务与元服务卡片的开发;
  • 第7篇:超级终端多设备协同开发;
  • 第8篇:服务联邦跨服务无缝打通;
  • 第9篇:安全加固与组件化架构;
  • 第10篇:AI原生与用户增长;
  • 第11篇:性能优化与Next原生合规;
  • 第12篇:运维监控、生态运营与专属变现。

结语 ✅

恭喜你!你已经完成了《鸿蒙APP开发从入门到精通》的第3篇,掌握了自定义组件的高级特性与数据双向绑定的核心技术。

从现在开始,你已具备了开发复杂业务场景的能力。未来的9篇文章将逐步构建一个完整的鸿蒙电商购物车全栈项目,并最终实现华为应用市场上架变现。

让我们一起期待鸿蒙生态的爆发! 🎉🎉🎉

Logo

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

更多推荐