鸿蒙APP开发从入门到精通:自定义组件与数据双向绑定
本文是《鸿蒙APP开发从入门到精通》系列的第3篇,重点讲解鸿蒙应用开发中的自定义组件与数据双向绑定技术。文章详细介绍了6种核心状态装饰器(@State、@Prop、@Link、@Provide/@Consume、@Observed/@ObjectLink、@StorageProp/@StorageLink)的功能和适用场景,并通过搜索框组件和购物车组件的实战案例,展示了如何实现父子组件间的数据传递
《鸿蒙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.png、clear.png(搜索框);minus.png、plus.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篇文章将逐步构建一个完整的鸿蒙电商购物车全栈项目,并最终实现华为应用市场上架变现。
让我们一起期待鸿蒙生态的爆发! 🎉🎉🎉
更多推荐



所有评论(0)