Agile Extension Case: Declarative UI

Declarative UI is a development paradigm oriented to UI programming. It allows developers to describe only the layout relationship between UI components and the binding relationship between UI components and statuses (data required for rendering), without considering the implementation details of UI rendering and refreshing. Therefore, development efficiency of the developer is improved. In recent years, the eDSL mode is used to build declarative UIs in the UI framework in the industry. In this section, the declarative UI is used as an example to describe how to use the domain extension capability of the Cangjie language to build the UI eDSL.

UI Component Layout

The UI eDSL must have the capacities of describing the layout details of various components on the two-dimensional plane, and clearly expressing the configuration information such as the length and width of components and the hierarchical relationship between components. In addition, it is expected that the UI eDSL can make code structures have some similarities with UIs to achieve the effect of "what you see is what you get.". In addition, the UI eDSL should be very concise to minimize the "noise" beyond UI descriptions. Assume that the following UI needs to be implemented. The UI consists of a segment of text and a button. The text and button need to be vertically centered. In addition, the click event processing logic needs to be set for the button. single

With the UI eDSL defined by Cangjie, the following code describes the expected UI. The Text component displays a segment of text, and the Button component implements the button function. To arrange the two components vertically, the two components are embedded in a Column layout component.

class CustomView {
    ...
    func build() {
        Column {
            Text("${count} times")
                .align( Center )
                .margin(top: 50.vp, bottom: 50.vp)
            Button("Click")
                .align( Center )
                .onClick { evt =>
                    count++
                }
        }.width(100.percent)
        .height(100.percent)
    }
    ...
}

For comparison, if the Cangjie language does not provide the corresponding extension capability, developers may need to write the following code. In terms of readability, the former code is clearer and simpler. In terms of character statistics, the latter code requires developers to write nearly 70% more code than the former code, which is a considerable overhead on more complex pages.

class CustomView {
    ...
    func build() {
        new Column ({ =>
            new Text("${count} times")
                .align(Alignment.Center)
                .margin(Length(50, Unit.vp), Length(0, Unit.vp), Length(50, Unit.vp), Length(0, Unit.vp))
            new Button("Click")
                .align(Alignment.Center)
                .onClick ({ evt =>
                    count++
                })
        }).width(Length(100, Unit.percent))
        .height(Length(100, Unit.percent))
    }
    ...
}

When the Cangjie language is used to define the preceding eDSL, the following features are used:

  • The trailing Lambda is used to describe the hierarchical relationship between components. For example, the Column component is used as the parent component of the Text and Button components, which determines the arrangement mode of the Text and Button components. In addition, "()" can be omitted by using the trailing Lambda to make the syntax simpler.

  • Named parameters and default parameter values are used to make parameter pass clearer and simpler. For example, during the margin setting process, only the values of top and bottom need to be set. For parameters that are not set, the default values are used. In addition, the named parameters enable developers to clearly know which parameters are set, without recording the parameter sequence. This improves code readability and reduces errors.

  • With the type extension capability, the expression capability of carrying a length unit for the integer type is extended. For example, 100.percent is equivalent to 100%, and 50.vp is equivalent to 50 vp. Compared with using only integers, the expression capability provides type verification assurance. Compared with using the Length class, the syntax is simpler and more readable.

  • The Cangjie language supports omitting the keyword "new" during class instantiation. Enumeration prefix omittance is implemented through type inference (for example, use Center instead of Alignment.Center), which further enhances the expression simplicity.

Binding Relationship Between UI Components and States

States are a group of data associated with a UI. In a declarative UI, view = f(state) is usually used to express a relationship between a UI (view) and a state (state), where f is used as a link between view and state, is implemented by the framework, and is hidden from UI developers. Generally, f is implemented as a responsive mechanism, that is:

  • Establish the binding relationship between state and the component in the view.
  • Capture state changes and trigger the updates of the corresponding components.

A counter is implemented below by the modification of the top example. An @State count is added for the component, and click event processing is added for the button. Each time the button is clicked, the value of count is incremented by 1. In addition, the Text component displays the current number of clicks, that is, the value of count.

class CustomView {
    @State var count: Int64 = 0
    ...
    func build() {
        Column {
            Text("${count} times")
                .align( Center )
                .margin(top: 50.vp, bottom: 50.vp)
            Button("Click")
                .align( Center )
                .onClick { evt =>
                    count++
                }
        }.width(100.percent)
        .height(100.percent)
    }
    ...
}

The @State macro is added to the count variable so that the count variable has the responsive capability, that is, the change in the click event can be captured and the refresh of the Text component can be triggered. The above implementation mechanism is hidden in the macro implementation of @State. The following is a schematic macro expansion code logic example (actually, as described above, macro expansion occurs in the compilation phase, and the code exists in the AST form based on the expansion logic):

class CustomView {
    private var count_ = State<Int64>(0)

    mut prop count: Int64 {
        get(): Int64 {
            count_.bindToComponent()
            count_.get()
        }
        set(newValue: Int64) {
            count_.set(newVaue)
            count_.notifyComponentChanges()
        }
    }
    ...
    func build() {
        Column {
            Text("${count} times")
                .align( Center )
                .margin(top: 50.vp, bottom: 50.vp)
            Button("Click")
                .align( Center )
                .onClick { evt =>
                    count++
                }
        }.width(100.percent)
        .height(100.percent)
    }
    ...
}

To achieve the preceding effect, the following features are used:

  • The @State macro for state management is defined. The macro is expanded to generate the corresponding state processing code, which reduces the workload of writing template-based and repetitive code and simplifies state declaration and management.
  • The attribute mechanism is used to implement the proxy of the actual status data. The form of count reading and writing retains unchanged externally. In the internal implementation, the get method is used to capture the read operation and establish the binding relationship between the state and the component. The set method is used to capture the write operation and instruct the bound component to update.

The preceding declarative UI case demonstrates that the flexible use of Cangjie's extension capabilities can improve the readability, simplicity, and correctness of code, simplify the workload of developers, lower the threshold for using frameworks or libraries, and facilitate ecosystem promotion.