鸿蒙开发速通(一)
ArkTS
在ArkTS中,类的字段必须在声明的时候或是在构造函数中显示初始化。类似于typescript的strictPropertyInitialization模式。
在ArkTS中必不可少的是Struct。在日常开发中会经常用到,主要处理界面。
页面和组件
在默认生成的页面代码是这样的:
@Entry // 1
@ComponentV2 // 2
struct Index { // 3
@Local message: string = 'Hello World'; // 4
build() {
RelativeContainer() { // 5
Text(this.message) // 6
.id('HelloWorld') // 7
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => { // 8
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}
-
@Entry:装饰器,表示这是一个页面(入口组件) -
@ComponentV2:表示自定义组件,更重要的是在组件内部使用状态管理V2版本。@Component对应的就是状态管理V1。现在推荐使用的是状态管理V2,不过参考代码大部分都是V1的。本文中使用的都是状态管理的V2版本。 -
Struct,自定义组件。自定义组件就是@Component和Struct的组合。 -
@Local,组件内部的状态,这里是message。在用户点击了界面上的Hello World文本之后会显示message的内容。 -
RelativeContainer,相对布局。在alignRules里定义了布局的规则。鸿蒙使用声明式UI来实现组件开发和布局。 -
Text,是文本组件。 -
id('xxxxxx'),属性方法,后面出现的fontSize()、alignRules()也是属性方法。 -
onClick,事件方法,用来响应Text组件的点击事件。
自定义组件
如上所述,鸿蒙试用报告声明式UI开发组件。
一个简单的例子:
Column() {
Text("Hello Bro")
// ...
}
这里包含了一个Column布局和一个Text组件。
正式自定义一个简单的组件:
@Preview
@ComponentV2
export struct ThemedButton {
@Param message: string = "Hello ThemedButton";
build() {
Row() {
Text(this.message);
}
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.width('100%')
.height(60)
.padding(20)
.borderColor(Color.Blue)
.borderWidth(1)
.borderRadius(30)
}
}
看起来效果是这样的:
在自定义组件的时候可以给组件加一个@Preview的注解,这样可以在IDE的Previewer里看到组件的效果。这样就不需要反复的运行项目才能看到效果了。
@Param是状态管理的一部分,会在后面的状态管理节点细讲。
组件作为参数
使用@BuildParam装饰参数,参数的类型是:() => void。参数可以根据要传入的组件定义。
完成代码:
@Preview
@ComponentV2
export struct Card {
// ...略...
@BuilderParam renderContent?: () => void; // 1
build() {
Stack({ alignContent: Alignment.TopStart }) {
Column() {
if (this.renderContent) {
this.renderContent() // 2
} else {
Text(this.message)
.height(20)
.width(300)
.align(Alignment.Center)
}
}
// ...略...
}
// ...略...
}
}
- 使用
@BuildParam装饰要传入的组件,类型是() => void。 - 在组件的
build方法中使用传入的组件。
在其他组件中这样使用:
build() {
// ...略...
Column() {
Card({ title: 'Task pool', message: this.message}) {
this.renderTaskpool() // *
}
}
// ...略...
在标记星号的行是一个尾随闭包。换成常规写法是:
Card({ title: 'Task pool', message: this.message, renderContent: this.renderTaskpool()})
复用样式
把几个标准样式放在一起可以定义一个可以复用的样式。
定义一个全局样式
@Styles
function themedBorder() {
.borderWidth(1)
.borderColor(Color.Gray)
}
定义一个组件内样式
@Styles
matchParent() {
.width('100%')
.height('100%')
}
使用的方法都一样,和标准的属性方法一样:
Column() {
Text(this.message)
.height(20)
.width(300)
.align(Alignment.Center)
}
.matchParent()
.themedBorder()
renderProps
HOC
组件的生命周期
文档在这里。
组件的生命周期:
aboutToAppear -> build -> onDidBuild -> aboutToDisappear。
-
aboutToAppear在build方法之前执行,在其中执行初始化组件的任务。不要执行耗时的任务。可以修改状态变量,会在build中起作用。 -
aboutToDisappear在组件销毁前执行,可以在其中执行资源的回收。不可以修改状态变量。
页面的生命周期
页面就是有@Entry装饰的自定义组件。
所以上面说到的自定义组件的生命周期方法都会被调用。额外的还增加了三个生命周期方法:
onPageShow、onPageHide和onBackPress。最后的生命周期方法调用是这样的:
弹窗
List
Grid
布局
Stack
层叠布局,看起来是这样式儿的
代码是这样的:
Stack({ alignContent: Alignment.BottomEnd }) {
Text('Layer 1')
.width('100%')
.height('100%')
.backgroundColor('#FFE66D')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
Text('Layer 2')
.width(150)
.height(150)
.backgroundColor('#FF6B6B')
.textAlign(TextAlign.Center)
.fontColor(Color.White)
Text('Layer 3')
.width(80)
.height(80)
.backgroundColor('#4ECDC4')
.textAlign(TextAlign.Center)
.fontColor(Color.White)
}
.width('100%')
.height(200)
.borderRadius(8)
.margin({ top: 20, bottom: 12 })
把颜色,大小等属性方法都删掉,看看关于Stack最重要的部分:
Stack({ alignContent: Alignment.BottomEnd }) { ///*
Text('Layer 1')
Text('Layer 2')
Text('Layer 3')
}
这里最重要的就是在初始化Stack的时候的参数:alignContent。Alignemnt枚举有几个不同的值,分别制定了Stack内的组件的排列顺序。如图:
Flex
和H5的flex基本类似,只是写法换了一下。比如,flex最核心的flex direction和justify content和align items的作用都一样。只是给定值的时候用了鸿蒙自定义的枚举值。
Flex({
direction: this.flexDirection,
justifyContent: this.justifyContent,
alignItems: this.alignItems
}) {
Text('Item 1')
.width(80)
.height(80)
.backgroundColor('#FF6B6B')
.textAlign(TextAlign.Center)
.fontColor(Color.White)
Text('Item 2')
.width(80)
.height(80)
.backgroundColor('#4ECDC4')
.textAlign(TextAlign.Center)
.fontColor(Color.White)
Text('Item 3')
.width(80)
.height(80)
.backgroundColor('#45B7D1')
.textAlign(TextAlign.Center)
.fontColor(Color.White)
}
.width('100%')
.height(200)
.backgroundColor('#F7F7F7')
.padding(12)
Column和Row
Column、Row就是Flex这个布局的语法糖。
Column就是FlexDirection的值为Column的时候,Row也一样。
RelativeContainer
这个布局用好了有神奇功效。可以把布局的嵌套减少,提高渲染效率。
看代码:
RelativeContainer() {
// 左上角元素
Row()
// 略
.id('topLeft')
.alignRules({ ///*
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
// 略
}
在RelativeContainer里的组件定位依赖的是相对于哪个组件的定位规则。本例中使用的是anchor: '__container__',也就是相对于容器定位,具体的定位规则是align: VerticalAlign.Top。在父容器的上部分。本例只要给出top和left就可以。
当然,可以相对定位的组件可以是父容器,也可以是容器内地其他组件。或者是参考边界,辅助线等。更多可以参考这里。
GridRow/GridCol
这个擅长解决不同屏幕尺寸的适配问题。文档在这里。
先看效果:
在折叠屏展开的时候,只显示一行,8列。在折叠屏折叠之后显示2行,4列。
代码:
GridRow({ /// 1
breakpoints: { /// 2
value: ['320vp', '600vp', '840vp', '1440vp',
'1600vp'], // 表示在保留默认断点['320vp', '600vp', '840vp']的同时自定义增加'1440vp', '1600vp'的断点,实际开发中需要根据实际使用场景,合理设置断点值实现一次开发多端适配。
reference: BreakpointsReference.WindowSize /// 3
},
columns: { /// 4
xs: 2, // 窗口宽度落入xs断点上,栅格容器分为2列。
sm: 4, // 窗口宽度落入sm断点上,栅格容器分为4列。
md: 8, // 窗口宽度落入md断点上,栅格容器分为8列。
lg: 12, // 窗口宽度落入lg断点上,栅格容器分为12列。
xl: 12, // 窗口宽度落入xl断点上,栅格容器分为12列。
xxl: 12 // 窗口宽度落入xxl断点上,栅格容器分为12列。
},
}) {
ForEach(this.bgColors, (color: ResourceColor, index?: number | undefined) => {
GridCol({ span: 1 }) { // 所有子组件占一列。
Row() {
Text(`${index}`)
}.width('100%').height('50vp')
}.backgroundColor(color)
})
}
.height(200)
.border({ color: 'rgb(39,135,217)', width: 2 })
.onBreakpointChange((breakPoint) => {
this.currentBreakpoint = breakPoint
})
- 基本的结构就是外面是
GridRow里面是GridCol。 -
breakpoints,也就是断点。其实更适合叫触发点。这里的value定义了一个数组。这里的值定义了屏幕宽度的触发点。屏幕的宽度到了某个值的范围后就会触发一个动作。这个动作在columns定义。 -
GridRow监听的是哪个组件的宽度,这里是Window的宽度。 -
columns定义的就是每个宽度对应要显示几列。比如屏幕宽度在xs的时候显示两列,sm宽度显示4列,等。也可以直接给定列数值,那么不管屏幕的宽度如何变化列数也就只显示给定的列数。
默认情况:
- API version 20之前,columns显示12列。没有设置columns的话,任何断点都是显示12列。
- API version 20之后,columns默认值为
{ xs: 2, sm: 4, md: 8, lg: 12, xl: 12, xxl: 12 }。
初识UIAbility
一个应用可以包含一个或者多个UIAbility。一个UIAbility可以在最近任务中作为一个任务显示。一个Ability可以包含一组界面。所以,使用Ability也可以达到避免加载不必要的资源的效果。
Ability的配置
配置文件module.json5在:
project
└── entry
└── src
└── main
└── ets
└── module.json5
默认的看起来是这样的:
{
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
// 其他略
}
]
}
在这个默认的配置文件中,已经标示了默认生成的Ability的文件位置,名称等。
UIAbility的基本使用
新建一个UIAbility
我们修改已经有的Todo App,把todo详情改成在一个Ability里显示。
在DevEco Studio中,点New->Ability就可以新建一个Ability。这个Ability就叫DetailAbility。也可以手动新建一个,不过要自己在module.json5里添加配置。具体方法可以参考上一节。
新建好之后就可以修改之前nav跳转到Detail页面的代码,这里就要唤起新建的这个Ability了:
.onClick(() => {
this.isShowFloatingButton(false)
// this.navPathStack.pushPathByName('detail', item) // 这里是nav跳转的
const want: Want = {
deviceId: '',
bundleName: 'com.example.myapplication',
abilityName: 'DetailAbility',
parameters: {
todo: JSON.stringify(item.toModel())
}
};
(this.getUIContext().getHostContext() as common.UIAbilityContext).startAbility(want) // 使用startAbility唤起`DetailAbility`
})
使用startAbility唤起DetailAbility的时候,还需要一个Want类型的参数。在这里指定了打开的Ability在哪里是谁。deviceId是空的,说明这个Ability在同一个设备。
再新建一个DetailPage,目前这个页面只显示了Detail Page文本。稍后更改这个页面。
注意,这一步不做页面显示白板。在entry/src/main/resources/profile/main_pages.json文档添加新建的这个页面。
{
"src": [
"pages/Index",
"pages/DetailPage"
]
}
Ability加载的页面都需要在这里添加配置。
但是,这个打不开Detail页面,默认的加载路径不是Detail页面:
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => { /// ***
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
在windowStage.loadContent方法中第一个参数就是页面的位置,自动生成的时候给的是:pages/Index, 但是我们要打开的是Detail页面。修改页面位置为pages/DetailPage就可以打开了。
效果是这样的:
一个Ability,对应在任务栏显示一个任务。这里有两个,默认的一个和新建的DetailAbility。
显示todo详情
现在要使用Want中的parameters了,这里传递了一个todo。但是转成了JSON字符串。而且传递的是一个Model实例。
在DetailAbility的onCreate中可以拿到这个want的实例,并从parameters中拿到这个json串。反序列化并使用:
const todo = want?.parameters?.['todo'];
if (!todo) {
hilog.error(DOMAIN, 'testTag', 'todo is null');
return;
}
const todoInfo = new TodoViewModel( JSON.parse(todo as string) as TodoModel);
AppStorageV2.connect<TodoViewModel>(TodoViewModel, 'todo_detail', () => todoInfo)
在使用的时候得到model实例,并转成viewmodel后使用。
注意,在DetailPage页面的onPageHide生命周期回调中需要关闭这个Ability:
onPageHide(): void {
(this.getUIContext().getHostContext() as common.UIAbilityContext).terminateSelf()
}
否则,没有办法在点击其他的todo的时候显示对应的todo的title。详情查看后面的UIAbility数据同步章节。
Ability的生命周期
onCreate
onWindowStageCreate
这里注意,加载页面是在onWindowStageCreate这个方法进行的:
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => { /// *
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
在onWindowStageCreate方法中使用windowStage.loadContent('pages/Index', ()=>{})加载了页面。
也可以在这个方法里订阅获焦/失焦、切到前台/切到后台、前台可交互/前台不可交互等事件。详细文档在这里。
onForeground
这是Ability的UI可见之前的最后一个回调,在这里申请需要的系统资源。
import { UIAbility } from '@kit.AbilityKit';
// ···
export default class EntryAbility extends UIAbility {
// ···
onForeground(): void {
// 申请系统需要的资源,或者重新申请在onBackground()中释放的资源
}
// ···
}
onBackground
在Ability的UI完全不见之后,触发onBackground回调。开发者可以在这个回调中释放不需要的系统资源,比如停止定位功能等。
onWindowStageWillDestroy
Ability在销毁之前,系统触发这个回调。这个时候WindowStage还没有销毁,还可以用。
onWindowStageWillDestroy(windowStage: window.WindowStage): void {
// 释放通过windowStage对象获取的资源
// 在onWindowStageWillDestroy()中注销WindowStage事件订阅(获焦/失焦、切到前台/切到后台、前台可交互/前台不可交互)
try {
if (windowStage) {
windowStage.off('windowStageEvent');
}
} catch (err) {
let code = (err as BusinessError).code;
let message = (err as BusinessError).message;
hilog.error(DOMAIN, 'testTag', `Failed to disable the listener for windowStageEvent. Code is ${code}, message is ${message}`);
}
}
onWindowStageDestroy
在这里WindowStage还是没有销毁。可以在这里释放UI资源。
onDestroy
UIAbility的最后一个生命周期回调。可以在这里做最后的资源释放,清理等工作。
onNewWant
在Ability实例已经创建,再次调用方法启动这个Ability的时候会触发这个回调。可以在这里更新加载的资源或者数据。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
// ···
export default class EntryAbility extends UIAbility {
// ···
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
// 更新资源、数据
}
}
UIAbility内数据同步
UIAblity和它内部的页面同步数据的方法有两种,一个是使用AppStorageV2(当然PersistenceV2也可以)和事件的方式。
使用AppStorageV2
export default class DetailAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const todo = want?.parameters?.['todo'];
if (!todo) {
hilog.error(DOMAIN, 'testTag', 'todo is null');
return;
}
const todoInfo = new TodoViewModel(JSON.parse(todo as string) as TodoModel);
AppStorageV2.connect<TodoViewModel>(TodoViewModel, 'todo_detail', () => todoInfo)
}
// 其他略
}
把want参数的序列化的字符串解析出来后,转成viewmodel然后通过AppStorage实现App全局共享,这样在DetailPage就可以拿到了。
使用EventHub事件的方式
使用EventHub的emit方法发出事件,使用EventHub的on接收事件。用完之后可以使用off方法取消该事件订阅。
在Ability里接收,在页面发出。
接收:
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
// ···
const DOMAIN = 0x0000;
const TAG: string = '[EventAbility]';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 获取eventHub
let eventhub = this.context.eventHub;
// 执行订阅操作
eventhub.on('event1', this.eventFunc);
eventhub.on('event1', (data: string) => {
// 触发事件,完成相应的业务操作
});
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onCreate');
}
eventFunc(argOne: object, argTwo: object): void {
hilog.info(DOMAIN, TAG, '1. ' + `${argOne}, ${argTwo}`);
return;
}
// ···
}
发出:
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct EventHubPage {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
eventHubFunc(): void {
// 不带参数触发自定义“event1”事件
this.context.eventHub.emit('event1');
// 带1个参数触发自定义“event1”事件
this.context.eventHub.emit('event1', 1);
// 带2个参数触发自定义“event1”事件
this.context.eventHub.emit('event1', 2, 'test');
// 开发者可以根据实际的业务场景设计事件传递的参数
}
build() {
Column() {
List({ initialIndex: 0 }) {
ListItem() {
Row() {
// ···
}
.onClick(() => {
this.eventHubFunc();
this.getUIContext().getPromptAction().showToast({
message: 'EventHubFuncA'
});
})
// ···
}
ListItem() {
Row() {
// ···
}
.onClick(() => {
this.context.eventHub.off('event1');
this.getUIContext().getPromptAction().showToast({
message: 'EventHubFuncB'
});
})
// ···
}
}
// ···
}
// ···
}
}
Ability之间跳转
文档在这里。
Ability的冷启动和热启动:
- 冷启动,就是这个Ability在内存中不存在。这次启动需要完成Ability的初始化和启动的动作。
- 热启动,这个Ability已经存在于内存中,但是不可见。启动的时候不需要在执行初始化的逻辑,只会触发
onNewWant生命周期方法。
启动一个Ability
const want: Want = {
deviceId: '', // 1
bundleName: 'com.example.myapplication',
moduleName: 'entry', // 2
abilityName: 'DetailAbility',
parameters: {
todo: JSON.stringify(item.toModel())
}
};
(this.getUIContext().getHostContext() as common.UIAbilityContext).startAbility(want)
接收这些want的数据:
-
deviceId,这里是空,表示本设备。 - 指定
moduleName,非必需。
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const todo = want?.parameters?.['todo'];
if (!todo) {
hilog.error(DOMAIN, 'testTag', 'todo is null');
return;
}
const todoInfo = new TodoViewModel(JSON.parse(todo as string) as TodoModel);
AppStorageV2.connect<TodoViewModel>(TodoViewModel, 'todo_detail', () => todoInfo)
}
启动一个Ability并获得返回结果
这次使用startAbilityForResult,这个方法可以打开一个Ability并获得返回结果。它返回一个Promise,所以获取返回结果需要用到then,或者使用await。
代码:
// 略
@Entry
@Component
struct MainPage {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
build() {
Column() {
List({ initialIndex: 0, space: 8 }) {
// ···
ListItem() {
Row() {
// ···
}
.onClick(() => {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext; // UIAbilityContext
const RESULT_CODE: number = 1001; // 1
let want: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.uiabilityinteraction',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbilityA',
parameters: {
// 自定义信息
// app.string.main_page_return_info资源文件中的value值为'来自EntryAbility MainPage页面'
info: $r('app.string.main_page_return_info')
}
};
context.startAbilityForResult(want).then((data) => { // 2
if (data?.resultCode === RESULT_CODE) {
// 解析被调用方UIAbility返回的信息
let info = data.want?.parameters?.info;
hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(info) ?? '');
if (info !== null) {
this.getUIContext().getPromptAction().showToast({
message: JSON.stringify(info)
});
}
}
hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(data.resultCode) ?? '');
}).catch((err: BusinessError) => { // 3
hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability for result. Code is ${err.code}, message is ${err.message}`);
});
})
}
// ···
}
// ···
}
// ···
}
}
- 设置一个返回结果的唯一标识,别接受了别的Ability的返回结果。
- 开始一个Ability并处理返回值:
startAbilityForResult(want).then((data) => {}。 - 处理异常。
但是被打开的Ability怎么把结果返回回去呢,使用terminateSelfWithResult()方法。代码:
const RESULT_CODE: number = 1001; // FuncAbilityA返回的结果
let abilityResult: common.AbilityResult = {
resultCode: RESULT_CODE,
want: {
bundleName: 'com.samples.uiabilityinteraction',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbilityA',
parameters: {
// app.string.ability_return_info资源文件中的value值为'来自FuncAbility Index页面'
info: $r('app.string.ability_return_info')
},
},
};
context.terminateSelfWithResult(abilityResult, (err) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to terminate self with result. Code is ${err.code}, message is ${err.message}`);
return;
}
});
状态管理
状态管理上,有V1和V2两个版本。推荐使用的是V2版本。在具体的开发中结合MVVM模式使用。
V1
略
V2
以下的装饰器都只能在ComponentV2装饰的组件内部使用。@ObservedV2和@Trace除外。
@Local
只在组件内部使用。
@Entry
@ComponentV2
struct Index {
// 点击的次数
@Local count: number = 0;
@Local message: string = 'Hello';
@Local flag: boolean = false;
build() {
Column() {
Text(`${this.count}`)
Text(`${this.message}`)
Text(`${this.flag}`)
Button('change Local')
.onClick(() => {
// 当@Local装饰简单类型时,能够观测到对变量的赋值
this.count++;
this.message += ' World';
this.flag = !this.flag;
})
}
}
}
@Param
接收付组件传入的值。
// 子组件
@ComponentV2
struct Child {
@Param count: number = 0; /// 1
@Require @Param message: string;
@Require @Param flag: boolean;
build() {
Column() {
Text(`Param ${this.count}`)
Text(`Param ${this.message}`)
Text(`Param ${this.flag}`)
}
}
}
// 父组件
@Entry
@ComponentV2
struct Index {
// 点击的次数
@Local count: number = 0;
@Local message: string = 'Hello';
@Local flag: boolean = false;
build() {
Column() {
Text(`Local ${this.count}`)
Text(`Local ${this.message}`)
Text(`Local ${this.flag}`)
Button('change Local')
.onClick(() => {
// 对数据源的更改会同步给子组件
this.count++;
this.message += ' World';
this.flag = !this.flag;
})
Child({ /// 2
count: this.count,
message: this.message,
flag: this.flag
})
}
}
}
@Event
接收父组件传入的方法,用来更新父组件的数据源。
// 子组件
@ComponentV2
struct Child {
@Param title: string = '';
@Param fontColor: Color = Color.Black;
@Event changeFactory: (x: number) => void = (x: number) => {}; /// 1
build() {
Column() {
Text(`${this.title}`)
.fontColor(this.fontColor)
Button('change to Title Two')
.onClick(() => {
this.changeFactory(2); /// 2
})
Button('change to Title One')
.onClick(() => {
this.changeFactory(1);
})
}
}
}
// 父组件
@Entry
@ComponentV2
struct Index {
@Local title: string = 'Title One';
@Local fontColor: Color = Color.Red;
build() {
Column() {
Child({
title: this.title,
fontColor: this.fontColor,
changeFactory: (type: number) => { /// 3
if (type == 1) {
this.title = 'Title One';
this.fontColor = Color.Red;
} else if (type == 2) {
this.title = 'Title Two';
this.fontColor = Color.Green;
}
}
})
}
}
}
- 在子组件定义一个
@Event回调。 - 在子组件调用这个回调。
- 在父组件传递一个箭头函数给子组件定义的。
在父组件给出的定义中,子组件调用这个回调的时候可以修改父组件的title和fontColor两个数据。
@ObservedV2和@Trace
用于监视对象内部属性的变化。
在上面描述的装饰器中,如果是一个对象,那么只能监听到赋值的变化。如果是一个数组等数据集合,一般只能监听到集合内部API的调用引起的变化。如果修改了数据某个对象的属性的值,是无法监听到的,也就是这样的修改不会出现在界面上。
这就需要用到@ObservedV2和@Trace的装饰器组合。
-
@ObservedV2装饰类。 -
@Trace装饰需要观察的属性。
注意:@ObservedV2装饰的类要在new出来之后才有观察变化的能力。
代码:
@ObservedV2 /// 1
class Son {
@Trace public age: number = 100; /// 2
}
class Father {
public son: Son = new Son(); /// 3
}
@Entry
@ComponentV2
struct Index {
father: Father = new Father(); /// 4
build() {
Column() {
// 当点击改变age时,Text组件会刷新
Text(`${this.father.son.age}`) /// 5
.onClick(() => {
this.father.son.age++; /// 6
})
}
}
}
- 使用
@ObservedV2装饰类。 - 使用
@Trace装饰需要监听的属性age。 - 被
@Observed装饰的类在另一个类力使用。不是使用的必要步骤,只是说明监听的穿透力强 - 初始化需要监听的类。必须初始化才可以监听到变化。
- 如果属性的值发生变化,而且被监听到了,那么在界面上显示对应的变化。
- 在点击事件中更改
@Trace装饰的属性。
@Provider / @Consumer
跨组件双向同步数据。遇到同名的时候,使用组件树上最近的那个。
定义的时候是这样的:
-
@Provider(aliasName?: string) varName : varType = initValue,aliasName为空的时候使用属性名作为aliasName。 -
@Consumer(aliasName?: string) varName : varType = initValue。aliasName,如果为空就是属性名,是@Provider和@Consumer关联的唯一key值。
@ComponentV2
struct Parent {
// 未定义aliasName, 使用属性名'str'作为aliasName
@Provider() str: string = 'hello';
}
@ComponentV2
struct Child {
// 定义aliasName为'str',使用aliasName去寻找
// 能够在Parent组件上找到, 使用@Provider的值'hello'
@Consumer('str') str: string = 'world';
}
- 这两个需要在同一个组件树中,在这个组件树的不同层级双向同步数据。
- 如果
@Consumer找不到对应的@Provider,则使用本地的默认值。
@Monitor
装饰一个方法,可以在装饰器参数指定监视的状态。被监听的状态变化的时候触发Monitor装饰的方法。
import { hilog } from '@kit.PerformanceAnalysisKit';
@Entry
@ComponentV2
struct Index {
@Local message: string = 'Hello World';
@Local name: string = 'Tom';
@Local age: number = 24;
@Monitor('message', 'name')
onStrChange(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
hilog.info(0xFF00, 'testTag', '%{public}s',
`${path} changed from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
});
}
build() {
Column() {
Button('change string')
.onClick(() => {
this.message += '!';
this.name = 'Jack';
})
}
}
}
也可以监听被@Trace装饰的属性的变化:
监听@Trace装饰的属性
@Monitor('info')
infoChange(monitor: IMonitor) {
hilog.info(0xFF00, 'testTag', '%{public}s', `info change`);
}
@Monitor('info.name') ///*
infoPropertyChange(monitor: IMonitor) {
hilog.info(0xFF00, 'testTag', '%{public}s', `info name change`);
}
属性name有@Trace装饰的时候可以被@Monitor监听到。
监听多个
@Monitor('region', 'job') /// *
onChange(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
hilog.info(0xFF00, 'testTag', '%{public}s',
`${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
})
}
可以在@ObservedV2装饰的类中使用
@Computed
装饰一个get属性,从一个或者多个获取到一个最终值。避免多个状态变化是多次计算。只做读,不做写。
@Computed
get sum() {
return this.count1 + this.count2 + this.count3;
}
@Type
你的类需要被序列化,这时你的类里面还定义了一个属性,这个属性的类型也是一个类。为了不在序列化的时候丢失这个属性的类型可以用@Type来装饰属性。
注意:
- 构造函数不包含参数
-
一般配合
PersistenceV2一起使用。
class Sample {
private data: number = 0;
}
@ObservedV2
class Info {
@Type(Sample)
@Trace public sample: Sample = new Sample(); // 正确用法
}
@AppStorageV2
整个app的状态管理,可以跨UIAbility共享数据。
定义:
AppStorageV2.connect(/* 参数 */)
代码:
AppStorageV2.connect<ThemeModel>(ThemeModel, 'app_theme', () => new ThemeModel(initialColorModel))
connect
这个方法用来创建或者获取存储的数据。
connect的参数。第一个是存放的类型。第二个可以是key,也可以是类型的默认构造器。如果第二个不是默认构造器,或者第二个参数不合法,那么第二个必须是默认构造器。
remove
使用remove删除指定的key对应的数据
keys
使用keys可以得到AppStorageV2中的全部key。
注意:
AppStorageV2只支持class类型。只能在UI线程使用。不支持collections.Set,collections.Map等类型。
@PersistenceV2
整个app的状态管理,另外带有持久化功能。
和AppStorageV2一样,只是这个可以持久化存储。在第二次打开app的时候对应的状态不会丢失。
MVVM
App的代码组织模式。
简单的应用可以使用这样的模式来实现。
基本的组成有:
- Model层,负责管理数据。
- ModelView层,连接Model和View,负责管理UI的状态和业务逻辑。通过监控Model数据的变化,处理业务逻辑并将数据更新到View层。
- UI层,展示数据和与用户交互。
另外还有对数据库或者服务器操作的repository层。
示例代码在这里。作为一个Todo类App的Model层,这里定义了TodoModel和TodoListModel。
Model
在TodoModel中,只负责持有数据。在TodoListModel中则需要借助repository处理本地SQLite的数据。入:
import { TodoModel } from './TodoModel';
import { TodoRepository } from '../libs/repository';
export class TodoListModel {
private _todos: TodoModel[] = []; /// 1
private _repo?: TodoRepository; /// 2
// 略
async loadTodoList() {
if (!this._repo) {
this._repo = await TodoRepository.getInstance(this._context)
}
const todoList = await this._repo?.queryAll() /// 3
this._todos = todoList ?? []
}
// 其他略
}
-
TodoListModel的数据,一组TodoModel实例。 -
TodoRepository,用来管理本地数据库的数据。 - 使用repository获取数据。
ModelView
import { TodoModel } from '../model/TodoModel'
@ObservedV2
export class TodoViewModel {
id: number = 0;
createdAt: number = Date.now();
updatedAt: number = Date.now();
@Trace title: string = ''; // 标题
@Trace description: string = ''; // 描述
@Trace completed: boolean = false; // 完成状态
@Trace version: number = 0; // 版本(用于乐观锁)
// 其他略
}
这里就使用了@ObservedV2和@Trace来监听数据的变化。
import { Type } from '@kit.ArkUI';
import { TodoListModel } from '../model/TodoListModel';
import { TodoModel } from '../model/TodoModel';
import { TodoViewModel } from './TodoViewModel';
@ObservedV2
export class TodoListViewModel {
@Trace todoList: TodoViewModel[] = [];
private _todoListModel?: TodoListModel
// TODO: Add error message for display
// 略
async addTodo(title: string, description: string = '') {
const todo: TodoModel = new TodoModel({ title, description });
await this._todoListModel?.addTodo(todo);
this.todoList.push(new TodoViewModel(todo));
}
// 其他略
}
在TodoListViewModel的Model数据就是_todoListModel,并使用@Trace进行深度监听。
UI / View
@Entry
@ComponentV2
struct Index {
// 略
@Local todoList: TodoListViewModel = new TodoListViewModel(this.getUIContext().getHostContext())
// 其他略
}
在视图中把TodoListViewModel的示例使用@Local装饰。这样就把ViewModel这个模式的各个要素串联到了一起。
导航
这里介绍Stack导航和Tab导航。
Navigation
Navigation四件套:
-
Navigation组件,这个必不可少。 -
NavPathStack实例。实际控制导航到哪里。 - 页面映射关系。这个定义在一个
@Builder装饰的方法里。 -
NavDestination。导航的目标“页”中最外面的组件就是NavDestination。
代码如下:
// ...
@Entry
@ComponentV2
struct Index {
navStack: NavPathStack = new NavPathStack() // 1
build() {
Navigation(this.navStack) { // 2
Column() {
ThemedButton({ message: '弹窗' })
.padding(10)
.onClick(() => {
this.navStack.pushPath({ name: 'Pops' }) // 3
})
}
// ...
}
.navDestination(this.pageMap) // 4
}
@Builder
pageMap(name: string) { // 5
if (name === "Pops") {
PopupSamples()
}
}
}
目标页:
@Preview
@ComponentV2
export struct PopupSamples {
build() {
NavDestination() { 、、 6
Column() {
Text('PopupSamples')
}
}
}
}
-
NavPathStack,作为页面的成员初始化。 -
Navigation,这就应该出现了,在布局顶层。并把NavPathStack的实例作为成员传入。这两个就关联在一起了。 - 使用
NavPathStack成员执行导航。 - 在
Navigation的属性方法中配置可以导航的页面。 - 定义页面地图。映射导航的名称和对应的组件。
更多高级内容稍后补充。。。
Tab
@Preview
@ComponentV2
export struct TabsSamples {
build() {
NavDestination() {
Tabs({barPosition: BarPosition.End}) { // 1
TabContent() { // 2
Text('Tab 1')
}
.tabBar("Tab 1") // 3
TabContent() {
Text('Tab 2')
}
.tabBar("Tab 2")
}
}
}
}
Tab布局的实现需要三件套: Tabs、TabContent和tabBar属性方法。
上面的代码中:
-
Tabs,Tab布局的总体设置都在这里。比如在上面的例子中配置tab bar的位置在底部。 -
TabContent,每个tab的内容容器。上例的内容为一个Text组件。 -
tabBar,在这配置tab按钮
访问网络
首先,可以使用如下代码实现一个极简的服务器,get请求后可以返回一个json串来验证鸿蒙网络请求正确与否。
如下的bash命令直接在terminal运行即可跑起来一个server。
node版本:
node -e "require('http').createServer((req, res) => { res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({status: 'ok'})); }).listen(8000); console.log('Server running on port 8000')"
python:
python3 -c "from http.server import HTTPServer, BaseHTTPRequestHandler; import json; class S(BaseHTTPRequestHandler): do_GET = do_POST = lambda s: (s.send_response(200), s.send_header('Content-Type', 'application/json'), s.end_headers(), s.wfile.write(json.dumps({'status': 'ok'}).encode())); HTTPServer(('0.0.0.0', 8000), S).serve_forever()"
也可以直接使用这个地址:
https://jsonplaceholder.typicode.com/posts/1
使用http模块
async sendHttpRequest() {
this.httpLoading = true
this.httpResponseText = ''
try {
const httpRequest = http.createHttp() /// 1
const url = 'https://jsonplaceholder.typicode.com/posts/1'
const response = await httpRequest.request(url, { /// 2
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json'
},
connectTimeout: 60000,
readTimeout: 60000
})
if (response.responseCode === 200) {
this.httpResponseText = JSON.stringify(JSON.parse(response.result as string), null, 2)
} else {
this.httpResponseText = `请求失败: HTTP ${response.responseCode}`
}
httpRequest.destroy() /// 3
} catch (error) {
this.httpResponseText = `请求出错: ${JSON.stringify(error)}`
} finally {
this.httpLoading = false
}
}
- 使用
http.createHttp()新建一个http请求实例:httpRequest。 - 使用
httpRequest请求服务器,并配置http method,请求的url地址等。 - 最后要销毁
httpRequest。
使用Axios访问网络:
async sendAxiosRequest() {
this.axiosLoading = true
this.axiosResponseText = ''
try {
const response:AxiosResponse<string> = await axios.get('https://jsonplaceholder.typicode.com/posts/1', {
headers: {
'Content-Type': 'application/json'
},
timeout: 60000
}) /// 1
this.axiosResponseText = JSON.stringify(response.data, null, 2)
} catch (error) {
this.axiosResponseText = `请求出错: ${JSON.stringify(error)}`
} finally {
this.axiosLoading = false
}
}
使用axios就简单多了。直接使用axios.get请求。
数据存储
SQLite, 四个读连接,一个写连接。
代码:
import { relationalStore, ValuesBucket } from '@kit.ArkData'
import { Context } from '@kit.AbilityKit'
import { TodoModel } from '../model/TodoModel'
export const DB_NAME = 'todo_db.db'
export const DB_VERSION = 1
export const CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS todo_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
completed INTEGER DEFAULT 0,
version INTEGER DEFAULT 1,
createdAt INTEGER,
updatedAt INTEGER
)
`
export const STORE_CONFIG: relationalStore.StoreConfig = {
name: DB_NAME,
securityLevel: relationalStore.SecurityLevel.S1
}
export class TodoRepository {
private static instance: TodoRepository
private rdbStore: relationalStore.RdbStore | null = null
private context: Context | null = null
static async getInstance(context?: Context): Promise<TodoRepository> {
if (!TodoRepository.instance) {
if (!context) {
throw new Error('Context must be provided for first initialization')
}
TodoRepository.instance = new TodoRepository()
await TodoRepository.instance.init(context)
}
return TodoRepository.instance
}
private async init(context: Context): Promise<void> {
this.context = context
this.rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG)
await this.rdbStore.executeSql(CREATE_TABLE_SQL)
}
async queryAll(): Promise<TodoModel[]> {
if (!this.rdbStore) {
this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
}
const resultSet = await this.rdbStore.querySql('SELECT * FROM todo_info ORDER BY createdAt DESC')
const list: TodoModel[] = []
while (resultSet.goToNextRow()) {
const todo = new TodoModel()
todo.id = resultSet.getLong(resultSet.getColumnIndex('id'))
todo.title = resultSet.getString(resultSet.getColumnIndex('title'))
todo.description = resultSet.getString(resultSet.getColumnIndex('description'))
todo.completed = resultSet.getLong(resultSet.getColumnIndex('completed')) === 1
todo.createdAt = resultSet.getDouble(resultSet.getColumnIndex('createdAt'))
todo.updatedAt = resultSet.getDouble(resultSet.getColumnIndex('updatedAt'))
list.push(todo)
}
resultSet.close()
return list
}
async getTodoById(id: number): Promise<TodoModel | null> {
if (!this.rdbStore) {
this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
}
const predicates = new relationalStore.RdbPredicates('todo');
predicates.equalTo('id', id);
const columns = ['id', 'title', 'description', 'completed', 'version', 'createdAt', 'updatedAt'];
const result = await this.rdbStore.query(predicates, columns);
if (result.goToNextRow()) {
const todo = TodoModel.fromDatabase({
id: result.getDouble(result.getColumnIndex('id')),
title: result.getString(result.getColumnIndex('title')),
description: result.getString(result.getColumnIndex('description')),
completed: result.getDouble(result.getColumnIndex('completed')),
version: result.getDouble(result.getColumnIndex('version')),
createdAt: result.getDouble(result.getColumnIndex('createdAt')),
updatedAt: result.getDouble(result.getColumnIndex('updatedAt'))
});
result.close();
return todo;
}
result.close();
return null;
}
async insert(todo: TodoModel): Promise<void> {
if (!this.rdbStore) {
this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
}
const valueBucket: ValuesBucket = {
// id: todo.id,
title: todo.title,
description: todo.description,
completed: todo.completed ? 1 : 0,
createdAt: todo.createdAt
}
await this.rdbStore.insert('todo_info', valueBucket)
}
async updateStatus(todo: TodoModel): Promise<void> {
const updateStatement = 'UPDATE todo_info SET ' + todo.toKeyValuePairs() + ` WHERE id = ${todo.id}}`
if (!this.rdbStore) {
this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
}
await this.rdbStore.executeSql(updateStatement)
}
async deleteTodo(id: number): Promise<boolean> {
if (!this.rdbStore) {
throw new Error('数据库未初始化');
}
const predicates = new relationalStore.RdbPredicates('todo_info');
predicates.equalTo('id', id);
const affectedRows = await this.rdbStore.delete(predicates);
return affectedRows > 0;
}
}
通知和推送
异步编程
有两种方式实现,一个是异步并发使用promise和async/await实现。依靠单线程的事件循环。
另外一种就是多线程并发。使用TaskPool和Worker实现。
异步并发
使用Promise或者async/await实现。如:
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
const randomNumber: number = Math.random();
if (randomNumber > 0.5) {
resolve(randomNumber);
} else {
reject(new Error('Random number is too small'));
}
}, 1000);
})
Async/Await
async function myAsyncFunction(): Promise<string> {
const result: string = await new Promise((resolve: Function) => {
setTimeout(() => {
resolve('Hello, world!');
}, 3000);
});
console.info(result); // 输出: Hello, world!
return result;
}
多线程并发
多线程并发可以使用taskpool和Worker实现。
Taskpool
底层原理是Actor模型。开发中可以使用TaskPool或者Worker实现。详细的文档可以看这里。
简言之,Actor模型的多个线程之间不同享内存,一个线程就是一个Actor。多个线程需要通信则互发消息。
taskpool的典型用法,看代码:
import { taskpool } from '@kit.ArkTS';
import { sleep } from '../utils';
@Concurrent // => 1
async function generateNumber(ms: number): Promise<number> {
await sleep(ms);
return Math.random();
}
@ComponentV2
export struct AsyncSamples {
@Local message: string = 'Async Samples';
aboutToAppear(): void {
const task = new taskpool.Task(generateNumber, 2000); // => 2
taskpool.execute(task).then((result) => { // => 3
console.log('Task result', result);
});
}
build() {
NavDestination() {
Column() {
Text(this.message)
}
// ...
}
}
}
注意:这里只说鸿蒙文档的典型用法,其他用法在后面会提到。
上面的代码,首先引入taskpool。
- 定义一个并发方法,通过
@Concurrent这个装饰器装饰一个方法实现。这个方法可以是一个async方法,也可以不是。 - 用定义好的并发方法新建一个
taskpool.Task实例。如果这个并发方法需要一个参数,那么在定义Task的时候在第二个参数给出。 - 使用
taskpool.execute执行前一步定义好的task。在then里获取执行的结果,并更新组件的状态。task的结果就显示在界面上了。
在定义并发方法的时候,方法内部使用的只能是局部变量,入参和import引入的变量。
taskpool也可以这样:
function someFunc(param: string) {
//...
}
taskpool.execute(someFunc, param).then((ret) => { // 定期执行可以使用executePeriodically
// ...
});
Worker
Worker实现三步:
- 新建一个新的文件放worker的代码。
- 新建
worker.ThreadWorker实例。 - 宿主现场和子线程之间互传消息。
具体实现如下:
、
点了Start worker之后开始执行线程代码。每次更新progress bar的进度,一直到达到百分之百。
代码如下:
import { MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort; /// 1
let progressTimer: number | null = null
let currentProgress: number = 0
workerPort.onmessage = (e: MessageEvents) => { /// 2
const message:string = e.data
if (message=== 'START') {
currentProgress = 0
// 清除旧定时器
if (progressTimer) {
clearInterval(progressTimer)
}
// 每 300ms 发送进度
progressTimer = setInterval(() => {
currentProgress += 2 // 每次增加 2%
// 发送进度回主线程
workerPort.postMessage({ /// 3
type: 'PROGRESS_UPDATE',
value: currentProgress
})
// 完成时清理
if (currentProgress >= 100) {
if (progressTimer) {
clearInterval(progressTimer)
progressTimer = null
}
workerPort.postMessage({ type: 'COMPLETED' })
}
}, 300)
}
if (message=== 'STOP') {
if (progressTimer) {
clearInterval(progressTimer)
progressTimer = null
}
}
}
- 初始化
workerPost实例,这是线程互通的关键。 - 接受主线程的消息,根据
START、STOP开始或者停止发送消息。 - 在本线程给主线程发送消息:
workerPort.postMessage。
在UI:
ThemedButton({
message: this.progressText, handleClick: () => {
let workerInstance = new worker.ThreadWorker('entry/ets/workers/worker.ets'); /// 1
workerInstance.postMessage('START'); /// 2
workerInstance.onmessage = ((e: MessageEvents) => { /// 3
const data: WorkerMessage = e.data as WorkerMessage;
if (data.type === 'PROGRESS_UPDATE') {
this.progressValue = data.value;
this.status = 'running';
} else if (data.type === 'COMPLETED') {
this.progressValue = 100;
this.status = 'completed';
}
});
}
})
- 使用定义好的worker.ets文件新建worker实例。
- 给子线程发送消息:
workerInstance.postMessage()。这里通知子线程开始执行。 - 接收子线程发送过来的消息,并更新界面。
注意:在处理worker文件的时候有些注意事项,请看这里。
Taskpool和Worker的对比,看这里。