1 -> Basic UI description
ArkTS is a custom component that is @ Component by decorators and data structures declared by @ Entry decorating struct keywords.
1.1 -> Basic Concepts
struct: Custom components can be implemented based on struct, and cannot have inheritance relationships, and new can be omitted for struct instantiation.
Decorator: A decorator gives a certain ability to the object being decorated, which can not only decorate a class or structure, but also decorate the properties of a class. Multiple decorators can be superimposed on the target element, defined in the same line or separated by multiple lines, it is recommended to separate multiple line definitions.
@Entry
@Component
struct MyComponent {
}
Build function: Custom components must define a build function, and custom constructors are disabled. The build function satisfies the Builder constructor interface definition and is used to define the declarative UI description of the component.
interface Builder {
build: () => void
}
@ Component: Decorate struct, structs have component-based capabilities after decorating, and need to implement the build method to create the UI.
@ Entry: Decorate struct, the component is decorated as the entrance to the page, and is rendered when the page loads.
@ Preview: Decorate struct, custom components decorated with @ Preview can be previewed in real-time on DevEco Studio's previewer, and when the page is loaded, custom components @Preview decorations will be created and displayed.
Illustrate:
You can use up to 10 @ Preview to decorate custom components in a single source file.
Chained call: Starts with "." Chained methods configure the property methods, event methods, and so on of UI components.
1.2 -> UI Description Specification
1.2.1 -> Parameterless construction configuration
If the interface definition of the component does not include the required construction parameters, you do not need to configure anything in the "()" after the component. For example, the Divider component does not contain construction parameters.
Column() {
Text('item 1')
Divider()
Text('item 2')
}
1.2.2 -> Configuration of required parameters
If the interface definition of a component contains parameters that must be constructed, the corresponding parameters must be configured in the "()" after the component, and the parameters can be assigned using constants.
For example:
The required parameter src of the Image component is as follows:
Image('https://xyz/test.jpg')
Required parameters of the Text component: content:
Text('test')
Variables or expressions can also be used for parameter assignment, where the result type returned by the expression must meet the parameter type requirements.
For example, set a variable or expression to construct the parameters of the Image and Text components:
Image(this.imagePath)
Image('https://' + this.imageUrl)
Text(`count: ${this.count}`)
1.2.3 -> Attribute Configuration
Configure the properties of the component using the property method, which immediately follows the component, and use the "." Operator concatenation.
Configure the font size property of the Text component:
Text('test')
.fontSize(12)
Use "." operators make chained calls and configure multiple properties of the component at the same time, as follows:
Image('test.jpg')
.alt('error.jpg')
.width(100)
.height(100)
In addition to passing constant parameters directly, you can also pass variables or expressions as follows:
Text('hello')
.fontSize(this.size)
Image('test.jpg')
.width(this.count % 2 === 0 ? 100 : 200)
.height(this.offset + 100)
For the built-in components, the framework also predefines some enumeration types for its properties for developers to call, and the enumeration types can be passed as parameters and must meet the parameter type requirements.
For example, you can configure the color and font properties of the Text component as follows:
Text(this.message)
.id('One Piece')
.fontColor(Color.Blue)
.fontSize(50)
.fontWeight(FontWeight.Bold)
1.2.4 -> Event Configuration
The event method allows you to configure the events supported by the component, which follows the component and is marked with "." Operator concatenation.
Use lambda expressions to configure the component's event method:
Button('add counter')
.onClick(() => {
this.counter += 2
})
Configure the component's event method with an anonymous function expression, requiring the use of bind, to ensure that this reference in the function body contains the component:
Button('add counter')
.onClick(function () {
this.counter += 2
}.bind(this))
Configure the component's event method using the component's member functions:
myClickHandler(): void {
this.counter += 2
}
...
Button('add counter')
.onClick(this.myClickHandler)
1.2.5 -> Subcomponent Configuration
For components that support subcomponent configuration, such as container components, in the {......} to add a UI description of the sub-component to the component. Column, Row, Stack, Grid, List, and other components are container components.
Here's an example of a simple column:
Column() {
Text('Hello')
.fontSize(100)
Divider()
Text(this.myText)
.fontSize(100)
.fontColor(Color.Red)
}
Container components can also be nested with each other to achieve a relatively complex multi-level nesting effect:
Column() {
Row() {
Image('test1.jpg')
.width(100)
.height(100)
Button('click +1')
.onClick(() => {
console.info('+1 clicked!')
})
}
Divider()
Row() {
Image('test2.jpg')
.width(100)
.height(100)
Button('click +2')
.onClick(() => {
console.info('+2 clicked!')
})
}
Divider()
Row() {
Image('test3.jpg')
.width(100)
.height(100)
Button('click +3')
.onClick(() => {
console.info('+3 clicked!')
})
}
}
2 -> State management
2.1 -> Basic Concepts
ArkTS provides a multi-dimensional state management mechanism, in the UI development framework, the data associated with the UI can not only be used within the component, but also can be passed between different component levels, such as between parent-child components, between grandparents and grandchildren, or on a global scale. In addition, from the perspective of data transmission form, it can be divided into read-only one-way transmission and changeable two-way transmission. These capabilities can be flexibly used to realize the linkage between data and UI.
2.2 -> State management of page-level variables
@ State, @ Prop, @ Link, @ Provide, @ Consume, @ ObjectLink, @ Observed, and @ Watch are used to manage the state of page-level variables.
2.2.1 -> @ State
The variables that @ State decorated are the internal state data of the component, and when these state data are modified, the build method of the component will be called to refresh the UI.
@ State status data has the following characteristics:
Supports multiple types of data: The value types and reference types of class, number, boolean, and string strongly typed data are supported, as well as the arrays composed of these strong types, such as Array, Array, Array, Array. Object and any are not supported.
Multi-instance support: The internal state data of different instances of the component is independent.
Internal Private: Properties marked as @State are private variables that can only be accessed within the component.
Local initialization required: All @State variables must be assigned initial values, and uninitialized variables can lead to undefined frame unusual behavior.
When you create a custom component, you can set the initial value of the state variable by the state variable name: When you create a component instance, you can explicitly specify the initial value of the @State state variable by the variable name.
Example:
In the example below:
The user-defined component MyComponent defines the @ State state variables count and title. If the value of count or title changes, run the MyComponent's build method to re-render the component;
There are multiple MyComponent component instances in an EntryComponent, and a change in the internal state of the first MyComponent does not affect the second MyComponent;
When you create a MyComponent instance, you initialize the variables in the component by using the variable name, for example:
MyComponent({ title: { value: 'Hello World 2' }, count: 7 })
// Test_State.ets
class Model {
value: string
constructor(value: string) {
this.value = value
}
}
@Entry
@Component
struct EntryComponent {
build() {
Column() {
MyComponent({ count: 1, increaseBy: 2 }) // 第1个MyComponent实例
MyComponent({ title: { value: 'Hello World 2' }, count: 7 }) // 第2个MyComponent实例
}
}
}
@Component
struct MyComponent {
@State title: Model = { value: 'Hello World' }
@State count: number = 0
private toggle: string = 'Hello World'
private increaseBy: number = 1
build() {
Column() {
Text(`${this.title.value}`).fontSize(30)
Button('Click to change title')
.margin(20)
.onClick(() => {
// 修改内部状态变量title
this.title.value = (this.toggle == this.title.value) ? 'Hello World' : 'Hello ArkUI'
})
Button(`Click to increase count=${this.count}`)
.margin(20)
.onClick(() => {
// 修改内部状态变量count
this.count += this.increaseBy
})
}
}
}
2.2.2 -> @ Prop
@ Prop have the same semantics as @ State, but are initialized differently. Variables @ Prop decorated must be initialized with the @ State variables provided by their parent component, allowing the component to modify @ Prop variables internally, but the changes to the variable are not notified to the parent component, and the changes to the parent component variable are synchronized to the variable @ prop decorated, i.e., @ Prop belong to one-way data binding.
@ Prop state data has the following characteristics:
Supports simple types: Only simple data types such as number, string, and boolean are supported.
Private: Only supports in-component access;
Support for multiple instances: multiple @ Prop-labeled properties can be defined in a component;
Initialize by passing values to @ Prop variables when creating a custom component: When creating a new instance of a component, all @ Prop variables must be initialized, initialization inside the component is not supported.
Example:
In the example below, when the "+1" or "-1" button is pressed, the parent component state changes, and the build method is re-executed, at which point a new CountDownComponent component instance is created. The countDownStartValue state variable of the parent component is used to initialize the @ Prop variable of the child component, and when the "count - costOfOneAttempt" button of the child component is pressed, its @ Prop variable count will be changed, and the CountDownComponent will be re-rendered, but the change in the count value will not affect the countDownStartValue value of the parent component.
// Test_Prop.ets
@Entry
@Component
struct ParentComponent {
@State countDownStartValue: number = 10 // 初始化countDownStartValue
build() {
Column() {
Text(`Grant ${this.countDownStartValue} nuggets to play.`).fontSize(18)
Button('+1 - Nuggets in New Game')
.margin(15)
.onClick(() => {
this.countDownStartValue += 1
})
Button('-1 - Nuggets in New Game')
.margin(15)
.onClick(() => {
this.countDownStartValue -= 1
})
// 创建子组件时,必须在构造函数参数中提供其@Prop变量count的初始值,同时初始化常规变量costOfOneAttempt(非Prop变量)
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
}
}
}
@Component
struct CountDownComponent {
@Prop count: number
private costOfOneAttempt: number
build() {
Column() {
if (this.count > 0) {
Text(`You have ${this.count} Nuggets left`).fontSize(18)
} else {
Text('Game over!').fontSize(18)
}
Button('count - costOfOneAttempt')
.margin(15)
.onClick(() => {
this.count -= this.costOfOneAttempt
})
}
}
}
2.2.3 -> @ Link
@ Link decorated variables can be bidirectionally bound to the @ State variables of the parent component:
Support for multiple types: @link supports the same data types as @ State, i.e., class, number, string, boolean, or arrays of these types;
Private: Only supports in-component access;
Single data source: The parent component must be a @ State variable used to initialize the @ Link variables of the child component;
Bidirectional communication: Changes to @ Link variables in the child component will be synchronized and modified to @ State variables in the parent component.
When creating a custom component, you need to pass a reference to the variable to the @ Link variable, and when you create a new instance of the component, you must initialize all @link variables with named parameters. @ Link variables can be initialized with a reference to a @ State variable or a @ Link variable, and @ State variable can create a reference with the '$' operator.
illustrate
@ Link variables cannot be initialized inside the component.
Simple type example:
@ Link semantics are derived from the '$' operator, i.e. $isPlaying is a two-way data binding of the internal state of this.isPlaying. When you click a button in a child component PlayButton, @ Link variable changes, the PlayButton will be refreshed at the same time as the Text and Button in the parent component, and similarly, when you click the Button in the parent component to modify this.isPlaying, the child component PlayButton will also refresh at the same time as the Text and Button in the parent component.
// Test_Link.ets
@Entry
@Component
struct Player {
@State isPlaying: boolean = false
build() {
Column() {
PlayButton({ buttonPlaying: $isPlaying })
Text(`Player is ${this.isPlaying ? '' : 'not'} playing`).fontSize(18)
Button('Parent:' + this.isPlaying)
.margin(15)
.onClick(() => {
this.isPlaying = !this.isPlaying
})
}
}
}
@Component
struct PlayButton {
@Link buttonPlaying: boolean
build() {
Column() {
Button(this.buttonPlaying ? 'pause' : 'play')
.margin(20)
.onClick(() => {
this.buttonPlaying = !this.buttonPlaying
})
}
}
}
Examples of complex types:
// Test_Link.ets
@Entry
@Component
struct Parent {
@State arr: number[] = [1, 2, 3]
build() {
Column() {
Child({ items: $arr })
Button('Parent Button: splice')
.margin(10)
.onClick(() => {
this.arr.splice(0, 1, 60)
})
ForEach(this.arr, item => {
Text(item.toString()).fontSize(18).margin(10)
}, item => item.toString())
}
}
}
@Component
struct Child {
@Link items: number[]
build() {
Column() {
Button('Child Button1: push')
.margin(15)
.onClick(() => {
this.items.push(100)
})
Button('Child Button2: replace whole item')
.margin(15)
.onClick(() => {
this.items = [100, 200, 300]
})
}
}
}
Examples of @ Link, @ State, and @ Prop combined:
In the following example, the ParentView consists of two subcomponents, ChildA and ChildB, and the state variable counter of the ParentView is used to initialize the @ Prop variable of ChildA and the @ Link variable of ChildB, respectively.
ChildB uses @ Link to establish two-way data binding, and when ChildB modifies the value of the counterRef state variable, the change will be synchronized to the ParentView and ChildA shares.
ChildA uses @ Prop to establish a one-way data binding from the ParentView to itself, and when ChildA modifies the counterVal state variable value, ChildA will re-render, but the change will not be communicated to ParentView and ChildB.
// Test_Combine.ets
@Entry
@Component
struct ParentView {
@State counter: number = 0
build() {
Column() {
ChildA({ counterVal: this.counter })
ChildB({ counterRef: $counter })
}
}
}
@Component
struct ChildA {
@Prop counterVal: number
build() {
Button(`ChildA: (${this.counterVal}) + 1`)
.margin(15)
.onClick(() => {
this.counterVal += 1
})
}
}
@Component
struct ChildB {
@Link counterRef: number
build() {
Button(`ChildB: (${this.counterRef}) + 1`)
.margin(15)
.onClick(() => {
this.counterRef += 1
})
}
}
2.2.4 - > @ Observed and ObjectLink data management
When a developer needs to set up bidirectional synchronization for a variable (parent_a) of a parent component in a child component, the developer can use a @ State decoration variable (parent_a) in the parent component, and use a variable corresponding to the @ Link decoration (child_a) in the child component. This enables data synchronization not only between a parent component and a single child component, but also between a parent component and multiple child components. As shown in the following figure, you can see that the parent component is set up for two-way synchronization for variables of type ClassA, so when the value of attribute c corresponding to the variable in child component 1 changes, the parent component will be notified of the synchronous change, and when the value of attribute c in the parent component changes, all child components will be notified of the synchronous change.
However, the above example is an overall synchronization of a data object, and when the developer only wants to synchronize some of the information about a data object in the parent component, the use of @ Link is not sufficient. If this partial information is a class object, it can be implemented using @ ObjectLink mate @ Observed, as shown in the following figure.
Set the requirements
@ Observed for classes and @ ObjectLink for variables.
The variable type of the @ ObjectLink decoration must be class type.
The class should be decorated with @Observed decorators.
Simple type parameters are not supported, and you can use @ Prop for one-way synchronization.
@ ObjectLink the variables of the decoration are immutable.
Changing properties is allowed, and when a change occurs, if the same object is referenced by multiple @ ObjectLink variables, all custom components that own those variables will be notified to re-render.
Variables @ ObjectLink decorations cannot be set with default values.
The parent component must be initialized with a TS expression that is participated by a variable that is decorated by @State, @link, @ StorageLink, @ Provide, or @ Consume.
Variables @ ObjectLink decorated are private and can only be accessed within the component.
example
// Test_ObjectLink.ets
// 父组件ViewB中的类对象ClassA与子组件ViewA保持数据同步时,可以使用@ObjectLink和@Observed,绑定该数据对象的父组件和其他子组件同步更新
var nextID: number = 0
@Observed
class ClassA {
public name: string
public c: number
public id: number
constructor(c: number, name: string = 'OK') {
this.name = name
this.c = c
this.id = nextID++
}
}
@Component
struct ViewA {
label: string = 'ViewA1'
@ObjectLink a: ClassA
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
.onClick(() => {
this.a.c += 1
})
}.margin({ top: 10 })
}
}
@Entry
@Component
struct ViewB {
@State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]
build() {
Column() {
ForEach(this.arrA, (item) => {
ViewA({ label: `#${item.id}`, a: item })
}, (item) => item.id.toString())
ViewA({ label: `this.arrA[first]`, a: this.arrA[0] })
ViewA({ label: `this.arrA[last]`, a: this.arrA[this.arrA.length - 1] })
Button(`ViewB: reset array`)
.margin({ top: 10 })
.onClick(() => {
this.arrA = [new ClassA(0), new ClassA(0)]
})
Button(`ViewB: push`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.push(new ClassA(0))
})
Button(`ViewB: shift`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.shift()
})
}.width('100%')
}
}
2.2.5 - > @ Consume and @ Provide
@ Provide, as a provider of data, can update the data of its children and grandchildren and trigger page rendering. @ Consume senses an update to the @ Provide data and triggers a re-rendering of the current custom component.
illustrate
When using @ Provide and @ Consume, you should avoid looping references that lead to endless loops.
example
// Test_consume.ets
@Entry
@Component
struct CompA {
@Provide("reviewVote") reviewVotes: number = 0;
build() {
Column() {
CompB()
Button(`CompA: ${this.reviewVotes}`)
.margin(10)
.onClick(() => {
this.reviewVotes += 1;
})
}
}
}
@Component
struct CompB {
build() {
Column() {
CompC()
}
}
}
@Component
struct CompC {
@Consume("reviewVote") reviewVotes: number
build() {
Column() {
Button(`CompC: ${this.reviewVotes}`)
.margin(10)
.onClick(() => {
this.reviewVotes += 1
})
}.width('100%')
}
}
2.2.6 -> @ Watch
@ Watch is used to listen for changes in state variables, and the syntax structure is:
@State @Watch("onChanged") count : number = 0
As shown above, add a @ Watch decorator to the state variable, register a callback method onChanged by @ Watch, and trigger the onChanged callback when the state variable count is changed.
Decorators@ State, @ Prop, @ Link, @ ObjectLink, @ Provide, @ Consume, @ StorageProp, and variables decorated by @StorageLink can be listened to by @ Watch.
// Test_Watch.ets
@Entry
@Component
struct CompA {
@State @Watch('onBasketUpdated') shopBasket: Array<number> = [7, 12, 47, 3]
@State totalPurchase: number = 0
@State addPurchase: number = 0
aboutToAppear() {
this.updateTotal()
}
updateTotal(): number {
let sum = 0;
this.shopBasket.forEach((i) => {
sum += i
})
// 计算新的购物篮总价值,如果超过100,则适用折扣
this.totalPurchase = (sum < 100) ? sum : 0.9 * sum
return this.totalPurchase
}
// shopBasket更改时触发该方法
onBasketUpdated(propName: string): void {
this.updateTotal()
}
build() {
Column() {
Button('add to basket ' + this.addPurchase)
.margin(15)
.onClick(() => {
this.addPurchase = Math.round(100 * Math.random())
this.shopBasket.push(this.addPurchase)
})
Text(`${this.totalPurchase}`)
.fontSize(30)
}
}
}
2.3 -> State management of application-level variables
2.3.1 -> AppStorage
AppStorage is a singleton object in an application, created by the UI framework when the application is started and destroyed when the application exits, providing a central store for application-wide mutable state properties.
AppStorage contains all the state properties that need to be accessed throughout the application, and as long as the application remains running, AppStorage will store all properties and property values, which can be accessed by unique key values.
Components can synchronize application state data with AppStorage through decorators, and the implementation of application business logic can also access AppStorage through APIs.
AppStorage's selection state property can be synchronized with different data sources or data silos, which can be local or remote on the device and have different features such as data persistence. Such data sources and sinks can be implemented in the business logic independently of the UI.
By default, properties in AppStorage are mutable, and AppStorage can also use immutable (read-only) properties.
@ StorageLink decorators
The component establishes a two-way data binding with AppStorage by using a state variable decorated with a @ StorageLink (key), and the key is the attribute key value in AppStorage. When you create a component that contains a @ StorageLink state variable, the value of that state variable is initialized with the value from AppStorage. Changes made to the @ StorageLink's state variables in the UI component are synchronized to AppStorage and from AppStorage to any other bound instances such as PersistentStorage or other bound UI components.
@ StorageProp decorators
The component establishes a one-way data binding with AppStorage by using a state variable decorated with a @ StorageProp (key), and the key identifies the attribute key value in AppStorage. When you create a component that contains a @ StoageProp state variable, the value of that state variable is initialized with the value in AppStorage. A change in the value of a property in AppStorage causes a state update to the bound UI component.
example
The value of the this.varA variable increases every time the user clicks the Count button, which is synchronized with varA in AppStorage. Each time the user clicks the current language button, modify the languageCode in AppStorage, and this change will be synchronized to the this.lang variable.
// Test_AppStorage.ets
@Entry
@Component
struct ComponentA {
@StorageLink('varA') varA: number = 2
@StorageProp('languageCode') lang: string = 'en'
private label: string = 'count'
aboutToAppear() {
this.label = (this.lang === 'zh') ? '数' : 'Count'
}
build() {
Column(){
Row({ space: 20 }) {
Button(`${this.label}: ${this.varA}`)
.onClick(() => {
AppStorage.Set<number>('varA', AppStorage.Get<number>('varA') + 1)
})
Button(`lang: ${this.lang}`)
.onClick(() => {
if (this.lang === 'zh') {
AppStorage.Set<string>('languageCode', 'en')
} else {
AppStorage.Set<string>('languageCode', 'zh')
}
this.label = (this.lang === 'zh') ? '数' : 'Count'
})
}
.margin({ bottom: 50 })
Row(){
Button(`更改@StorageLink修饰的变量:${this.varA}`).fontSize(10)
.onClick(() => {
this.varA++
})
}.margin({ bottom: 50 })
Row(){
Button(`更改@StorageProp修饰的变量:${this.lang}`).fontSize(10)
.onClick(() => {
this.lang = 'test'
})
}
}
}
}
2.3.2 -> PersistentStorage
The PersistentStorage class provides some static methods for managing application persistence data, which can link the persistence data of a specific tag to AppStorage, and access the corresponding persistent data through the AppStorage interface, or access the variables of the corresponding key through @StorageLink decorators.
illustrate
When using the PersistProp API, ensure that the corresponding key exists in AppStorage.
When you use the DeleteProp API, it can only take effect on the data that has been linked to the current startup.
// Test_PersistentStorage.ets
PersistentStorage.PersistProp("highScore", "0");
@Entry
@Component
struct PersistentComponent {
@StorageLink('highScore') highScore: string = '0'
@State currentScore: number = 0
build() {
Column() {
if (this.currentScore === Number(this.highScore)) {
Text(`new highScore : ${this.highScore}`)
}
Button() {
Text(`goal!, currentScore : ${this.currentScore}`)
.fontSize(10)
}.onClick(() => {
this.currentScore++
if (this.currentScore > Number(this.highScore)) {
this.highScore = this.currentScore.toString()
}
})
}
}
}
2.3.3 -> Environment
Environment is a singleton object created by the framework when an application starts, and it provides AppStorage with a set of environment state properties that the application needs that describe the environment environment on which the application runs. Environment and its properties are immutable, and all property value types are simple types. The following example shows how to obtain a voice environment from an environment:
Environment.EnvProp("accessibilityEnabled", "default");
var enable = AppStorage.Get("accessibilityEnabled");
accessibilityEnabled is the default system variable identifier provided by the Environment. First, you need to bind the corresponding system properties to AppStorage, and then access the property data of the corresponding system through methods or decorators in AppStorage.
Top comments (0)