HarmonyOS开发 - 餐饮APP中多门店多窗口打开实例补充

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文件,具体步骤如下。

  1. 在工程Module对应的ets目录下,右键选择"New > Directory",新建一个目录并命名为abilityStage。
  2. 在myabilitystage目录,右键选择"New > ArkTS File",新建一个文件并命名为MyAbilityStage.ets。
  3. 打开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状态器获取门店信息。

以上步骤完成后,在打开不同门店时,则可以显示对应门店信息了。如下图:

​​​​​​​

相关推荐
s_daqing40 分钟前
华为手机建议使用adb卸载的app
adb·华为·智能手机
华研前沿标杆游学4 小时前
预约参观华为基地,见证行业巅峰
华为
编程百晓君4 小时前
一文彻底拿捏DevEco Studio的使用小技巧
华为·harmonyos
SmartBrain5 小时前
华为管理变革之道:组织文化与活力
华为
SmartBrain5 小时前
华为管理变革之道:管理制度创新
华为
制造数字化方案研究院5 小时前
华为 IPD,究竟有什么特点?(一)
华为
有颜有货5 小时前
华为:数字化转型只有“起点”,没有“终点”
华为·数字化转型
轻口味5 小时前
【每日学点鸿蒙知识】私仓搭建、resources创建文件夹、hvigor如何动态设置版本、SM3摘要算法、SP存储报错等
华为·json·harmonyos
凯子坚持 c6 小时前
仓颉编程语言深入教程:基础概念和数据类型
开发语言·华为
JasonYin~6 小时前
HarmonyOS NEXT 实战之元服务:静态案例效果---查看国际航班服务
华为·harmonyos