HarmonyOS应用六之应用程序进阶一
目录:
- 1、UIAbility的冷启动和UIAbility热启动
- 2、静态资源和动态资源的访问
- 3、页面跳转
- 3.1、页面返回跳转
- 4、HAR的ArkUI组件、接口、资源,供其他应用或当前应用的其他模块引用
- 4.1、导出HAR的ArkUI组件
- 4.2、引用HAR的ArkUI组件
- 5、循环渲染
- 6、状态管理最佳实践
- 7、AppStorage的使用精细化拆分复杂状态
1、UIAbility的冷启动和UIAbility热启动
- UIAbility冷启动:指的是UIAbility实例处于完全关闭状态下被启动,这需要完整地加载和初始化UIAbility实例的代码、资源等。
目标UIAbility冷启动时,在目标UIAbility的onCreate()生命周期回调中,接收调用方传过来的参数。然后在目标UIAbility的onWindowStageCreate()生命周期回调中,解析调用方传递过来的want参数,获取到需要加载的页面信息url,传入windowStage.loadContent()方法。
import { AbilityConstant, Want, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window, UIContext } from '@kit.ArkUI';
const DOMAIN_NUMBER: number = 0xFF00;
const TAG: string = '[EntryAbility]';
export default class EntryAbility extends UIAbility {
funcAbilityWant: Want | undefined = undefined;
uiContext: UIContext | undefined = undefined;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 接收调用方UIAbility传过来的参数
this.funcAbilityWant = want;
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onWindowStageCreate');
// Main window is created, set main page for this ability
let url = 'pages/Index';
if (this.funcAbilityWant?.parameters?.router && this.funcAbilityWant.parameters.router === 'funcA') {
url = 'pages/Page_ColdStartUp';
}
windowStage.loadContent(url, (err, data) => {
// ...
});
}
}
- UIAbility热启动:指的是UIAbility实例已经启动并在前台运行过,由于某些原因切换到后台,再次启动该UIAbility实例,这种情况下可以快速恢复UIAbility实例的状态。
在应用开发中,会遇到目标UIAbility实例之前已经启动过的场景,这时再次启动目标UIAbility时,不会重新走初始化逻辑,只会直接触发onNewWant()生命周期方法。为了实现跳转到指定页面,需要在onNewWant()中解析参数进行处理。
例如短信应用和联系人应用配合使用的场景。
1、用户先打开短信应用,短信应用的UIAbility实例启动,显示短信应用的主页。
2、用户将设备回到桌面界面,短信应用进入后台运行状态。
3、用户打开联系人应用,找到联系人张三。
4、用户点击联系人张三的短信按钮,会重新启动短信应用的UIAbility实例。
5、由于短信应用的UIAbility实例已经启动过了,此时会触发该UIAbility的onNewWant()回调,而不会再走onCreate()和onWindowStageCreate()等初始化逻辑。
import { AbilityConstant, Want, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import type { Router, UIContext } from '@kit.ArkUI';
import type { BusinessError } from '@kit.BasicServicesKit';
const DOMAIN_NUMBER: number = 0xFF00;
const TAG: string = '[EntryAbility]';
export default class EntryAbility extends UIAbility {
funcAbilityWant: Want | undefined = undefined;
uiContext: UIContext | undefined = undefined;
// ...
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want?.parameters?.router && want.parameters.router === 'funcA') {
let funcAUrl = 'pages/Page_HotStartUp';
if (this.uiContext) {
let router: Router = this.uiContext.getRouter();
router.pushUrl({
url: funcAUrl
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to push url. Code is ${err.code}, message is ${err.message}`);
});
}
}
}
}
热启动是前提得有冷启动,就是应用启动后处于后台运行,当从其他位置点击进来需要进入后台运行的应用程序时就需要使用到热启动。
2、静态资源和动态资源的访问
在组件中,经常需要使用字符串、图片等资源。HSP中的组件需要使用资源时,一般将其所用资源放在HSP包内,而非放在HSP的使用方处,以符合高内聚低耦合的原则。
在工程中,常通过 r / r/ r/rawfile的形式引用应用资源。可以用 r / r/ r/rawfile访问本模块resources目录下的资源,如访问resources目录下定义的图片src/main/resources/base/media/example.png时,可以用$r(“app.media.example”)
// library/src/main/ets/pages/Index.ets
// 正确用例
Image($r('app.media.example'))
.id('example')
.borderRadius('48px')
// 错误用例
Image("../../resources/base/media/example.png")
.id('example')
.borderRadius('48px')
将需要对外提供的资源封装为一个资源管理类:
// library/src/main/ets/ResManager.ets
export class ResManager{
static getPic(): Resource{
return $r('app.media.pic');
}
static getDesc(): Resource{
return $r('app.string.shared_desc');
}
}
对外暴露的接口,需要在入口文件index.ets中声明:
// library/index.ets
export { ResManager } from './src/main/ets/ResManager';
3、页面跳转
其中router.pushUrl方法的入参中url的内容为:
'@bundle:com.samples.hspsample/library/ets/pages/Menu'
url内容的模板为:
'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)'
3.1、页面返回跳转
页面返回router.back方法的入参中url说明:
如果从HSP页面返回HAP页面,url的内容为:
'pages/Index'
url内容的模板为:
'页面所在的文件名(不加.ets后缀)'
如果从HSP1的页面跳到HSP2的页面后,需要返回到HSP1的页面,url的内容为:
'@bundle:com.samples.hspsample/library/ets/pages/Menu'
url内容的模板为:
'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)'
4、HAR的ArkUI组件、接口、资源,供其他应用或当前应用的其他模块引用
4.1、导出HAR的ArkUI组件
Index.ets文件是HAR导出声明文件的入口,HAR需要导出的接口,统一在Index.ets文件中导出。Index.ets文件是DevEco Studio默认自动生成的,用户也可以自定义,在模块的oh-package.json5文件中的main字段配置入口声明文件,配置如下所示:
{
"main": "Index.ets"
}
导出ArkUI组件
ArkUI组件的导出方式与ts的导出方式一致,通过export导出ArkUI组件,示例如下:
// library/src/main/ets/components/mainpage/MainPage.ets
@Component
export struct MainPage {
@State message: string = 'HAR MainPage';
build() {
Column() {
Row() {
Text(this.message)
.fontSize(32)
.fontWeight(FontWeight.Bold)
}
.margin({ top: '32px' })
.height(56)
.width('624px')
Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center, alignContent: FlexAlign.Center }) {
Column() {
Image($r('app.media.pic_empty')).width('33%')
Text($r('app.string.empty'))
.fontSize(14)
.fontColor($r('app.color.text_color'))
}
}.width('100%')
.height('90%')
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.page_background'))
}
}
HAR对外暴露的接口,在Index.ets导出文件中声明如下所示:
// library/Index.ets
export { MainPage } from './src/main/ets/components/mainpage/MainPage';
4.2、引用HAR的ArkUI组件
HAR的依赖配置成功后,可以引用HAR的ArkUI组件。ArkUI组件的导入方式与ts的导入方式一致,通过import引入HAR导出的ArkUI组件,示例如下所示:
// entry/src/main/ets/pages/IndexSec.ets
import { MainPage } from 'library';
@Entry
@Component
struct IndexSec {
build() {
Row() {
// 引用HAR的ArkUI组件
MainPage()
}
.height('100%')
}
}
5、循环渲染
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
}, (item: string) => item)
}
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(50)
}
}
运行效果如图所示:
此次是循环遍历数据,通过@Prop监听父组件中的值,进行字体大小为50的逻辑处理。
6、状态管理最佳实践
@Observed
class Translate {
translateX: number = 20;
}
@Entry
@Component
struct UnnecessaryState1 {
@State translateObj: Translate = new Translate(); // 同时存在读写操作,并关联了Button组件,推荐使用状态变量
buttonMsg = 'I am button'; // 仅读取变量buttonMsg的值,没有任何写的操作,直接使用一般变量即可
build() {
Column() {
Button(this.buttonMsg)
.onClick(() => {
animateTo({
duration: 50
}, () => {
this.translateObj.translateX = (this.translateObj.translateX + 50) % 150; // 点击时给变量translateObj重新赋值
})
})
}
.translate({
x: this.translateObj.translateX // 读取translateObj中的值
})
}
}
没有关联任何UI组件的状态变量和没有修改过的状态变量不应该定义为状态变量,直接使用一般变量即可,否则会影响性能。
7、AppStorage的使用精细化拆分复杂状态
对AppStorage的使用,以“HMOS世界App”中共享用户信息和用户收藏信息为例,描述如何拆分状态存储。用户信息和用户收藏信息涉及的模块和界面展示如下:
- “我的”模块顶部有展示用户信息的组件“UserInfoView”,底部有展示用户收藏列表,列表卡片上需要高亮展示用户是否点赞了当前文章。
- “探索”模块首页展示技术文章列表,列表卡片上同样需要展示用户是否点赞了当前文章。
- 当两个模块中任一模块的卡片有点赞交互时,需要同步用户是否对文章点赞的状态给另一个模块。
当前项目中已经使用AppStorage存储用户信息UserData,UserData的数据结构和“UserInfoView”组件使用UserData状态展示用户信息的代码如下:
//用户信息UserData的数据结构
export interface UserData {
id: string;
username: string;
description: string;
// ...
}
//在业务类中获取服务端用户信息
getUserData(): void {
this.userAccountRepository.getUserData().then((data: UserData) => {
//1.将用户信息数据存储到AppStorage中
AppStorage.setOrCreate('userData', data);
})
}
//“我的”模块顶部展示用户信息的视图组件
@Component
struct UserInfoView {
//2.在UI中使用@StorageLink装饰器接收AppStorage中存储的用户信息
@StorageLink('userData') userData: UserData | null = null;
build() {
Column() {
Row({ space: Constants.MIDDLE_SPACE }) {
// ...
Column() {
//3.展示用户信息userData中的用户名
Text(this.userData? this.userData.username : $r('app.string.default_login'))
// ...
}
}
// ...
}
// ...
}
}
现在“探索“模块和“我的“模块需要共享用户的收藏列表信息,只需要共享收藏的文章id数组即可。不同模块间的状态共享考虑将其也存储在AppStorage中,有如下两种存储方案:
-
收藏信息也是用户信息的一部分,将收藏信息作为用户信息userData的一个属性,存储在当前AppStorage里key值为“userData”的变量上。
-
收藏信息单独存入AppStorage中,不与用户信息userData绑定。
第一种方案的代码实现如下:
export interface UserData {
id: string;
username: string;
description: string;
// 1. 在用户信息UserData上增加用户收藏的资源列表id信息类型定义
collectedIds: string[];
// ...
}
//在业务类中获取服务端用户信息
getUserData(): void {
this.userAccountRepository.getUserData().then((data: UserData) => {
//2.将用户信息数据存储到AppStorage中
AppStorage.setOrCreate('userData', data);
})
}
// 探索模块的文章卡片组件
@Component
export struct ArticleCardView {
//3.在探索文章列表卡片上通过@StorageLink装饰器获取用户信息对象userData
@StorageLink('userData') userData: UserData | null = null;
@Prop articleItem: LearningResource = new LearningResource();
//4.根据收藏信息数组计算当前文章是否被收藏
isCollected(): boolean {
return this.userData && this.userData.collectedIds.some((id: string) => id === this.articleItem.id);
}
//7.处理界面点赞交互逻辑:使用@StorageLink装饰器接收的userData状态子属性collectedIds被修改后,新值会同步到AppStorage中
handleCollected(): void {
const index = this.userData?.collectedIds.findIndex((id: string) => id === this.articleItem.id);
if (index === -1) {
this.userData?.collectedIds.push(resourceId);
} else {
this.userData?.collectedIds.splice(index, 1);
}
// ...
}
build(){
ActionButtonView({
//5.根据当前文章是否被用户收藏,判断收藏图标是否高亮
imgResource: this.isCollected() ? $r('app.media.btn_favorites_on') : $r('app.media.btn_favorites_normal'),
count: this.articleItem.collectionCount,
textWidth: $r('app.float.star_icon_width')
})
.onClick(() => {
//6.用户点击收藏图标时,调用处理收藏状态修改的函数
this.handleCollected();
})
}
}
这种实现方案下,当用户在“UserInfoView ”组件上重新修改用户描述信息userData.description属性值时,属性值变化将同步回AppStorage中。ArkUI监听到AppStorage中key值为“userData”的值变化,随后通知所有使用了AppStorage中key值为“userData”的组件重新渲染。
在上述界面中,“我的“模块中展示用户信息的组件“UserInfoView ”会重新渲染。由于“探索”模块的文章卡片组件ArticleCardView 通过@StorageLink装饰器绑定了AppStorage中key值为“userData”的变量,所有的文章卡片组件也都会重新渲染。而这些组件与用户描述信息无关,不应该被描述信息的修改变化影响,从而导致渲染刷新。
改为使用上述第二种方案实现,代码如下:
//在业务类中获取用户信息
getUserData(): void {
this.userAccountRepository.getUserData().then((data: UserData) => {
//1.将用户收藏信息单独数据存储到AppStorage中
AppStorage.setOrCreate('collectedIds', data.collectedIds);
AppStorage.setOrCreate('userData', data);
})
}
// 探索模块的文章卡片组件
@Component
export struct ArticleCardView {
//2.通过@StorageLink装饰器获取AppStorage中存储的收藏信息
@StorageLink('collectedIds') collectedIds: string[] = [];
@Prop articleItem: LearningResource = new LearningResource();
//3.根据收藏信息数组计算当前文章是否被收藏
isCollected(): boolean {
return this.collectedIds.some((id: string) => id === this.articleItem.id);
}
//6.处理界面点赞交互逻辑:使用@StorageLink装饰器接收的状态collectedIds被修改后,新值会同步到AppStorage中
handleCollected(): void {
const index = this.collectedIds.findIndex((id: string) => id === this.articleItem.id);
if (index === -1) {
this.collectedIds.push(resourceId);
} else {
this.collectedIds.splice(index, 1);
}
// ...
}
build(){
ActionButtonView({
//4.根据当前文章是否被用户收藏,判断收藏图标是否高亮
imgResource: this.isCollected() ? $r('app.media.btn_favorites_on') : $r('app.media.btn_favorites_normal'),
count: this.articleItem.collectionCount,
textWidth: $r('app.float.star_icon_width')
})
.onClick(() => {
//5.用户点击收藏图标时,调用处理收藏状态修改的函数
this.handleCollected();
})
}
}
在此方案中,由于文章卡片组件没有绑定AppStorage中key值为“userData”的变量,当用户编辑修改了用户描述userData.description的值时, 文章卡片组件不会重新渲染。
并且,当用户点击文章卡片上的收藏按钮修改文章收藏状态时,变化同步回AppStorage中的key值为“collectedIds”的变量。ArkUI监听到AppStorage中key值为“collectedIds”的值变化,只会通知所有绑定了AppStorage中key值为“collectedIds”变量的组件重新渲染,不会造成“我的“模块用户信息组件“UserInfoView ”重新渲染。
因此,从性能的角度考虑,在使用LocalStorage或AppStorage装饰器存储状态变量时需要合理设计状态的数据结构,避免无意义的渲染刷新。
说明:
过分追求状态结构拆分可能在某些场景导致组件设计过度,不利于维护。此时,可以将对象或类上经常一起改变的几个属性聚合成一个新的对象或类模型,并使用@Observed装饰器修饰,再作为属性挂载到之前的对象或类上。通过此方法,当属性变化时ArkUI只会通知变化给新的对象或类,不会通知最上层的对象。这样既可以有效的减少无用渲染次数,又能使代码更好维护。
如类ClassA上存在属性b、c、d。其中c和d经常一起发生变化,即当c的状态修改时同时也要修改d的状态。
class ClassA{
b: string;
c: number;
d: boolean;
}
此时,将c和d组合在一起做为新的类ClassE的属性并使用@Observed装饰器修饰。对于ClassA去掉c、d属性,新增属性e且其类型为ClassE,设计如下:
class ClassA{
b: string;
e: ClassE;
}
@Observed
class ClassE{
c: number;
d: boolean;
}
使用此方案,在AppStorage中存入数据结构为ClassA的变量。当ClassA实例的属性e中的属性c的值变化时,状态变化会通知使用ClassE实例的组件重新渲染,不会通知所有使用AppStorage中ClassA实例的组件更新,即只使用了ClassA实例b属性的组件不会重新渲染。
总结:
上述状态变量案例也可以使用此方法去实现防止资源的浪费,使用@Observed修饰一个新类里面包含collectedIds,当collectedIds发生改变只会刷新新类,而不会刷新UserData,从而避免了资源的浪费。
伪代码如下:
export interface UserData {
id: string;
username: string;
description: string;
e: ClassE;
@Observed
class ClassE{
// 1. 在用户信息UserData上增加用户收藏的资源列表id信息类型定义
collectedIds: string[];
// ...
}
}