specified启动模式为指定实例模式,有一些特殊场景,例如多门店应用中每次打开一个门店都希望能新建一个门店实例,而重复打开同一个门店都是同一门店实例。
此篇为餐饮APP中多门店实例的补充内容,以解决同一门店多次点击重复创建新窗口问题。上一篇文章地址:HarmonyOS开发 - 餐饮APP中多门店多窗口打开实例-CSDN博客,需要了解上一篇内容的朋友可以前去查看,此篇中使用到的代码也不重复此讲了,直接在上一篇代码基础上进行讲解。
一、module.json5配置
首先需要将module.json5文件中的配置launchType修改为specified,代码如下:
TypeScript
{
"module": {
"name": "store",
"type": "feature",
"description": "$string:module_desc",
"mainElement": "StoreAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "StoreAbility",
"srcEntry": "./ets/storeability/StoreAbility.ts",
"description": "$string:StoreAbility_desc",
"icon": "$media:icon",
"label": "$string:StoreAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"launchType": "specified"
}
]
}
}
此时重新运行App,点击不同门店会发现能同时开启多个子门店窗口。如下图:
在每个门店上多点几次,会发现不停的打开新窗口。这时又希望同一门店,重复点击时始终打开同一门店实例窗口,而不是重新创建新实例窗口,又该怎么解决呢?
解决这个问题,要在启动UIAbility之前,为该UIAbility实例指定一个唯一的字符串instanceKey,这样在调用startAbility()方法时,应用就可以根据指定的instanceKey来识别响应请求的UIAbility实例。在EntryAbility中,调用startAbility()方法时,可以在want参数中增加一个自定义参数,例如instanceKey,以此来区分不同的UIAbility实例。
二、AbilityStage组件容器
AbilityStage是一个Module级别的组件容器,应用的HAP在首次加载时会创建一个AbilityStage实例,可以对该Module进行初始化等操作。
AbilityStage与Module一一对应,即一个Module拥有一个AbilityStage。
2.1 创建MyAbilityStage.ets
DevEco Studio默认工程中未自动生成AbilityStage,如需要使用AbilityStage的能力,可以手动新建一个AbilityStage文件,具体步骤如下。
- 在工程Module对应的ets目录下,右键选择"New > Directory",新建一个目录并命名为abilityStage。
- 在myabilitystage目录,右键选择"New > ArkTS File",新建一个文件并命名为MyAbilityStage.ets。
- 打开MyAbilityStage.ets文件,导入AbilityStage的依赖包,自定义类继承AbilityStage并加上需要的生命周期回调,示例中增加了一个onCreate()生命周期回调。
TypeScript
import AbilityStage from '@ohos.app.ability.AbilityStage';
import Want from '@ohos.app.ability.Want';
import hilog from '@ohos.hilog';
export default class MyAbilityStage extends AbilityStage{
onAcceptWant(want: Want): string {
// 判断ability name是否为跳转实例ability
if(want.abilityName === 'StoreAbility') {
hilog.info(0x0000, 'testTag', `want instance key ${want.parameters.instanceKey as string}`)
// 返回
return 'StoreAbilityInstance_' + want.parameters.instanceKey
}
return ''
}
}
2.2 配置module.json5
在module.json5配置文件中,通过配置 srcEntry 参数来指定模块对应的代码路径,以作为HAP加载的入口。
TypeScript
{
"module": {
"name": "store",
"type": "feature",
"srcEntry": "./ets/abilityStage/MyAbilityStage.ets",
"description": "$string:module_desc",
// 略...
"abilities": [
{
"name": "StoreAbility",
"srcEntry": "./ets/storeability/StoreAbility.ts",
"description": "$string:StoreAbility_desc",
"icon": "$media:icon",
"label": "$string:StoreAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"launchType": "specified"
}
]
}
}
AbilityStage拥有onCreate()生命周期回调和onAcceptWant()、onConfigurationUpdated()、onMemoryLevel()事件回调。
- onCreate()生命周期回调:在开始加载对应Module的第一个UIAbility实例之前会先创建AbilityStage,并在AbilityStage创建完成之后执行其onCreate()生命周期回调。AbilityStage模块提供在Module加载的时候,通知开发者,可以在此进行该Module的初始化(如资源预加载,线程创建等)能力。
- onAcceptWant()事件回调:UIAbility指定实例模式(specified)启动时候触发的事件回调,具体使用请参见UIAbility启动模式综述。
- onConfigurationUpdated()事件回调:当系统全局配置发生变更时触发的事件,系统语言、深浅色等,配置项目前均定义在Configuration类中。
- onMemoryLevel()事件回调:当系统调整内存时触发的事件。
三、Want概述
3.1 Want的定义与用途
Want是对象间信息传递的载体,可以用于应用组件间的信息传递。其使用场景之一是作为startAbility()的参数,包含了指定的启动目标以及启动时需携带的相关数据,如bundleName和abilityName字段分别指明目标Ability所在应用的包名以及对应包内的Ability名称。当UIAbilityA启动UIAbilityB并需要传入一些数据给UIAbilityB时,Want可以作为一个载体将数据传给UIAbilityB。
3.2 Want的类型
- 显式Want:在启动Ability时指定了abilityName和bundleName的Want称为显式Want。
当有明确处理请求的对象时,通过提供目标Ability所在应用的包名信息(bundleName),并在Want内指定abilityName便可启动目标Ability。显式Want通常用于在当前应用开发中启动某个已知的Ability。参数说明参见Want参数说明。
TypeScript
let wantInfo = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.example.myapplication',
abilityName: 'FuncAbility',
}
- 隐式Want:在启动UIAbility时未指定abilityName的Want称为隐式Want。
当请求处理的对象不明确时,希望在当前应用中使用其他应用提供的某个能力(通过skills标签定义),而不关心提供该能力的具体应用,可以使用隐式Want。例如使用隐式Want描述需要打开一个链接的请求,而不关心通过具体哪个应用打开,系统将匹配声明支持该请求的所有应用。
TypeScript
let wantInfo = {
// uncomment line below if wish to implicitly query only in the specific bundle.
// bundleName: 'com.example.myapplication',
action: 'ohos.want.action.search',
// entities can be omitted
entities: [ 'entity.system.browsable' ],
uri: 'https://www.test.com:8080/query/student',
type: 'text/plain',
};
**说明:**根据系统中待匹配Ability的匹配情况不同,使用隐式Want启动Ability时会出现以下三种情况。
- 未匹配到满足条件的Ability:启动失败。
- 匹配到一个满足条件的Ability:直接启动该Ability。
- 匹配到多个满足条件的Ability(UIAbility):弹出选择框让用户选择。
3.3 Want参数说明
名称 | 读写属性 | 类型 | 必填 | 描述 |
---|---|---|---|---|
deviceId | 只读 | string | 否 | 表示目标Ability所在设备ID。如果未设置该字段,则表明本设备。 |
bundleName | 只读 | string | 否 | 表示目标Ability所在应用名称。 |
moduleName | 只读 | string | 否 | 表示目标Ability所属的模块名称。 |
abilityName | 只读 | string | 否 | 表示目标Ability名称。如果未设置该字段,则该Want为隐式。如果在Want中同时指定了bundleName,moduleName和abilityName,则Want可以直接匹配到指定的Ability。 |
uri | 只读 | string | 否 | 表示携带的数据,一般配合type使用,指明待处理的数据类型。如果在Want中指定了uri,则Want将匹配指定的Uri信息,包括scheme, schemeSpecificPart, authority和path信息。 |
type | 只读 | string | 否 | 表示携带数据类型,使用MIME类型规范。例如:"text/plain"、"image/*"等。 |
action | 只读 | string | 否 | 表示要执行的通用操作(如:查看、分享、应用详情)。在隐式Want中,您可定义该字段,配合uri或parameters来表示对数据要执行的操作。如打开,查看该uri数据。例如,当uri为一段网址,action为ohos.want.action.viewData则表示匹配可查看该网址的Ability。 |
entities | 只读 | Array<string> | 否 | 表示目标Ability额外的类别信息(如:浏览器,视频播放器),在隐式Want中是对action的补充。在隐式Want中,您可定义该字段,来过滤匹配UIAbility类别,如必须是浏览器。例如,在action字段的举例中,可存在多个应用声明了支持查看网址的操作,其中有应用为普通社交应用,有的为浏览器应用,您可通过entity.system.browsable过滤掉非浏览器的其他应用。 |
flags | 只读 | number | 否 | 表示处理Want的方式。例如通过wantConstant.Flags.FLAG_ABILITY_CONTINUATION表示是否以设备间迁移方式启动Ability。 |
parameters | 只读 | {[key: string]: any} | 否 | 此参数用于传递自定义数据,通过用户自定义的键值对进行数据填充,具体支持的数据类型如Want API所示。 |
四、entry模块
了解Want后,回到entry主模块,打开门店列表页面,添加instanceKey,与2.1中的MyAbilityStage相配合,在创建UIAbility实例之前,为该实例指定一个唯一的字符串Key,这样在调用startAbility()方法时,应用就可以根据指定的Key来识别响应请求的UIAbility实例。在EntryAbility中,调用startAbility()方法时,可以在want参数中增加一个自定义参数,例如instanceKey,以此来区分不同的UIAbility实例。
pages/Stores.ets修改后代码如下:
TypeScript
import router from '@ohos.router'
import common from '@ohos.app.ability.common'
import Want from '@ohos.app.ability.Want'
type StoresType = {
id: number
name: string
thumb: Resource
}
@Entry
@Component
struct Stores {
@State productList: Array<StoresType> = [
{ id: 1, name: '门店一', thumb: $rawfile('u62.png'), },
{ id: 2, name: '门店二', thumb: $rawfile('u76.png') }
]
// 定义内联构建函数
@Builder ImageItems(item: StoresType){
Row(){
Column(){
Image(item.thumb)
.width("100%")
.height("100vp")
.borderRadius(8)
Text(item.name)
.width("100%")
.height("36vp")
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontSize("18fp")
}.onClick(() => {
const context = getContext(this) as common.UIAbilityContext
let want: Want = {
'deviceId': '', // deviceId为空表示本设备
'bundleName': 'com.example.multistoreapplication',
'abilityName': 'StoreAbility',
'moduleName': 'store', // moduleName非必选
'parameters': {
'instanceKey': 'store_' + item.id
}
};
context.startAbility(want)
console.log('testTag', item.id)
})
}.width('100%').padding(10)
}
build() {
Row() {
Column() {
Row(){
Image($rawfile('back.png'))
.width("35vp")
.height("35vp")
.margin({ top: "0vp", bottom: "0vp", left: "0vp", right: "0vp" })
.onClick(() => {
// 返回上一页
router.back()
})
}.width('100%').justifyContent(FlexAlign.Start)
Text("门店列表")
.width("100%")
.height("50vp")
.fontSize("16fp")
.fontWeight(FontWeight.Bold)
.padding(10)
// 每行分两列展示
GridRow({columns: 2}){
ForEach(this.productList, (item: StoresType, index) => {
GridCol(){
this.ImageItems(item)
}
})
}
}.padding(5)
}
.width("100%")
}
}
在Want中parameters字段中添加了instanceKey参数,使用每个门店的id作为唯一key值。
此时,重启应用,反复点击某个门店,则只会打开一个新窗口;当点击另一个门店时,才会再次打开另一个新窗口。如下图:
五、信息传递
主应用打开子应用时,需要将一些数据传递给子模块,此时则需要使用Want,Want是对象间信息传递的载体, 可以用于应用组件间的信息传递。
5.1 支持类型
通过自定字段传递数据,以下为当前支持类型。
- 字符串(String)
- 数字(Number)
- 布尔(Boolean)
- 对象(Object)
- 数组(Array)
- 文件描述符(FD)
- parameter参数用法:以ability.params.backToOtherMissionStack为例,ServiceExtension在拉起UIAbility的时候,可以支持跨任务链返回。
具体示例可查看官网,地址:文档中心
5.2 传递参数
如上图,当进入不同门店时,希望将顶部门店信息更新为当前打开门店名称,则需要在entry主模块中Stores门店列表页,点击门店时将对应门店信息通过Want传递给即将打开的子模块。
entry模块中pages/Stores.ets文件,在parameters字段上添加门店信息,增加storeName字段,代码如下:
TypeScript
Column(){
Image(item.thumb)
.width("100%")
.height("100vp")
.borderRadius(8)
Text(item.name)
.width("100%")
.height("36vp")
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontSize("18fp")
}.onClick(() => {
const context = getContext(this) as common.UIAbilityContext
let want: Want = {
'deviceId': '', // deviceId为空表示本设备
'bundleName': 'com.example.multistoreapplication',
'abilityName': 'StoreAbility',
'moduleName': 'store', // moduleName非必选
'parameters': {
'instanceKey': 'store_' + item.id,
'storeName': item.name
}
};
context.startAbility(want)
console.log('testTag', item.id)
})
5.3 修改header组件
在接收参数前,修改将首页头部组件中门店信息对应字段,改为从外部传入。打开store模块中的components/Header.ets头部组件文件,修改后代码如下:
TypeScript
import router from '@ohos.router'
@Preview
@Component
export default struct Header {
// 搜索关键词
@State keyword: string = ''
private storeName: string = ''
// 关键词内容改变时回调函数
private onSearchChange: (data: string) => void
build() {
Row() {
Text(this.storeName + "\n")
.width("65vp")
.height("40vp")
.lineHeight("30vp")
.fontSize("14fp")
TextInput({ text: this.keyword, placeholder: "请输入搜索关键词" })
.width("170vp")
.height("40vp")
.placeholderColor("#262626")
.placeholderFont({ size: 12 })
.onChange((e) => this.keyword = e)
Button("搜索")
.width("70vp")
.height("40vp")
.onClick(() => {
this.onSearchChange(this.keyword) // 点击查询事件,将信息传递给父组件
})
}
.width("100%")
.padding({ top: "10vp", bottom: "10vp", left: "15vp", right: "15vp" })
.justifyContent(FlexAlign.SpaceBetween)
}
}
5.4 接收参数
在"5.2传递参数"中,将门店信息放在parameters字段中,那么在store模块子门店中如何获取这个参数呢?打开store模块中的storeability/StoreAbility.ts文件后,大家会发现onCreate回调函数上有want参数,再查看UIAbility实例的属性发现内部存在launchWant字段,从字面意思可见是存储want参数。如下图:
LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内"数据库",应用程序可以创建多个LocalStorage实例,LocalStorage实例可以在页面内共享,也可以通过GetShared接口,实现跨页面、UIAbility实例内共享。
综上所述,可以通过launchWant获取到parameters数据, 再实例LocalStorage对象将数据共享出去。此时打开storeability/StoreAbility.ts文件,找到onWindowStageCreate回调函数,内部调用的windowStage.loadContent()函数为多态函数,当第二个参数为LocalStorage实例时,其中内容为实例共享的数据。
原代码如下:
TypeScript
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
export default class StoreAbility extends UIAbility {
onCreate(want, launchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
// 略...
}
将loadContent()函数中第二个入参修改为LocalStorage实例后的代码如下:
TypeScript
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
export default class StoreAbility extends UIAbility {
onCreate(want, launchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
hilog.info(0x0000, 'testTag', 'storeName:' + this.launchWant.parameters.storeName);
windowStage.loadContent('pages/Index', new LocalStorage(this.launchWant.parameters), (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
// 略...
}
上述代码完成后,重启后再次打开门店,在控制台上查看输出的日志内容,会发现通过launchWant成功取到了storeName信息。如下图:
5.5 获取共享数据
通过上述操作,在store模块中已成功获取到want中的参数,并且将信息存储到共享数据中。此时打开子门店store模块的首页(路径:pages/Index.ets)文件,通过GetShared()函数获取到共享数据。代码如下:
TypeScript
import Header from '../components/Header'
import CarouseMap from '../components/Index/CarouselMap'
import CategoryList from '../components/Index/CategoryList'
import ProductList from '../components/Index/ProductList'
@Entry
@Component
struct Index {
@State keyword: string = ''
@State StoreName: string = '-'
aboutToAppear(){
const localStorage = LocalStorage.GetShared()
this.StoreName = localStorage.get('storeName') as string
}
build() {
Row() {
Column() {
Tabs({
barPosition: BarPosition.End
}){
TabContent(){
// 增加Column原因是因为TabContent中只有一个子组件,否则会报错
Column(){
Header({storeName: this.StoreName, onSearchChange: this.onSearchChange.bind(this)}) // 顶部导航
List(){
ListItem(){
Column(){
CarouseMap() // 轮播图
CategoryList() // 分类列表
ProductList() // 产品列表
}
}
}.width('100%').layoutWeight(1)
}.width('100%').height('100%').alignItems(HorizontalAlign.Start)
}.tabBar({icon: $rawfile('u48.png'), text: '首页'})
TabContent(){
Text('待开发中...')
}.tabBar({icon: $rawfile('u43.png'), text: '订单'})
TabContent(){
Text('待开发中...')
}.tabBar({icon: $rawfile('u53.png'), text: '我的'})
}
.barMode(BarMode.Fixed)
.barHeight(60)
}
.width('100%')
}
.height('100%').alignItems(VerticalAlign.Top)
}
/**
* 搜索内容改变事件
* @param data
*/
onSearchChange(data: string){
console.log('search value:', data)
}
}
如下图,定义StoreName门店名称字段后,通过LocalStorage页面级的UI状态器获取门店信息。
以上步骤完成后,在打开不同门店时,则可以显示对应门店信息了。如下图: