Intelligent Assistant
Chat with our virtual assistant to get answers promptly.
We use essential cookies for the website to function, as well as analytics cookies for analyzing and creating statistics of the website performance. To agree to the use of analytics cookies, click "Accept All". You can manage your preferences at any time by clicking "Cookie Settings" on the footer. More Information.
HarmonyOS
In ArkUI app development, you may need to encapsulate UI components or styles. This encapsulation enables the reuse of identical or similar code functionality, thereby improving development efficiency while also facilitating project maintenance and team collaboration.
Typical component encapsulation scenarios include:
When developing different service features, you may need to use components with the same style. For example, the login button on the sign-in page and the checkout button on the shopping page—both within the same app and representing confirmation actions—may share the same UI style. In such cases, you can extract the common styles of the Button component, encapsulate them, and enable global reuse. The figure below shows a Button component in two different states: default and pressed.

The common style of a component is implemented by setting the attributes of the component. In ArkUI, you can use the AttributeModifier to encapsulate the attributes in the same AttributeModifier implementation class and apply the AttributeModifier instance object to the component whose style needs to be reused.
The provider defines an implementation class for the AttributeModifier API to encapsulate common style attributes.
- export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
- private buttonType: ButtonType = ButtonType.Normal;
-
- constructor() {
- }
-
- applyNormalAttribute(instance: ButtonAttribute): void {
- instance.type(this.buttonType);
- instance.width(200);
- instance.height(50);
- instance.fontSize(20);
- instance.fontColor('#0A59F7')
- instance.backgroundColor('#0D000000')
- }
-
- applyPressedAttribute(instance: ButtonAttribute): void {
- instance.fontColor('#0A59F7')
- instance.backgroundColor('#26000000')
- }
-
- type(type: ButtonType): MyButtonModifier {
- this.buttonType = type;
- return this;
- }
- }
The consumer creates an AttributeModifier instance and passes it as a parameter to the attributeModifier() method of the system component. The encapsulated styles are then applied to that component.
- @Entry
- @Component
- struct AttributeStylePage {
- modifier = new MyButtonModifier()
- .type(ButtonType.Capsule)
-
- build() {
- NavDestination() {
- Column() {
- Button('Capsule Button')
- .attributeModifier(this.modifier)
- }
- .margin({ top: $r('app.float.margin_top') })
- .justifyContent(FlexAlign.Start)
- .alignItems(HorizontalAlign.Center)
- .width('100%')
- .height('100%')
- }
- .title(getResourceString($r('app.string.common_style_extract'), this))
- }
- }
AttributeModifier applies only to system components. Attributes of custom components cannot be modified.
AttributeModifier instances can be exported and reused across files, supporting modification of attributes and events for components in various states.
For details, see AttributeModifier Usage.
In app development, beyond UI styles, layouts and logic may also require reuse. In such cases, consider encapsulating UI content with the same functionality or style into a custom component. For example, the figure below shows a custom component containing an image and text, implemented through a vertical arrangement of an Image component and a Text component. The styles of the Image and Text components can be modified by the consumer.

Use @Component to create a custom component, encapsulate the static part in the component, and provide the variable part as parameters.
In this scenario, variables are added to set the attributeModifier() method of the child components. External consumers can then pass AttributeModifier instances via parameters, thereby enabling style modifications of the internal child components.
The provider encapsulates default attribute styles: implementing the AttributeModifier API classes for the Image and Text components separately to provide methods to set customizable attributes such as width and height.
- export class CustomImageModifier implements AttributeModifier<ImageAttribute> {
- private imageWidth: Length = 0;
- private imageHeight: Length = 0;
-
- constructor(width: Length, height: Length) {
- this.imageWidth = width;
- this.imageHeight = height;
- }
-
- width(width: Length) {
- this.imageWidth = width;
- return this;
- }
-
- height(height: Length) {
- this.imageHeight = height;
- return this;
- }
-
- applyNormalAttribute(instance: ImageAttribute): void {
- instance.width(this.imageWidth);
- instance.height(this.imageHeight);
- instance.borderRadius($r('app.float.border_radius'))
-
- }
- }
-
- export class CustomTextModifier implements AttributeModifier<TextAttribute> {
- constructor() {
- }
-
- applyNormalAttribute(instance: TextAttribute): void {
- instance.fontSize($r('app.float.font_size_l'));
- }
- }
The provider encapsulates the custom component CustomImageText by adding relevant variables and applying default AttributeModifier objects to configure the Image and Text components.
- @Component
- export struct CustomImageText {
- @Prop imageAttribute: AttributeModifier<ImageAttribute> = new CustomImageModifier(100, 100);
- @Prop textAttribute: AttributeModifier<TextAttribute> = new CustomTextModifier();
- @Prop imageSrc: PixelMap | ResourceStr | DrawableDescriptor;
- @Prop text: string;
- onClickEvent?: () => void;
-
- build() {
- Column({ space: 12 }) {
- Image(this.imageSrc)
- .attributeModifier(this.imageAttribute)
- Text(this.text)
- .attributeModifier(this.textAttribute)
- }.onClick(() => {
- if (this.onClickEvent !== undefined) {
- this.onClickEvent();
- }
- })
- }
- }
The consumer adds the CustomImageText component to their layout. They have the option to create implementation class instances of the AttributeModifier API for the Image and Text components and pass them as parameters to the CustomImageText component to modify its attribute styles.
- @Component
- struct CommonComponent {
- imageAttribute: CustomImageModifier = new CustomImageModifier(330, 330);
-
- build() {
- NavDestination() {
- Column() {
- CustomImageText({
- imageAttribute: this.imageAttribute,
- imageSrc: $r('app.media.image'),
- text: 'Scenery',
- onClickEvent: () => {
- this.getUIContext().getPromptAction().showToast({ message: 'Clicked' })
- }
- })
- }
- .margin({ top: $r('app.float.margin_top') })
- .justifyContent(FlexAlign.Start)
- .alignItems(HorizontalAlign.Center)
- .width('100%')
- .height('100%')
- }
- .title(getResourceString($r('app.string.common'), this))
- }
- }
As shown in the figure below, team A has implemented a component factory class that encapsulates multiple components. In different development scenarios, team B wants to retrieve corresponding components from the factory class instance by using the component names. For instance, by passing the parameter TextInput or Radio to the instance, team B can obtain the TextInput or Radio component template, respectively.

The system provides the @Builder decorator, whose decorated methods comply with the syntax rules of the custom component build() function. After the method decorated by @Builder is passed to wrapBuilder, the WrappedBuilder object is returned. This object can be assigned and transferred.
With the wrapBuilder function, the component factory can store various components using a Map structure, where the key is the component name and the value is a WrappedBuilder object. During use, the corresponding component can be retrieved by its key value.
On the component factory implementation side, encapsulate the components targeted for factory processing via global @Builder methods.
- @Builder
- function myRadio() {
- Text($r('app.string.radio'))
- .width('100%')
- .fontColor($r('sys.color.mask_secondary'))
- Row() {
- Radio({ value: '1', group: 'radioGroup' })
- .margin({ right: $r('app.float.margin_right') })
- Text('man')
- }
- .width('100%')
-
- Row() {
- Radio({ value: '0', group: 'radioGroup' })
- .margin({ right: $r('app.float.margin_right') })
- Text('woman')
- }
- .width('100%')
- }
-
- @Builder
- function myCheckBox() {
- Text($r('app.string.checkbox'))
- .width('100%')
- .fontColor($r('sys.color.mask_secondary'))
- Row() {
- CheckboxGroup({ group: 'checkboxGroup' })
- .checkboxShape(CheckBoxShape.ROUNDED_SQUARE)
- Text('all')
- .margin({ left: $r('app.float.margin_right') })
- }
- .width('100%')
-
- Row() {
- Checkbox({ name: '1', group: 'checkboxGroup' })
- .shape(CheckBoxShape.ROUNDED_SQUARE)
- .margin({ right: $r('app.float.margin_right') })
- Text('text1')
- }
- .width('100%')
-
- Row() {
- Checkbox({ name: '0', group: 'checkboxGroup' })
- .shape(CheckBoxShape.ROUNDED_SQUARE)
- .margin({ right: $r('app.float.margin_right') })
- Text('text2')
- }
- .width('100%')
- }
On the component factory implementation side, use the wrapBuilder function to wrap the encapsulated global @Builder methods and store the return values as values in the component factory Map. After all components are stored, export the component factory for external use.
- let factoryMap: Map<string, object> = new Map();
-
- factoryMap.set('Radio', wrapBuilder(myRadio));
- factoryMap.set('Checkbox', wrapBuilder(myCheckBox));
-
- export { factoryMap };
The consumer imports the component factory and obtains the corresponding WrappedBuilder object based on a key value.
- import { factoryMap } from '../view/FactoryMap';
- // ...
- let myRadio: WrappedBuilder<[]> = factoryMap.get('Radio') as WrappedBuilder<[]>;
- let myCheckbox: WrappedBuilder<[]> = factoryMap.get('Checkbox') as WrappedBuilder<[]>;
The consumer applies the specific component by calling the build() method of the WrappedBuilder object within the build() function.
- @Component
- struct ComponentFactory {
- build() {
- NavDestination() {
- Column({ space: 12 }) {
- myRadio.builder();
- myCheckbox.builder();
- }
- .width('100%')
- .padding($r('app.float.padding'))
- }
- .title(getResourceString($r('app.string.factory'), this))
- }
- }
The wrapBuilder method has the following constraints:
Main approaches include:
Method 1: Using the Controller class
- export class Controller {
- action = () => {
- };
- }
-
- @Component
- export struct ChildComponent {
- @State bgColor: ResourceColor = Color.White;
- controller: Controller | undefined = undefined;
- private switchColor = () => {
- if (this.bgColor === Color.White) {
- this.bgColor = Color.Red;
- } else {
- this.bgColor = Color.White;
- }
- }
-
- aboutToAppear(): void {
- if (this.controller) {
- this.controller.action = this.switchColor;
- }
- }
-
- build() {
- Column() {
- Text('Child Component')
- }.backgroundColor(this.bgColor).borderWidth(1)
- }
- }
-
- @Entry
- @Component
- struct Index {
- private childRef = new Controller();
-
- build() {
- Column() {
- ChildComponent({ controller: this.childRef })
-
- Button('Switch Color')
- .onClick(() => {
- this.childRef.action();
- })
- .margin({ top: 16 })
- }
- .width('100%')
- .alignItems(HorizontalAlign.Center)
- }
- }
Method 2: Using @Watch
Use @Watch to listen to the state variable. When the parent component modifies the variable, the callback of @Watch is executed, that is, the method in the child component is called.
- @Component
- export struct ChildComponent {
- @State bgColor: ResourceColor = Color.White;
- @Link @Watch('switchColor') checkFlag: boolean;
-
- private switchColor() {
- if (this.checkFlag) {
- this.bgColor = Color.Red;
- } else {
- this.bgColor = Color.White;
- }
- }
-
- build() {
- Column() {
- Text('Child Component')
- }.backgroundColor(this.bgColor)
- .borderWidth(1)
- }
- }
-
- @Entry
- @Component
- struct Index {
- @State childCheckFlag: boolean = false;
-
- build() {
- Column() {
- ChildComponent({ checkFlag: this.childCheckFlag })
-
- Button('Switch Color')
- .onClick(() => {
- this.childCheckFlag = !this.childCheckFlag;
- })
- .margin({ top: 16 })
- }
- .width('100%')
- .alignItems(HorizontalAlign.Center)
- }
- }
Approach 3: using Emitter
The Emitter communication mechanism is used. An event listener is added to the child component, and the parent component emits the corresponding event. When the event is listened to, the method in the child component is called.
- @Component
- export struct ChildComponent {
- public static readonly EVENT_ID_SWITCH_COLOR = 'SWITCH_COLOR';
- @State bgColor: ResourceColor = Color.White;
- private switchColor = () => {
- if (this.bgColor === Color.White) {
- this.bgColor = Color.Red;
- } else {
- this.bgColor = Color.White;
- }
- }
-
- aboutToAppear(): void {
- emitter.on(ChildComponent.EVENT_ID_SWITCH_COLOR, this.switchColor);
- }
-
- aboutToDisappear(): void {
- emitter.off(ChildComponent.EVENT_ID_SWITCH_COLOR, this.switchColor);
- }
-
- build() {
- Column() {
- Text('Child Component')
- }.backgroundColor(this.bgColor)
- .borderWidth(1)
- }
- }
-
- @Entry
- @Component
- struct Index {
- build() {
- Column() {
- ChildComponent()
-
- Button('Switch Color')
- .onClick(() => {
- emitter.emit(ChildComponent.EVENT_ID_SWITCH_COLOR);
- })
- .margin({ top: 16 })
- }
- .width('100%')
- .alignItems(HorizontalAlign.Center)
- }
- }
Add a callback method in the child component. When the parent component uses the child component, pass the method of the parent component as a parameter.
- import { BusinessError } from '@kit.BasicServicesKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
-
- @Component
- export struct ChildComponent {
- call = () => {
- };
-
- build() {
- Column() {
- Button('Child Component')
- .onClick(() => {
- this.call();
- })
- }
- }
- }
-
- @Entry
- @Component
- struct Index {
- parentAction() {
- try {
- this.getUIContext().getPromptAction().showToast({ message: 'Parent Action' });
- } catch (error) {
- let err = error as BusinessError;
- hilog.warn(0x000, 'testTag', `showToast failed, code=${err.code}, message=${err.message}`);
- }
- }
-
- build() {
- Column() {
- ChildComponent({ call: this.parentAction })
- }
- .width('100%')
- .alignItems(HorizontalAlign.Center)
- }
- }
Use the @BuilderParam parameter or @BuilderParam trailing closure method to expose the variable UI content as a variable in the encapsulated child component. When the parent component applies the child component, the specific UI content is implemented.
- @Component
- export struct ChildComponent {
- @Builder
- customBuilder() {
- }
-
- @BuilderParam customBuilderParam: () => void = this.customBuilder;
-
- build() {
- Column() {
- Text('Text in Child')
- this.customBuilderParam();
- }
- }
- }
-
- @Entry
- @Component
- struct Index {
- @Builder
- componentBuilder() {
- Text(`Parent builder`)
- }
-
- build() {
- Column() {
- ChildComponent() {
- this.componentBuilder();
- }
- }
- .width('100%')
- .alignItems(HorizontalAlign.Center)
- }
- }
You can pack UI components into global @Builder, and then use wrapBuilder to encapsulate each @Builder in sequence. The obtained WrappedBuilder object or array can be used to pass parameters, and ForEach can be used to render the passed array.
For details, see Assigning a Value to a Variable by the @Builder Method to Use the Variable in UI Syntax.
Intelligent Assistant
Chat with our virtual assistant to get answers promptly.
Quick start
Helps you find desired resources with ease.