16. Support for GUI Programming

This section describes the built-in mechanisms that ArkTS provides to create graphical user interface (GUI) programs. The section is based on the ArkUI Declarative Core Language Specification available at <https://gitee.com/arkui-finland/arkui-edsl-core-spec/blob/master/arkui-core-spec.md>.

ArkUI provides a set of extensions of the standard to declaratively describe the programs’ GUI, and the interaction between the GUI components. ArkTS aims to adopt ArkUI syntax and semantics as long as they do not contradict ArkTS.

This section is actively under development, and some of its parts can be still underspecified. In such cases please refer to the original specification.


16.1. Annotations

The term annotations as used further in this section denotes special language elements that alter the semantics of classes (class-level annotations), structs (struct-level annotations), struct fields (field-level annotations), or functions (function-level annotations) in a predefined manner. The syntax and semantics of each annotation are defined in the below example that illustrates the idea in general:

 1 @Component
 2 struct MyComponent {
 3     @State @Watch("onValueChanged") value : string = ""
 4
 5     onValueChanged(value: string) : void {
 6         // ...
 7     }
 8
 9     build() : void {
10         // ...
11     }
12 }

16.2. GUI Structs

GUI structs are used to define UI components. From the language perspective, a GUI struct is a restricted form of a non-primitive type that is designed to define GUI expressively and efficiently.

Each GUI struct is required to implement its builder (i.e., the method responsible for the visual rendering of components).

guiStructDeclaration:
    guiEntryAnnotation? guiComponentAnnotation 'struct' identifier
    guiStructBody
    ;

guiStructBody:
    '{'
    guiStructBodyDeclaration*
    guiMainComponentBuilderDeclaration
    guiStructBodyDeclaration*
    '}'
    ;

guiStructBodyDeclaration:
    guiAccessModifier?
    (
    | guiStructFieldDeclaration
    | guiLifeCycleCallbackDeclaration
    | guiCustomComponentBuilderDeclaration
    | classFieldDeclaration
    | classMethodDeclaration
    )
    ;

guiAccessModifier:
    'private'
    ;

guiStructFieldDeclaration:
    guiStructFieldAnnotationDeclaration
    variableDeclaration
    ;

guiStructFieldAnnotationDeclaration:
    guiBuilderParamAnnotation
    | ( guiDataSynchronizationAnnotation guiWatchAnnotation? )
    ;

guiDataSynchronizationAnnotation:
    guiConsumeAnnotation
    | guiLinkAnnotation
    | guiLocalStorageLinkAnnotation
    | guiLocalStoragePropAnnotation
    | guiObjectLinkAnnotation
    | guiPropAnnotation
    | guiProvideAnnotation
    | guiStateAnnotation
    | guiStorageLinkAnnotation
    | guiStoragePropAnnotation
    ;

guiMainComponentBuilderDeclaration:
    guiAccessModifier?
    'build'
    '(' ')' (':' 'void')? block
    ;

guiCustomComponentBuilderDeclaration:
    guiBuilderAnnotation
    guiAccessModifier?
    identifier
    '(' ')' (':' 'void')? block
    ;

guiLifeCycleCallbackDeclaration:
    guiAccessModifier?
    ( 'aboutToAppear' | 'aboutToDisappear' )
    '(' ')' ':' 'void' block
    ;

16.3. Builder Function Syntax Conventions

The following syntax conventions apply to any builder function (component’s main builder, component’s custom builder, or stand-alone global custom builder):

  • The required result of \(C(\{...\})\) for any predefined or custom component C is to initialize the component with the data from the \(\{...\}\) block, and to render it. Concrete semantics depends on the implementation. For illustrative purposes, it can be expressed as \((new C(\{...\})).build()\), where the object literal \(\{...\}\) is handled as an initializer of the component’s fields.

  • The required result of \(C() \{...\}\) for any predefined or custom component C is to initialize the component, and to render it by passing the data from the \(\{...\}\) block to the component’s builder function. Specific semantics depends on the implementation. For illustrative purposes, it can be expressed as \(new C().build(\{...\})\), where the \(\{...\}\) block is handled as a lambda to be passed as an argument to the builder.


16.4. Builder Function Restrictions

Restrictions apply to any builder function (component’s main builder, component’s custom builder, or stand-alone global custom builder), and the following is not allowed:

  • Declaring local variables.

  • Constructing new objects.

  • Function calls, except the following:

    • Calling builders by name.

    • Calling builders by the reference stored in the @BuilderParam-annotated struct field.

    • Calling a predefined builder ForEach for iterative rendering.

    • Calling a function that does not mutate the program state (note that all logging functions are thus prohibited, as they mutate the state).

    • Using conditional if … else syntax.


16.5. Annotations List


16.5.1. @Builder Annotation

Function-level annotation for defining a custom builder is applicable to the following:

  • Methods of GUI structs to define custom builder functions inside a GUI struct.

  • Stand-alone functions to define global custom builders.

guiBuilderAnnotation:
    '@' 'Builder'
    ;

16.5.2. @BuilderParam Annotation

Field-level annotation for defining a reference to a custom builder is applicable only to member fields of GUI structs.

guiBuilderParamAnnotation:
    '@' 'BuilderParam'
    ;

16.5.3. @Component Annotation

Struct-level annotation for marking a struct as a GUI struct is applicable to any struct as long as it complies with the limitations imposed onto GUI structs.

guiComponentAnnotation:
    '@' 'Component'
    ;

16.5.4. @Consume Annotation

@Consume is a field-level annotation that establishes two-way synchronization between a child component at an arbitrary nesting level, and a parent component.

An @Consume-annotated field in a child component shares the same value with a field in the parent component. The source field of the parent component must be annotated with @Provide.

The annotation @Consume is applicable only to member fields of GUI structs.

guiConsumeAnnotation:
    '@' 'Consume'
    | '@' 'Consume' '(' StringLiteral ')'
    ;

16.5.5. @Entry Annotation

Struct-level annotation to indicate the topmost component on the page is applicable only to GUI structs.

guiEntryAnnotation:
    '@' 'Entry'
    | '@' 'Entry' '(' StringLiteral ')'
    ;

16.5.8. @LocalStorageProp Annotation

@LocalStorageProp is a field-level annotation that establishes one-way synchronization with a property inside a LocalStorage. The synchronization of value is unidirectional from the LocalStorage to the annotated field.

The annotation @LocalStorageProp is applicable only to member fields of GUI structs.

guiLocalStoragePropAnnotation:
    '@' 'LocalStorageProp' '(' StringLiteral ')'
    ;

16.5.10. @Observed Annotation

@Observed is a class-level annotation that establishes two-way synchronization between instances of an @Observed-annotated class, and @ObjectLink-annotated member fields of GUI structs.

The annotation @Observed is applicable only to non-GUI classes.

guiObservedAnnotation:
    '@' 'Observed'
    ;

16.5.11. @Prop Annotation

The annotation @Prop has the same semantics as @State, and only differs in how the variable must be initialized and updated:

  • An @Prop-annotated field must be initialized with a primitive or a reference type value provided by its parent component. It must not be initialized locally.

  • An @Prop-annotated field can be modified locally, but the change does not propagate back to its parent component. Whenever that data source changes, the @Prop-annotated field is updated, and any locally-made changes are overwritten. Hence, the sync of the value is uni-directional from the parent to the owning component.

This annotation @Prop is applicable only to member fields of GUI structs.

guiPropAnnotation:
    '@' 'Prop'
    ;

16.5.12. @Provide Annotation

The annotation @Provide has the same semantics as @State with the following additional features:

  • An @Provide-annotated field automatically becomes available to all components that are descendants of the providing component.

The annotation @Provide is applicable only to member fields of GUI structs.

guiProvideAnnotation:
    '@' 'Provide'
    | '@' 'Provide' '(' StringLiteral ')'
    ;

16.5.13. @State Annotation

@State is a field-level annotation, which indicates that the annotated field holds a part of component’s state. Changing any @State-field triggers component re-rendering.

The annotation @State is applicable only to member fields of GUI structs.

guiStateAnnotation:
    '@' 'State'
    ;

16.5.15. @StorageProp Annotation

@StorageProp is a field-level annotation that establishes one-way synchronization with a property inside an AppStorage. The synchronization of value is uni-directional from the AppStorage to the annotated field.

The annotation @StorageProp is applicable only to member fields of GUI structs.

guiStoragePropAnnotation:
    '@' 'StorageProp' '(' StringLiteral ')'
    ;

16.5.16. @Watch Annotation

@StorageProp is a field-level annotation that specifies a callback to be executed when the value of the annotated field changes.

The annotation @StorageProp is applicable only to member fields of GUI structs with the following annotations:

  • @Consume,

  • @Link,

  • @LocalStorageLink,

  • @LocalStorageProp,

  • @ObjectLink,

  • @Prop,

  • @Provide,

  • @State,

  • @StorageLink, and

  • @StorageProp.

guiWatchAnnotation:
    '@' 'Watch' '(' StringLiteral ')'
    ;

16.6. Callable Types

A type is callable if the name of the type can be used in a call expression. A call expression that uses the name of a type is called a type call expression. Only class and struct types can be callable. To make a type callable, a static method with the name ‘invoke’ or ‘instantiate’ must be defined or inherited:

1 class C {
2     static invoke() { console.log("invoked") }
3 }
4 C() // prints: invoked
5 C.invoke() // also prints: invoked

In the above example, ‘C()’ is a type call expression. It is the short form of the normal method call ‘C.invoke()’. Using an explicit call is always valid for the methods ‘invoke’ and ‘instantiate’.

Note: Only a constructor—not the methods ‘invoke’ or ‘instantiate’—is called in a new expression:

1 class C {
2     static invoke() { console.log("invoked") }
3     constructor() { console.log("constructed") }
4 }
5 let x = new C() // constructor is called

The methods ‘invoke’ and ‘instantiate’ are similar but have differences as discussed below.

A compile-time error occurs if a callable type contains both the ‘invoke’ and ‘instantiate’ methods.


16.6.1. Callable Types with Invoke Method

The method ‘invoke’ can have an arbitrary signature. It can be used in a type call expression in either case. If the signature has parameters, then the call must contain corresponding arguments.

1 class Add {
2     static invoke(a: number, b: number): number {
3         return a + b
4     }
5 }
6 console.log(Add(2, 2)) // prints: 4

16.6.2. Callable Types with Instantiate Method

The method ‘instantiate’ can have an arbitrary signature by itself. If it is to be used in a type call expression, then its first parameter must be a ‘factory’ (i.e., it must be a parameterless function type returning some class or struct type). The method can have or not have other parameters, and those parameters can be arbitrary.

In a type call expression, the argument corresponding to the ‘factory’ parameter is passed implicitly:

1 class C {
2     static instantiate(factory: () => C): C {
3         return factory()
4     }
5 }
6 let x = C() // factory is passed implicitly
7
8 // Explicit call of 'instantiate' requires explicit 'factory':
9 let y = C.instantiate(() => { return new C()})

If the method ‘instantiate’ has additional parameters, then the call must contain corresponding arguments:

1 class C {
2     name = ""
3     static instantiate(factory: () => C, name: string): C {
4         let x = factory()
5         x.name = name
6         return x
7     }
8 }
9 let x = C("Bob") // factory is passed implicitly

A compile-time error occurs in a type call expression with type T, if:

  • T has neither method ‘invoke’ nor method ‘instantiate’; or

  • T has the method ‘instantiate’ but its first parameter is not a ‘factory’.

1 class C {
2     static instantiate(factory: string): C {
3         return factory()
4     }
5 }
6 let x = C() // compile-time error, wrong 'instantiate' 1st parameter

16.7. Additional Features


16.7.1. Methods Returning this

A return type of an instance method of a class or a struct can be this. It means that the return type is the class or struct type the method belongs to.

The extended grammar for a method signature (see Signatures) is as follows:

returnType:
    ':' (type | 'this')
    ;

The only result that is allowed to be returned from such a method is this:

1 class C {
2     foo(): this {
3         return this
4     }
5 }

The return type of an overridden method in a subclass must also be this:

1 class D extends C {
2     foo(): this {
3         return this
4     }
5 }
6
7 let x = new C().foo() // type of 'x' is 'C'
8 let y = new D().foo() // type of 'y' is 'D'

Otherwise, compile-time error occurs.


16.7.2. Unary operator ‘$$’

A prefix unary operator ‘$$’ is used to pass primitive types by reference. It is added to ArkTS to support the legacy ArkUI code. As the use of this operator is deprecated, it is to be removed in the future versions of the language.

The operator ‘$$’ can be followed by an identifier. The code ‘$$this.a’ is considered to be the same as ‘$$ this.a’ and ‘$$(this.a)’.


16.8. Example of GUI Programming

  1 // ViewModel classes -----------------------
  2
  3 let nextId : number = 0
  4
  5 @Observed class ObservedArray<T> extends Array<T> {
  6     constructor(arr: T[]) {
  7         super(arr)
  8     }
  9 }
 10
 11 @Observed class Address {
 12     street : string
 13     zip : number
 14     city : string
 15
 16     constructor(street : string, zip: number, city : string) {
 17         this.street = street
 18         this.zip = zip
 19         this.city = city
 20     }
 21 }
 22
 23 @Observed class Person {
 24     id_ : string
 25     name: string
 26     address : Address
 27     phones: ObservedArray<string>
 28
 29     constructor(
 30         name: string,
 31         street : string,
 32         zip: number,
 33         city : string,
 34         phones: string[]
 35     ) {
 36         this.id_ = nextId as string
 37         nextId++
 38         this.name = name
 39         this.address = new Address(street, zip, city)
 40         this.phones = new ObservedArray<string>(phones)
 41     }
 42 }
 43
 44 class AddressBook {
 45     me : Person
 46     contacts : ObservedArray<Person>
 47
 48     constructor(me : Person, contacts : Person[]) {
 49         this.me = me
 50         this.contacts = new ObservedArray<Person>(contacts)
 51     }
 52 }
 53
 54 // @Components -----------------------
 55
 56 /* Renders the name of a Person object and
 57    the first number in the phones ObservedArray<string>
 58    For also the phone number to update we need two
 59    @ObjectLink here, person and phones, cannot use
 60    this.person.phones. Changes of inner Array not observed.
 61    onClick updates selectedPerson also in
 62    AddressBookView, PersonEditView */
 63 @Component struct PersonView {
 64
 65     @ObjectLink person : Person
 66     @ObjectLink phones : ObservedArray<string>
 67
 68     @Link selectedPerson : Person
 69
 70     build() {
 71         Flex({
 72             direction: FlexDirection.Row,
 73             justifyContent: FlexAlign.SpaceBetween })
 74         {
 75             Text(this.person.name)
 76             if (this.phones.length != 0) {
 77                 Text(this.phones[0])
 78             }
 79         }
 80         .height(55)
 81         .backgroundColor(
 82             this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff"
 83         )
 84         .onClick(() => {
 85             this.selectedPerson = this.person
 86         })
 87     }
 88 }
 89
 90 /* Renders all details
 91    @Prop get initialized from parent AddressBookView,
 92    TextInput onChange modifies local copies only on
 93    "Save Changes" copy all data from @Prop to @ObjectLink,
 94    syncs to selectedPerson in other @Components. */
 95 @Component struct PersonEditView {
 96
 97     @Consume addrBook : AddressBook
 98
 99     /* Person object and sub-objects owned by the parent Component */
100     @Link selectedPerson: Person
101
102     /* editing on local copy until save is handled */
103     @Prop name: string = ""
104     @Prop address : Address | null = null
105     @Prop phones : ObservedArray<string> | null = null
106
107     selectedPersonIndex() : number {
108         return this.addrBook.contacts.findIndex(
109             (person) => person.id_ == this.selectedPerson.id_
110         )
111     }
112
113     build() {
114         Column() {
115             TextInput({ text: this.name})
116                 .onChange((value) => {
117                     this.name = value
118                 })
119
120             TextInput({text: this.address.street})
121                 .onChange((value) => {
122                     this.address.street = value
123                 })
124
125             TextInput({text: this.address.city})
126                 .onChange((value) => {
127                     this.address.city = value
128                 })
129
130             TextInput({text: this.address.zip.toString()})
131                 .onChange((value) => {
132                     const result = parseInt(value)
133                     this.address.zip = isNaN(result) ? 0 : result
134                 })
135
136             if (this.phones.length > 0) {
137                 ForEach(this.phones, (phone, index) => {
138                     TextInput({text: phone})
139                         .width(150)
140                         .onChange((value) => {
141                             console.log(index + ". " + value + " value has changed")
142                             this.phones[index] = value
143                         })
144                 }, (phone, index) => index + "-" + phone)
145             }
146
147             Flex({
148                 direction: FlexDirection.Row,
149                 justifyContent: FlexAlign.SpaceBetween
150             }) {
151                 Text("Save Changes")
152                     .onClick(() => {
153                         // copy values from local copy to the provided ref
154                         // to Person object owned by  parent Component.
155                         // Avoid creating new Objects, modify properties of
156                         // existing
157                         this.selectedPerson.name           = this.name
158                         this.selectedPerson.address.street = this.address.street
159                         this.selectedPerson.address.city   = this.address.city
160                         this.selectedPerson.address.zip    = this.address.zip
161                         this.phones.forEach((phone : string, index : number) => {
162                             this.selectedPerson.phones[index] = phone
163                         })
164                     })
165
166                 if (this.selectedPersonIndex() != -1) {
167                     Text("Delete Contact")
168                         .onClick(() => {
169                             let index = this.selectedPersonIndex()
170                             console.log("delete contact at index " + index)
171
172                             // delete found contact
173                             this.addrBook.contacts.splice(index, 1)
174
175                             // determine new selectedPerson
176                             index = (index < this.addrBook.contacts.length)
177                                 ? index
178                                 : index - 1
179
180                             // if no contact left, set me as selectedPerson
181                             this.selectedPerson = (index >= 0)
182                                 ? this.addrBook.contacts[index]
183                                 : this.addrBook.me
184                         })
185                 }
186             }
187         }
188     }
189 }
190
191 @Component struct AddressBookView {
192
193     @ObjectLink me : Person
194     @ObjectLink contacts : ObservedArray<Person>
195     @State selectedPerson: Person | null = null
196
197     aboutToAppear() {
198         this.selectedPerson = this.me
199     }
200
201     build() {
202         Flex({
203             direction: FlexDirection.Column,
204             justifyContent: FlexAlign.Start
205         }) {
206             Text("Me:")
207             PersonView({
208                 person: this.me,
209                 phones: this.me.phones,
210                 selectedPerson: this.$selectedPerson
211             })
212
213             Divider().height(8)
214
215             Flex({
216                 direction: FlexDirection.Row,
217                 justifyContent: FlexAlign.SpaceBetween
218             }) {
219                 Text("Contacts:")
220                 Text("Add")
221                     .onClick(() => {
222                         this.selectedPerson = new Person ("", "", 0, "", ["+86"])
223                         this.contacts.push(this.selectedPerson)
224                     })
225             }
226             .height(50)
227
228             ForEach(this.contacts,
229                 contact => {
230                     PersonView({
231                         person: contact,
232                         phones: contact.phones,
233                         selectedPerson: this.$selectedPerson
234                     })
235                 }, contact => contact.id_
236             )
237
238             Divider().height(8)
239
240             Text("Edit:")
241             PersonEditView({
242                 selectedPerson: this.$selectedPerson,
243                 name: this.selectedPerson.name,
244                 address: this.selectedPerson.address,
245                 phones: this.selectedPerson.phones
246             })
247         }
248         .borderStyle(BorderStyle.Solid)
249         .borderWidth(5)
250         .borderColor(0xAFEEEE)
251         .borderRadius(5)
252     }
253 }
254
255 @Entry
256 @Component struct PageEntry {
257     @Provide addrBook : AddressBook = new AddressBook(
258         new Person(
259             "Mighty Panda",
260             "Wonder str., 8",
261             888,
262             "Shanghai",
263             ["+8611122223333", "+8677788889999", "+8655566667777"]
264         ),
265         [
266         new Person(
267             "Curious Squirrel",
268             "Wonder str., 8",
269             888,
270             "Hangzhou",
271             ["+8611122223332", "+8677788889998", "+8655566667776"]
272         ),
273         new Person(
274             "Wise Tiger",
275             "Wonder str., 8",
276             888,
277             "Nanjing",
278             ["+8610101010101", "+8620202020202", "+8630303030303"]
279         ),
280         new Person(
281             "Mysterious Dragon",
282             "Wonder str., 8",
283             888,
284             "Suzhou",
285             ["+8610000000000", "+8680000000000"]
286         ),
287     ]);
288
289     build() {
290         AddressBookView({
291             me: this.addrBook.me,
292             contacts: this.addrBook.contacts,
293             selectedPerson: this.addrBook.me
294         })
295     }
296 }