HarmonyOS Best Practices: Dynamic Component Creation

HarmonyOS Best Practices: Dynamic Component Creation

Publish Date: Jun 26
0 0

HarmonyOS Best Practices: Dynamic Component Creation

Introduction

Hello everyone! I'm Jun Moxiao from the Qinglan Zhuma organization. Today, I'll share an analysis of dynamic component creation in HarmonyOS.

What Problem Does Dynamic Component Creation Solve?

It addresses the issue of slow component loading by accelerating component rendering and creation.

What's the Principle Behind It?

According to the official documentation, the ArkUI framework's dynamic UI operations support component pre-creation. This allows developers to create components outside the build lifecycle. Once created, these components can undergo property setting and layout calculations. When used during page loading, this significantly improves page response speed.

As shown in the diagram below, the component pre-creation mechanism utilizes idle time during animation execution to pre-create components and set properties. After the animation ends, properties and layouts are updated, saving component creation time and accelerating page rendering.

Component Pre-creation Mechanism

Dynamic Component Creation Uses FrameNode Custom Nodes

What performance advantages do FrameNode custom nodes offer?

  1. Reduced Custom Component Creation Overhead: In declarative development paradigms, using ArkUI custom components to define each node in the node tree often leads to inefficient node creation. This is primarily because each node requires memory allocation in the ArkTS engine to store custom components and state variables. During node creation, operations like component ID, component closure, and state variable dependency collection must be performed. In contrast, using ArkUI's FrameNode avoids creating custom component objects and state variable objects, eliminating the need for dependency collection, thus significantly improving component creation speed.

  2. Faster Component Updates: In dynamic layout framework update scenarios, there's typically a UI component tree (TreeA) created from a tree-structured data model (ViewModelA). When updating TreeA with a new data structure (ViewModelB), although declarative development paradigms can achieve data-driven automatic updates, this process involves numerous diff operations, as shown below. For the ArkTS engine, executing diff algorithms on a complex component tree (depth exceeding 30 layers, containing 100-200 components) makes it nearly impossible to maintain full frame rates at 120Hz. However, using ArkUI's FrameNode extension, the framework can independently control the update process, achieving efficient pruning as needed. This is particularly beneficial for dynamic layout frameworks serving specific business needs, enabling rapid update operations.

Component Update Process

  1. Direct Component Tree Manipulation: Declarative development paradigms also face challenges in updating component tree structures. For example, moving a subtree from one child node to another cannot be done by directly adjusting component instance structural relationships—it requires re-rendering the entire component tree. With ArkUI's FrameNode extension, you can easily manipulate the subtree by operating on FrameNode and transplant it to another node, resulting in only local rendering refreshes and better performance.

Component Tree Manipulation

Implementation Steps

1. Create Custom Nodes

// Parameter type
class Params {
  text: string
  img: ResourceStr

  constructor(text: string, img: ResourceStr) {
    this.text = text;
    this.img = img;
  }
}
// UI component
@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(20)
    Image(params.img)
      .width(30)
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Implement NodeController Class

class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;

  constructor() {
    super();
  }

  makeNode(context: UIContext): FrameNode | null {
    this.textNode = new BuilderNode(context);
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params('Hello', $r('app.media.startIcon')));
    return this.textNode.getFrameNode();
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Use the BuilderNode's build method to construct the component tree. The build() method requires two parameters: the first is the global @builder method wrapped by wrapBuilder(), and the second is the parameter object required by the @builder method. If the @builder method has no parameters or has default parameters, the second parameter of build() can be omitted.

3. Display Custom Nodes

@Entry
@Component
struct Index {
  textNodeController: TextNodeController = new TextNodeController()

  build() {
    Column() {
      NodeContainer(this.textNodeController)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Result:

Display Result

4. Dynamically Remove Components

NodeContainer nodes can be removed or displayed using conditional control statements:

Column() {
  if (this.isShow) {
    NodeContainer(this.textNodeController)
  }
  Button('Click me')
    .onClick(() => {
      this.isShow = !this.isShow
    })
}
Enter fullscreen mode Exit fullscreen mode

5. Dynamically Update Components

replaceBuilderNode(newNode: BuilderNode<Object[]>) {
  this.textNode = newNode;
  this.rebuild();
}
Enter fullscreen mode Exit fullscreen mode
@Entry
@Component
struct Index {
  textNodeController: TextNodeController = new TextNodeController()
  @State isShow: boolean = true

  buildNewNode(): BuilderNode<[Params]> {
    let uiContext: UIContext = this.getUIContext();
    let textNode = new BuilderNode<[Params]>(uiContext);
    textNode.build(wrapBuilder<[Params]>(buildText), new Params('Create new node', $r('app.media.app_background')))
    return textNode;
  }

  build() {
    Column() {
      NodeContainer(this.textNodeController)
      Button('Click me')
        .onClick(() => {
          this.textNodeController.replaceBuilderNode(this.buildNewNode());
        })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: After clicking, the image will flicker because the node is deleted and then added again. If you use update() to update the custom node, the image flickering won't occur.

Demo:

update() {
  if (this.textNode !== null) {
    this.textNode.update(new Params('Update', $r('app.media.background')));
  }
}
Enter fullscreen mode Exit fullscreen mode
@Entry
@Component
struct Index {
  textNodeController: TextNodeController = new TextNodeController()
  @State isShow: boolean = true

  buildNewNode(): BuilderNode<[Params]> {
    let uiContext: UIContext = this.getUIContext();
    let textNode = new BuilderNode<[Params]>(uiContext);
    textNode.build(wrapBuilder<[Params]>(buildText), new Params('Create new node', $r('app.media.app_background')))
    return textNode;
  }

  build() {
    Column() {
      NodeContainer(this.textNodeController)
      Button('Click to create new node')
        .onClick(() => {
          this.textNodeController.replaceBuilderNode(this.buildNewNode());
        })
      Button('Click to update node')
        .onClick(() => {
          this.textNodeController.update()
        })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Comments 0 total

    Add comment