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.6. @Link Annotation¶
@Link is a field-level annotation that establishes two-way synchronization between a child component and a parent component.
An @Link-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 @State, @StorageLink, or @Link.
The annotation @Link is applicable only to member fields of GUI structs.
guiLinkAnnotation:
'@' 'Link'
;
16.5.7. @LocalStorageLink Annotation¶
@LocalStorageLink is a field-level annotation that establishes two-way synchronization with a property inside a LocalStorage.
The @LocalStorageLink annotation is applicable only to member fields of GUI structs.
guiLocalStorageLinkAnnotation:
'@' 'LocalStorageLink' '(' 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.9. @ObjectLink Annotation¶
@ObjectLink is a field-level annotation that establishes two-way synchronization with objects of @Observed-annotated classes.
The annotation @ObjectLink is applicable only to member fields of GUI structs.
guiObjectLinkAnnotation:
'@' 'ObjectLink'
;
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.14. @StorageLink Annotation¶
@StorageLink is a field-level annotation that establishes two-way synchronization with a property inside an AppStorage.
The annotation @StorageLink is applicable only to member fields of GUI structs.
guiStorageLinkAnnotation:
'@' 'StorageLink' '(' StringLiteral ')'
;
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 }