零基础快速上手HarmonyOS ArkTS开发3---应用程序框架
接着上一次零基础快速上手HarmonyOS ArkTS开发2---ArkTS开发实践继续往下学习,如上次文末所示,这次来学习新的章节:
其实这章的知识点也比较简单,类似于Android的Intent之间Activity的跳转相关的知识,而在鸿蒙中叫UIAbility:
什么是UIAbility?
它是一种包含用户界面的应用组件,主要是和用户进行交互,而Android中的Activity也是用户界面的组件,异曲同工,下面来看一下应用程序存在的以下几种交互形式,它们都是由UIAbility来进行构建的:
形式一:点击桌面图标进入应用
如图:
其中打开的图库应用就是基于UIAbility实现的一个实例。
形式二: 一个应用拉起另一个应用
比如图库应用中可以将图片分享到备忘录:
其中备忘录应用也是使用UIAbility实现的一个应用实例。
形式三:最近任务列表切回应用
比如从最近任务列表中切回图库应用:
其中任务列表中的应用任务也是基于UIAbility实现的一个应用实例。
单UIAbility应用和多UIAbility应用:
每一个UIAbility实例都对应于一个最近任务列表中的任务,它作为系统调度的单元,提供窗口用于绘制界面,一个应用可以有单个UIAbility,也可以有多个,建议将一个独立的功能模块放到同一个UIAbility当中,比如:
浏览器应用:可以用单个UIAbility结合多页面的形式让用户进行搜索和浏览内容,而像聊天应用它里面会增加一个“外卖功能”的场景,所以聊天应用和外卖功能作为2个UIAbility存在,而当在外卖详情有新的聊天消息时,就可以通过最近任务列表切回到聊天窗口继续进行聊天对话。
而一个UIAbility可以对应多个页面,例如在新闻应用在浏览内容时可以进行多页面的跳转。
UIAbility内的页面创建:
在创建一个默认工程里,就会有一个UIAbility:
并且在pages中会有一个入口页面Index:
而新建一个页面的话可以这样:
页面间的跳转和数据传递:
现在已经创建了2个页面了,接下来就可以来进行跳转了。
Index页面跳转到Second页面:
而具体的实现首先需要导入router路由模块:
接下来再调用router.pushUrl来指定需要跳转的Second页面路径既可:
Index页面带数据跳转到Second页面:
通常我们在跳转页面时,是需要携带参数的,在跳转的时候定义params参数既可:
而在接收页面,用如下方式获取发送过来的参数:
其效果为:
Second页面返回到Index页面:
当我们在Second进行了操作之后,想返回到Index页面,调用router.back()方法既可实现返回上一页面的功能:
效果为:
UIAbility的生命周期:
先来看一下官方给的生命周期状态图:
掌握好生命周期对于写出正确的程序很重要,下面则具体的过一下,其实官网对这块描述也非常详细了。
Create状态:
当UIAbility实例创建完成时会触发,系统会调用onCreate()回调,这里可以对应用进行一个初始化的操作【方法名都跟Acitivty.onCreate()一模一样~~】:
WindowStageCreate状态:
UIAbility实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。每一个UIAbility实例都对应持有一个WindowStage实例。
WindowStage为本地窗口管理器,用于管理窗口相关的内容,例如与界面相关的获焦/失焦、可见/不可见。
可以在onWindowStageCreate回调中,设置UI页面加载、设置WindowStage的事件订阅。
在onWindowStageCreate(windowStage)中通过loadContent接口设置应用要加载的页面。
Foreground和Background状态:
Foreground和Background状态,分别在UIAbility切换至前台或者切换至后台时触发。
分别对应于onForeground回调和onBackground回调。
类似于Android中的onResume()和onStop(),这里举一个使用场景:
例如用户打开地图应用查看当前地理位置的时候,假设地图应用已获得用户的定位权限授权。在UI页面显示之前,可以在onForeground回调中打开定位功能,从而获取到当前的位置信息。
当地图应用切换到后台状态,可以在onBackground回调中停止定位功能,以节省系统的资源消耗。
WindowStageDestroy状态:
在UIAbility实例销毁之前,则会先进入onWindowStageDestroy回调,我们可以在该回调中释放UI页面资源。
Destroy状态:
在UIAbility销毁时触发。可以在onDestroy回调中进行系统资源的释放、数据的保存等操作。
UIAbility的启动模式
在Android中的Activity会有不同的启动模式,很重要但是应用不当也很容易产生BUG,同样的,对标的UIAbility也是一样,关于这块的知识先大致有个了解,具体在实战开发中再去慢慢体会。
singleton(单实例模式)
简介:
singleton启动模式为单实例模式,也是默认情况下的启动模式。
每次调用startAbility()方法时,如果应用进程中该类型的UIAbility实例已经存在,则复用系统中的UIAbility实例。系统中只存在唯一一个该UIAbility实例,即在最近任务列表中只存在一个该类型的UIAbility实例。
场景举例:
对于浏览器或者新闻等应用,用户在打开该应用,并浏览访问相关内容后,回到桌面,再次打开该应用,显示的仍然是用户当前访问的界面。
效果:
看一下官方给的一个实例的效果:
注意:
应用的UIAbility实例已创建,该UIAbility配置为单实例模式,再次调用startAbility()方法启动该UIAbility实例。由于启动的还是原来的UIAbility实例,并未重新创建一个新的UIAbility实例,此时只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。
看到“onNewWant”这个名字,有木有很熟悉,Android的“onNewIntent”~~
使用:
在module.json5文件中的“launchType”字段配置为“singleton”即可。
multiton(多实例模式)
简介:
multiton启动模式为多实例模式,每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。这种情况下可以将UIAbility配置为multiton(多实例模式)。
这个跟Android的“standard”启动模式差不多。
场景举例:
对于应用的分屏操作,用户希望使用两个不同应用(例如备忘录应用和图库应用)之间进行分屏,也希望能使用同一个应用(例如备忘录应用自身)进行分屏。
效果:
看一下官方给的一个实例的效果:
也就是每次打开时,都生成了一个新的实例。
使用:
specified(指定实例模式)
简介:
specified启动模式为指定实例模式,针对一些特殊场景使用(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)。
效果:
看一下官方给的一个实例的效果:
可以看到,新建文档每次都是打开的一个新实例,而查看已保存的文档永远只有一个实例。
使用:
这里的使用就稍微麻烦一些,依照官网的说明,这里简单过一下。
例如有两个UIAbility:EntryAbility和SpecifiedAbility,SpecifiedAbility配置为指定实例模式启动,需要从EntryAbility的页面中启动SpecifiedAbility。
1、先将要启动的SpecifiedAbility指定为specified启动模式:
2、接下来则来启动该SpecifiedAbility,具体调用如官网示例所示:
// 在启动指定实例模式的UIAbility时,给每一个UIAbility实例配置一个独立的Key标识 // 例如在文档使用场景中,可以用文档路径作为Key标识 import common from '@ohos.app.ability.common'; import Want from '@ohos.app.ability.Want'; import { BusinessError } from '@ohos.base'; function getInstance() { return 'key'; } let context:common.UIAbilityContext = ...; // context为调用方UIAbility的UIAbilityContext let want: Want = { deviceId: '', // deviceId为空表示本设备 bundleName: 'com.example.myapplication', abilityName: 'SpecifiedAbility', moduleName: 'specified', // moduleName非必选 parameters: { // 自定义信息 instanceKey: getInstance(), }, } context.startAbility(want).then(() => { console.info('Succeeded in starting ability.'); }).catch((err: BusinessError) => { console.error(`Failed to start ability. Code is ${err.code}, message is ${err.message}`); })
跟Android一样,其Activity的跳转也是用Context,而这里的context的实例是这么初始化的:
其中startAbility时需要传一个want参数,里面就是指定跳转的一些信息:
其中这个want参数中,有一个自定义的信息叫instanceKey:
那这个instanceKey是啥含义呢?从这参数名称就大概能猜到:实例主键,官方有这么一句与此参数相关的说明:“业务逻辑会根据这个参数返回一个字符串Key,用于标识当前UIAbility实例。如果返回的Key已经对应一个已启动的UIAbility实例,系统会将该UIAbility实例拉回前台并获焦,而不会创建新的实例。如果返回的Key没有对应已启动的UIAbility实例,则系统会创建新的UIAbility实例并启动。”,也就是说,如果启动的这个SpecifiedAbility,由于我们指定了它是一个指定实例模式,所以当发现启动的实例的key已经对应一个已启动的UIAbility实例,则直接会复用,此时就不会再重新创建一个新的UIAbility实例了,而如果没有发现则就会创建新的UIAbility实例。
3、接下来在SpecifiedAbility还得进行参数的处理:
import AbilityStage from '@ohos.app.ability.AbilityStage'; import Want from '@ohos.app.ability.Want'; export default class MyAbilityStage extends AbilityStage { onAcceptWant(want: Want): string { // 在被调用方的AbilityStage中,针对启动模式为specified的UIAbility返回一个UIAbility实例对应的一个Key值 // 当前示例指的是module1 Module的SpecifiedAbility if (want.abilityName === 'SpecifiedAbility') { // 返回的字符串Key标识为自定义拼接的字符串内容 if (want.parameters) { return `SpecifiedAbilityInstance_${want.parameters.instanceKey}`; } } return ''; } }
在SpecifiedAbility启动之前,会先进入对应的AbilityStage的onAcceptWant()生命周期回调中,以获取该UIAbility实例的Key值。
此时系统会自动匹配,如果存在与该UIAbility实例匹配的Key,则会启动与之绑定的UIAbility实例,并进入该UIAbility实例的onNewWant()回调函数;否则会创建一个新的UIAbility实例,并进入该UIAbility实例的onCreate()回调函数和onWindowStageCreate()回调函数。
Codelab实践:
接下来借用官方的效果来实战一下。
效果:
也就是页面之间的一个跳转,并且数据传递,比较简单。
实现:
1、新建工程:
此时保证能够运行:
2、创建IndexPage.ets:
先将默认的“Index.ets”改个名:
此时则需要注意:改名后,修改工程entryability目录下EntryAbility.ets文件中windowStage.loadContent方法第一个参数为pages/IndexPage,修改后,启动应用会自动加载IndexPage页。
3、创建SecondPage.ets:
接下来再来创建第二个页面,这里通过向导来创建(选中工程entry > src > main > ets > pages目录,点击鼠标右键 > New > Page),如下:
用向导创建有一个好处,就是DevEco Studio会自动在工程目录entry > src > main > resources > base > profile > main_pages.json文件中添加pages/SecondPage。
如果是通过copy的方式来创建,则需要手动的在这个文件中去添加一下,稍麻烦一点。
3、页面跳转:
1、IndexPage中准备布局:
这块比较简单,就不从0开始了,从官方开源的源码中copy:
@Entry @Component struct Index { @State message: string = CommonConstants.INDEX_MESSAGE build() { Row() { Column() { Text(this.message) .fontSize(CommonConstants.FONT_SIZE) .fontWeight(FontWeight.Bold) Blank() Button($r('app.string.next')) .fontSize(CommonConstants.BUTTON_FONT_SIZE) .width(CommonConstants.BUTTON_WIDTH) .height(CommonConstants.BUTTON_HEIGHT) .backgroundColor($r('app.color.button_bg')) } .width(CommonConstants.FULL_WIDTH) .height(CommonConstants.LAYOUT_HEIGHT) } .height(CommonConstants.FULL_HEIGHT) .backgroundColor($r('app.color.page_bg')) } }
此时会涉及到常量和色值:
CommonConstants.ets:
/** * Common constants for all features. */ export default class CommonConstants { /** * The index page message. */ static readonly INDEX_MESSAGE: string = 'Index Page'; /** * The src msg. */ static readonly SECOND_SRC_MSG: string = 'Index页面传来的数据'; /** * The second page url. */ static readonly SECOND_URL: string = 'pages/SecondPage'; /** * The second page message. */ static readonly SECOND_MESSAGE: string = 'Second Page'; /** * The src param. */ static readonly SECOND_SRC_PARAM: string = 'src'; /** * Full the width. */ static readonly FULL_WIDTH: string = '100%'; /** * Full the height. */ static readonly FULL_HEIGHT: string = '100%'; /** * The hello world font size. */ static readonly FONT_SIZE: string = '38fp'; /** * The param font size. */ static readonly PARAMS_FONT_SIZE: string = '20fp'; /** * The button font size. */ static readonly BUTTON_FONT_SIZE: string = '16fp'; /** * The button width. */ static readonly BUTTON_WIDTH: string = '296vp'; /** * The button height. */ static readonly BUTTON_HEIGHT: string = '40vp'; /** * The layout height. */ static readonly LAYOUT_HEIGHT: string = '140vp'; /** * The param opacity. */ static readonly PARAMS_OPACITY: number = 0.6; }
Logger.ets:
import hilog from '@ohos.hilog'; /** * Common log for all features. */ export class Logger { private domain: number; private prefix: string; private format: string = `%{public}s, %{public}s`; constructor(prefix: string) { this.prefix = prefix; this.domain = 0xFF00; } debug(...args: string[]) { hilog.debug(this.domain, this.prefix, this.format, args); } info(...args: string[]) { hilog.info(this.domain, this.prefix, this.format, args); } warn(...args: string[]) { hilog.warn(this.domain, this.prefix, this.format, args); } error(...args: string[]) { hilog.error(this.domain, this.prefix, this.format, args); } fatal(...args: string[]) { hilog.fatal(this.domain, this.prefix, this.format, args); } isLoggable(level: number) { hilog.isLoggable(this.domain, this.prefix, level); } } export default new Logger('[PageRouter]');
这块Log的输出可以参考:@ohos.hilog (HiLog日志打印)
color.json:
{ "color": [ { "name": "white", "value": "#FFFFFF" }, { "name": "button_bg", "value": "#007DFF" }, { "name": "page_bg", "value": "#F1F3F5" } ] }
string.json:
{ "string": [ { "name": "module_desc", "value": "module description" }, { "name": "EntryAbility_desc", "value": "description" }, { "name": "EntryAbility_label", "value": "pageRouter" }, { "name": "next", "value": "Next" }, { "name": "back", "value": "Back" } ] }
此时运行发现报错了:
这是因为我把start_window_background这种色值给删掉了,这里改一下,直接改用白色既可:
再运行。
2、跳转到SecondPage:
此时点击按钮时,让其跳到第二个页面上来,所以增加点击事件:
此时需要导入路由模块:
其中url定义在常量文件中:
此时再运行看一下:
3、SecondPage准备布局:
接下来准备一下第二个页面的内容:
其中会接收第一个页面传过来的参数,另外从这个页面还可以回到第一个页面,具体布局如下:
import router from '@ohos.router' import CommonConstants from '../common/constants/CommonConstants' @Entry @Component struct SecondPage { @State message: string = 'Second Page' build() { Row() { Column() { Text(this.message) .fontSize(CommonConstants.FONT_SIZE) .fontWeight(FontWeight.Bold) Text('这个是要获取上一个页面参数的') .fontSize(CommonConstants.PARAMS_FONT_SIZE) .opacity(CommonConstants.PARAMS_OPACITY) Blank() Button($r('app.string.back')) .fontSize(CommonConstants.BUTTON_FONT_SIZE) .width(CommonConstants.BUTTON_WIDTH) .height(CommonConstants.BUTTON_HEIGHT) .backgroundColor($r('app.color.button_bg')) } .width(CommonConstants.FULL_WIDTH) .height(CommonConstants.LAYOUT_HEIGHT) } .height(CommonConstants.FULL_HEIGHT) .backgroundColor($r('app.color.page_bg')) } }
此时看一下页面效果:
4、传递数据:
接下来我们在跳转时携带一个参数,来看一下如何在页面之间进行数据的传递:
首先在第一个页面跳转时增加一个参数:
然后在第二个页面来接收第一个页面传递过来的参数,这块知识上面已经学习过了,下面来看代码:
运行一下:
4、页面返回:
最后,第二个页面如果想返回到上一个页面,如下处理:
同时回来时也可以携带参数:
这块逻辑差不多,就不演示了,下面来看一下返回上一个页面的效果:
总结:
这节其实比较简单,也是构建一个项目必备的基础,值得提一嘴的是,有木有发现官网的源码中,页面上对于常量全抽取到了一个外部统一的文件当中了:
页面中没一个把色值、文本写死的,维护性大大提高,这种好习惯值得我们去学习。
这节可能觉得太简单了,学起来感觉秒秒钟就搞定了,但下节起就开始复杂了,贴上官方的效果动图提前感受一下:
涉及到的知识点就会比较多了:
加油~~