鸿蒙 ArkTS 开发:自定义组件与数据双向绑定
介绍鸿蒙 ArkTS 开发中的自定义组件与数据双向绑定技术。涵盖核心状态装饰器(@State、@Prop、@Link 等)的使用场景,通过搜索框和购物车组件实战演示父子组件通信及复杂对象绑定。同时包含主题化服务实现与夜间模式切换方案,以及搜索与购物车业务逻辑的服务层封装,帮助开发者构建可复用的组件架构。

介绍鸿蒙 ArkTS 开发中的自定义组件与数据双向绑定技术。涵盖核心状态装饰器(@State、@Prop、@Link 等)的使用场景,通过搜索框和购物车组件实战演示父子组件通信及复杂对象绑定。同时包含主题化服务实现与夜间模式切换方案,以及搜索与购物车业务逻辑的服务层封装,帮助开发者构建可复用的组件架构。

本文深入讲解鸿蒙 ArkTS 开发中的自定义组件与数据双向绑定技术,重点阐述数据驱动 UI 更新的核心机制。
学习目标:
学习重点:
ArkTS 提供了 6 种核心状态装饰器,用于实现数据驱动 UI 更新:
| 装饰器 | 功能 | 适用场景 |
|---|---|---|
| @State | 组件内部状态,数据变化自动更新 UI | 组件自身状态管理 |
| @Prop | 父子组件单向绑定(父→子),子组件不可修改 | 组件属性传递 |
| @Link | 父子组件双向绑定(父↔子),子组件可修改 | 表单输入、状态共享 |
| @Provide/@Consume | 跨组件层级状态共享(祖先→后代),支持双向绑定 | 主题切换、用户信息共享 |
| @Observed/@ObjectLink | 复杂对象的双向绑定,对象属性变化自动更新 UI | 商品详情、用户信息等复杂数据 |
| @StorageProp/@StorageLink | 应用级存储状态绑定,数据持久化 | 用户设置、购物车数据等持久化数据 |
文件路径: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' });
});
}
}
文件路径: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);
}
}
基于项目架构,实现以下功能:
在 entry/src/main/ets 目录下创建以下文件夹:
themes:存放主题化相关代码;services:存放业务服务相关代码(如搜索服务、购物车服务)。文件路径: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'
}
};
文件路径: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;
}
}
}
文件路径: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();
// });
}
}
文件路径: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()));
}
}
文件路径: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);
});
}
}
文件路径: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 = [];
}
}
文件路径: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();
});
}
}
文件路径: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%')
.height('100%')
.backgroundColor('#F5F5F5');
}
}
在 entry/src/main/resources/base/media 目录下添加以下图片资源(图片格式为.png,尺寸符合要求):
search.png、clear.png(搜索框);minus.png、plus.png(数量控制);delete.png(删除按钮)。本文介绍了鸿蒙 ArkTS 开发中的自定义组件与数据双向绑定技术。涵盖了核心状态装饰器的使用场景,通过搜索框和购物车组件实战演示了父子组件通信及复杂对象绑定。同时包含了主题化服务实现与夜间模式切换方案,以及搜索与购物车业务逻辑的服务层封装,帮助开发者构建可复用的组件架构。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online