表示,表示该组件占父组件尺寸的百分比,如将组件的width设置为50%,代表其宽度为父组件的50%。
(2) 样式导入: 为了模块化管理和代码复用,CSS样式文件支持用@import 语句导入 CSS 文件。
(3) 声明样式: 每个页面目录下存在一个与布局HML文件同名的CSS文件,用来描述该HML页面中组件的样式,决定组件应该如何显示。
第一,内部样式,支持使用style、class属性来控制组件的样式,示例代码如下:
Hello World
/* index.css */
.container {
justify-content: center;
}
第二,文件导入,合并外部样式文件。例如,在common目录中定义样式文件style.css,并在index.css文件中进行导入,代码如下:
/* 第3章/CSS语法参考-外部定义样式style.css文件代码 */
.title {
font-size: 50px;
}
/* 第3章/CSS语法参考-导入外部定义样式index.css文件代码 */
@import '../../common/style.css';//导入外部样式
.container {
justify-content: center;
}
(4) 选择器: CSS选择器用于选择需要添加样式的元素,支持的选择器见表39。
表39CSS选择器的样式元素
选择器样例样 例 描 述
.class.container用于选择class=container的组件
#id#titleId用于选择id=titleId的组件
示例代码如下:
/* 第3章/CSS语法参考-选择器*/
/* 页面样式xxx.css */
/* 对class="title"的组件设置样式 */
.title {
font-size: 30px;
}
/* 对id="contentId"的组件设置样式 */
#contentId {
font-size: 20px;
}
(5) 选择器的优先级: 选择器的优先级的计算规则与w3c规则保持一致(只支持内联样式、id、class),其中内联样式为在元素style属性中声明的样式。当多条选择器声明匹配到同一元素时,各类选择器的优先级由高到低的顺序为内联样式 > id > class。
(6) 样式预编译: 预编译提供了利用特有语法生成CSS的程序,可以提供变量、运算等功能,令开发者更便捷地定义组件样式,目前支持less、sass和scss的预编译。当使用样式预编译时,需要将原CSS文件的后缀改为less、sass或scss,如将index.css改为index.less、index.sass或index.scss。
当前文件使用样式预编译,例如将原index.css改为index.less,代码如下:
/*第3章/CSS语法参考-样式预编译index.less*/
/*定义变量*/
@colorBackground: #000000;
.container {
background-color: @colorBackground; /*使用当前less文件中定义的变量*/
}
引用预编译文件,例如common中存在style.scss文件,将原index.css改为index.scss,并引入style.scss,代码如下:
/*第3章/CSS语法参考-引入预编译文件style.scss*/
/*定义变量*/
$colorBackground: #000000;
在index.scss中引用:
/*index.scss*/
/*引入外部SCSS文件*/
@import '../../common/style.scss';
.container {
background-color: $colorBackground; /*使用style.scss中定义的变量*/
}
需说明的是引用的预编译文件建议放在common目录进行管理。
3) 配置数据和事件
卡片使用JSON文件配置所用的变量和事件,变量的声明在data字段下,事件的声明在actions字段下,示例代码如下:
"//":"第3章/卡片配置数据和事件"
{
"data": {
"temperature": "35°C",
"city": "hangzhou"
},
"actions": {
"routerEventName": {
"action": "router",
"abilityName": "com.example.myapplication.FormAbility",
"params": {
"message": "weather",
"temperature": "{{temperature}}"
}
},
"messageEventName": {
"action": "message",
"params": {
"message": "weather update"
}
}
}
}
3. 资源访问
1) 访问JS模块资源
卡片工程可以访问的资源包括JS模块的resources资源、应用resources资源6+(所有JS模块共享)和系统预置资源6+。
(1) 资源限定词: 资源限定词可以由一个或多个表征应用场景或设备特征的限定词组合而成,包括深色模式、屏幕密度等维度,限定词之间通过中画线()连接。开发者在resources目录下创建限定词文件时,需要掌握限定词文件的命名要求以及限定词文件与设备状态的匹配规则。
(2) 资源限定词的命名要求: 限定词的组合顺序为深色模式和屏幕密度。开发者可以根据应用的使用场景和设备特征,选择其中的一类或几类限定词组成目录名称,顺序不可颠倒。限定词之间均采用中画线()连接,例如resdarkldpi.json。每类限定词的取值必须符合表310的条件,否则,将无法匹配目录中的资源文件,限定词对大小写敏感。resources资源文件的资源限定词有前缀res,例如resldpi.json。resources资源文件的默认资源限定文件为resdefaults.json。资源限定文件中不支持使用枚举格式的颜色设置资源,具体见表310。
表310资源限定词
类型含义与取值说明
深色模式表示设备的深色模式,取值如dark
屏幕密度表示设备的屏幕密度(单位为dpi),取值: ldpi表示低密度屏幕(~120dpi)(0.75基准密度); mdpi表示中密度屏幕(~160dpi)(基准密度); hdpi表示高密度屏幕(~240dpi)(1.5基准密度); xhdpi表示超高密度屏幕(~320dpi)(2.0基准密度); xxhdpi表示超超高密度屏幕(~480dpi)(3.0基准密度); xxxhdpi表示超超超高密度屏幕(~640dpi)(4.0基准密度)
需要说明的是如果当前设备的DPI不完全匹配定义的DPI,则将选取更接近当前设备DPI的资源文件。例如当前设备为2.7×基准密度,会选择resxxhdpi.json文件中定义的资源; 开发者还可以定义一个resdefaults.json资源文件,用于当对应密度资源文件中没有对应的资源词条时,应用将尝试在resdefaults.json文件中匹配对应的资源词条,如果仍未查找到对应词条,则图片加载失败。
(3) 限定词与设备状态的匹配规则: 在为设备匹配对应的资源文件时,限定词目录匹配的优先级从高到低依次为深色模式 > 屏幕密度。在资源限定词目录均未匹配的情况下,匹配默认资源限定文件。
如果限定词目录中包含资源限定词,则对应限定词的取值必须与当前的设备状态完全一致,这样该目录才能参与设备的资源匹配。例如资源限定文件reshdpi.json与当前设备密度xhdpi无法匹配。
(4) 引用JS模块内的resources资源: 在应用开发的HML和JS文件中使用$r的语法,可以对JS模块内的resources目录下的json资源进行格式化,获取相应的资源内容。具体见表311。
表311引用JS模块内的resources资源
属性类型描述
$r(key: string) =>
string获取资源限定下具体的资源内容。例如: this.$r('strings.hello')
参数说明: key定义在资源限定文件中的键值,如strings.hello
resdefaults.json示例:
{
strings: {
hello: 'hello world'
}
}
引用JS模块内resources资源,示例代码如下:
//第3章/引用JS模块内resources资源
resources/res-dark.json:
{
"image": {
"clockFace": "common/dark_face.png"
},
"colors": {
"background": "#000000"
}
}
resources/res-defaults.json:
{
"image": {
"clockFace": "common/face.png"
},
"colors": {
"background": "#ffffff"
}
}
需要说明的是资源限定文件中不支持颜色枚举格式。
2) 访问应用资源
从 API Version 6 开始,在HML/CSS/JSON文件中,可以引用应用资源,包括颜色、圆角和图片类型的资源。
应用资源由开发者在resources目录中定义,目前仅支持使用在color.json文件中自定义的颜色资源、在float.json文件中自定义的圆角资源及在media目录中存放的图片资源。
resources目录的基础结构如下所示,同一个资源,可以在base子目录和dark子目录各定义一个值。浅色模式下用base目录中定义的值,深色模式下用dark目录下定义的值。若某资源仅在base目录中有定义,则其在深浅色模式下的表现相同。具体示例代码如下:
//第3章/resources目录的基础结构
├─ java
├─ js
└─ resources -> 与java、js目录同级的resources目录
├─ base -> 定义浅色模式下的颜色、圆角或图片
│├─ element
││├─ color.json
││└─ float.json
│└─ media
│└─ my_background_image.png
└─ dark -> 定义深色模式下的颜色、圆角或图片(如果未定义,则深色模式下继续使用base目录中的相关定义)
├─ element
│├─ color.json
│└─ float.json
└─ media
└─ my_background_image.png
引用JS模块内的resources资源,访问应用资源,color.json文件的格式说明,代码如下:
"//": "第3章/访问应用资源-color.json文件的格式说明"
{
"color": [
{
"name": "my_background_color",
"value": "#ffff0000"
},
{
"name": "my_foreground_color",
"value": "#ff0000ff"
}
]
}
引用JS模块内的resources资源,访问应用资源,float.json文件的格式说明,代码如下:
"//": "第3章/访问应用资源-float.json文件的格式说明"
{
"float":[
{
"name":"my_radius",
"value":"28.0vp"
},
{
"name":"my_radius_xs",
"value":"4.0vp"
}
]
}
在卡片工程的CSS文件中,通过@app.type.resource_id的形式引用应用资源。根据引用资源类型的不同,type可以取color(颜色)、float(圆角)和media(图片)。resource_id代表应用资源id,即color.json或float.json文件中的name字段,或者media目录中的图片文件的名称(不包含图片类型后缀),具体示例代码如下:
//第3章/通过@app.type.resource_id的形式引用应用资源
.divA {
background-color: "@app.color.my_background_color";
border-radius: "@app.float.my_radius";
}
.divB {
background-image: "@app.media.my_background_image";
}
在HML文件中,通过{{$r('app.type.resource_id')}}的形式引用应用资源,各个字段的含义与CSS文件相同,具体示例代码如下:
在JSON文件中,通过this.$r('app.type.resource_id')的形式引用应用资源,各个字段的含义与CSS文件相同,具体示例代码如下:
//第3章/通过this.$r('app.type.resource_id')的形式引用应用资源
{
"data":{
"myColor": "this.$r('app.color.my_background_color')",
"myRadius": "this.$r('app.float.my_radius')",
"myImage":"this.$r('app.media.my_background_image')"
}
}
3) 访问系统资源
在HML/CSS/JSON文件中,可以引用系统预置资源,包括颜色、圆角和图片类型的资源。说明,从 API Version 6 开始支持。在卡片工程的CSS文件中,通过@sys.type.resource_id的形式引用系统资源。根据引用资源类型的不同,type可以取color(颜色)、float(圆角)和media(图片)。resource_id代表系统资源id,系统资源预置在系统中,具体示例代码如下:
//第3章/访问系统资源-系统资源预置
.divA {
background-color: "@sys.color.fa_background";
border-radius: "@sys.float.fa_corner_radius_card";
}
.divB {
background-image: "@sys.media.fa_card_background";
}
在HML文件中,通过{{$r('sys.type.resource_id')}}的形式引用系统资源,各个字段的含义与CSS文件相同,具体示例代码如下:
在JSON文件中,通过this.$r('sys.type.resource_id')的形式引用系统资源,各个字段的含义与CSS文件相同,具体示例代码如下:
//第3章/通过this.$r('sys.type.resource_id')的形式引用系统资源
{
"data":{
"sysColor": "this.$r('sys.color.fa_background')",
"sysRadius": "this.$r('sys.float.fa_corner_radius_card')",
"sysImage":"this.$r('sys.media.fa_card_background')"
}
}
4. 多语言支持
基于开发框架的应用会覆盖多个国家和地区,开发框架支持多语言能力后,应用开发者无须开发多个不同语言的版本就可以同时支持多种语言的切换,为项目维护带来便利。开发者仅需要通过定义资源文件和引用资源两个步骤,就可以使用开发框架的多语言能力。
1) 定义资源文件
资源文件用于存放应用在多种语言场景下的资源内容,开发框架使用JSON文件保存资源。
在文件组织中指定的i18n文件夹内放置每个语言地区下的资源定义文件即可,资源文件命名为“语言地区.json”格式,例如英文(美国)的资源文件命名为enUS.json。当开发框架无法在应用中找到系统语言的资源文件时,默认使用enUS.json文件中的资源内容。
由于不同语言针对单复数有不同的匹配规则,在资源文件中可使用zero、one、two、few、many、other定义不同单复数场景下的词条内容。例如,中文不区分单复数,仅存在other场景; 英文存在one、other场景; 阿拉伯语存在上述6种场景。
以enUS.json和arAE.json为例,演示资源文件的格式,代码如下:
"//":"第3章/定义资源文件示例"
{
"strings": {
"hello": "Hello world!",
"symbol": "@#$%^&*()_+-={}[]\\|:;\"'<>,./?",
"plurals": {
"one": "one person",
"other": "other people"
}
},
"files": {
"image": "image/en_picture.PNG"
}
}
{
"strings": {
"plurals": {
"zero": "",
"one": "",
"two": "",
"few": "",
"many": "",
"other": ""
}
}
}
2) 引用资源
在应用开发的页面中使用多语言的语法,包含简单格式化和单复数格式化两种,这两种方法都可以在HML或JS中使用。
(1) 简单格式化方法。
在应用中使用$t方法引用资源,$t既可以在HML中使用,也可以在JS中使用。系统将根据当前语言环境和指定的资源路径(通过$t的path参数设置),显示对应语言的资源文件中的内容,具体见表312和表313。
表312简单格式化
属性类型参数必填描述
$tFunction参见表313是根据系统语言完成简单的替换: this.$t('strings.hello')
表313$t参数说明
参数类型必填描述
pathstring是资源路径
多语言支持引用资源简单格式化,示例代码如下:
{{ $t('strings.hello') }}
(2) 单复数格式化方法,具体见表314和表315。
表314单复数格式化
属性类型参数必填描述
$tFunction参见表315是根据系统语言完成单复数替换: this.$tc('strings.plurals')
说明: 定义资源的内容,通过JSON格式的key区分,key为zero、one、two、few、many、other
表315$tc参数说明
参数类型必填描述
pathstring是资源路径
countnumber是要表达的值
多语言支持引用资源单复数格式化,示例代码如下:
{{ $tc('strings.plurals', 0) }}
{{ $tc('strings.plurals', 1) }}
{{ $tc('strings.plurals', 2) }}
{{ $tc('strings.plurals', 6) }}
{{ $tc('strings.plurals', 50) }}
{{ $tc('strings.plurals', 100) }}
5. 低版本兼容
卡片特性不断增加,使用了新特性的卡片,在不支持这些新特性的旧系统上可能显示异常。可以在卡片工程中指定最小SDK版本,防止使用新特性的卡片被推送后安装在旧系统上。也可以参考本章节的内容,在卡片开发阶段做前向兼容适配。
说明; 低版本兼容能力从 API Version 6 开始支持。
开发者可以通过JSON配置文件配置前向兼容能力。该文件提供了apiVersion属性,用于兼容版本,该字段和卡片配置文件的数据字段data、事件字段actions同级。在apiVersion标签下定义的内容会基于当前运行版本信息,覆盖原始的data标签内容。
示例说明,假设JS服务卡片框架从API Version 6开始支持引用系统内置资源颜色,从API Version 7开始支持slider组件(仅用于举例,不代表实际情况),则可以按照以下方式,做前向兼容,代码如下:
hello world
xxx.json配置文件:
{
"data": {
"myBackgroundColor": "#87ceeb",
"canUseSlider": "false"
},
"apiVersion": {
"6": {
"myBackgroundColor": "@sys.color.fa_background"
},
"7": {
"canUseSlider": "true"
}
}
}
JS服务卡片开发框架会根据应用中的配置及当前系统运行版本,选取最合适的数据。假设系统运行版本在5及以下,则实际解析的myBackgroundColor值为#87ceeb,canUseSlider值为false; 假设系统运行版本为6,则实际解析的myBackgroundColor值为@sys.color.fa_background,canUseSlider值为false; 假设系统运行版本为7及以上,则实际解析的myBackgroundColor值为@sys.color.fa_background,canUseSlider值为true。
6. 设置主题样式
卡片支持修改自定义主题字段。用app_background设置卡片背景颜色。
如将相关卡片背景色设置为透明,代码如下:
{
"styles": {
"app_background": "#00000000"
}
}
3.3Ability框架
本节主要阐述HarmonyOS应用、原子化服务与服务卡片开发所具备的抽象能力Ability,包括三大分类: 运行原理、基本开发逻辑及它们之间的信息传递方式。
3.3.1Ability概述
Ability是应用所具备能力的抽象,也是应用程序的重要组成部分。一个应用可以具备多种能力,即可包含多个Ability,HarmonyOS支持应用以Ability为单位进行部署。
Ability可以分为FA(Feature Ability)和PA(Particle Ability)两种类型,每种类型为开发者提供了不同的模板,以便实现不同的业务功能。
FA支持Page Ability,Page模板是FA唯一支持的模板,用于提供与用户交互的能力。一个Page实例可以包含一组相关页面,每个页面用一个AbilitySlice实例表示。
PA支持Service Ability和Data Ability。
Service模板,用于提供后台运行任务的能力。Data模板,用于对外部提供统一的数据访问抽象。
在配置文件(config.json)中注册Ability时,可以通过配置Ability元素中的type属性来指定Ability模板类型。其中type的取值可以为page、service或data,分别代表Page模板、Service模板、Data模板。为了便于表述,后文中将基于Page模板、Service模板、Data模板实现的Ability分别简称为Page、Service、Data。
Ability配置示例代码如下:
"//": "第3章/Ability类型设置"
{
"module": {
...
"abilities": [
{
...
"type": "page"
...
}
]
...
}
...
}
3.3.2Page Ability基本概念
1. Page与AbilitySlice
Page模板(以下简称Page)是FA唯一支持的模板,用于提供与用户交互的能力。一个Page可以由一个或多个AbilitySlice构成,AbilitySlice是指应用的
图311Page与AbilitySlice
单个页面及其控制逻辑的总和。当一个Page由多个AbilitySlice共同构成时,这些AbilitySlice页面提供的业务能力具有高度相关性。例如,邮件功能可以通过一个Page实现,其中包含两个AbilitySlice。一个AbilitySlice用于展示邮件列表,另一个AbilitySlice用于展示邮件详情。Page和AbilitySlice的关系如图311所示。
相比于桌面场景,移动场景下应用之间的交互更为频繁。通常,单个应用专注于某个方面的能力开发,当它需要其他能力辅助时,会调用其他应用提供的能力。例如,打车应用提供了联系司机的业务功能入口,当用户在使用该功能时,会跳转到通话应用的拨号页面。与此类似,HarmonyOS支持不同Page之间的跳转,并可以指定跳转到目标Page中某个具体的AbilitySlice。
AbilitySlice路由可配置。虽然一个Page可以包含多个AbilitySlice,但是Page进入前台时界面默认只展示一个AbilitySlice。默认展示的AbilitySlice是通过setMainRoute()方法来指定的。如果需要更改默认展示的AbilitySlice,则可以通过addActionRoute()方法为此AbilitySlice配置一条路由规则。此时,当其他Page实例期望导航到此AbilitySlice时,可以在Intent中指定Action,同Page间导航方法不一样。
setMainRoute()方法与addActionRoute()方法的使用,示例代码如下:
//第3章/Page和Ability-路由配置setMainRoute()方法与addActionRoute()方法的使用
public class MyAbility extends Ability {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
//设置主路由
setMainRoute(MainSlice.class.getName());
//设置操作路由
addActionRoute("action.pay", PaySlice.class.getName());
addActionRoute("action.scan", ScanSlice.class.getName());
}
}
为在addActionRoute()方法中使用的动作命名,需要在应用配置文件(config.json)中注册,代码如下:
"//": "第3章/注册addActionRoute()方法中使用的动作命名"
{
"module": {
"abilities": [
{
"skills":[
{
"actions":[
"action.pay",
"action.scan"
]
}
]
...
}
]
...
}
...
}
2. Page Ability生命周期
系统管理或用户操作等行为均会引起Page实例在其生命周期的不同状态之间进行转换。Ability类提供的回调机制能够让Page及时感知外界变化,从而正确地应对状态变化,例如释放资源,这有助于提升应用的性能和稳健性。
Page生命周期的不同状态转换及其对应的回调如图312所示。
图312Page生命周期
具体说明如下:
(1) INACTIVE状态是一种短暂存在的状态,可理解为“激活中”。
(2) onStart(): 当系统首次创建Page实例时,触发该回调。对于一个Page实例,该回调在其整个生命周期中仅触发一次,Page在该逻辑后将进入INACTIVE状态。开发者必须重写该方法,并在此配置默认展示的AbilitySlice,代码如下:
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(FooSlice.class.getName());
}
(3) onActive(): Page会在进入INACTIVE状态后来到前台,然后系统调用此回调。Page在此之后进入ACTIVE状态,该状态是应用与用户交互的状态。Page将保持在此状态,除非某类事件发生而导致Page失去焦点,例如用户单击返回键或导航到其他Page。当此类事件发生时,会触发Page回到INACTIVE状态,系统将调用onInactive()回调。此后,Page可能重新回到ACTIVE状态,系统将再次调用onActive()回调,因此,开发者通常需要成对实现onActive()和onInactive(),并在onActive()中获取在onInactive()中被释放的资源。
(4) onInactive(): 当Page失去焦点时,系统将调用此回调,此后Page进入INACTIVE状态。开发者可以在此回调中实现Page失去焦点时应表现的恰当行为。
(5) onBackground(): 如果Page不再对用户可见,系统将调用此回调通知开发者用户进行相应的资源释放,此后Page进入BACKGROUND状态。开发者应该在此回调中释放Page不可见时无用的资源,或在此回调中执行较为耗时的状态保存操作。
(6) onForeground(): 处于BACKGROUND状态的Page仍然驻留在内存中,当重新回到前台时(例如用户重新导航到此Page),系统将先调用onForeground()回调通知开发者,而后Page的生命周期状态回到INACTIVE状态。开发者应当在此回调中重新申请在onBackground()中释放的资源,最后Page的生命周期状态进一步回到ACTIVE状态,系统将通过onActive()回调通知开发者用户。
(7) onStop(): 系统将要销毁Page时,将会触发此回调函数,通知用户进行系统资源的释放。销毁Page的可能原因包括以下几方面。一是用户通过系统管理能力关闭指定Page,例如使用任务管理器关闭Page。二是用户行为触发Page的terminateAbility()方法调用,例如使用应用的退出功能。三是配置变更导致系统暂时销毁Page并重建。四是系统出于资源管理的目的,自动触发对处于BACKGROUND状态Page的销毁。
3. AbilitySlice生命周期
AbilitySlice作为Page的组成单元,其生命周期依托于其所属Page的生命周期。AbilitySlice和Page具有相同的生命周期状态和同名的回调,当Page生命周期发生变化时,它的AbilitySlice也会发生相同的生命周期变化。此外,AbilitySlice还具有独立于Page的生命周期变化,这发生在同一Page中的不同AbilitySlice之间进行导航时,此时Page的生命周期状态不会改变。
AbilitySlice生命周期回调与Page的相应回调类似,因此不再赘述。由于AbilitySlice承载具体的页面,开发者必须重写AbilitySlice的onStart()回调,并在此方法中通过setUIContent()方法设置页面,示例代码如下:
//第3章/重写AbilitySlice的onStart()回调,setUIContent()方法的使用
@Override
protected void onStart(Intent intent) {
super.onStart(intent);
setUIContent(ResourceTable.Layout_main_layout);
}
AbilitySlice实例的创建和管理通常由应用负责,系统仅在特定情况下会创建AbilitySlice实例。例如,当通过导航启动某个AbilitySlice时,由系统负责实例化,但是在同一个Page中的不同AbilitySlice间进行导航时则由应用负责实例化。
4. Page与AbilitySlice生命周期关联
当AbilitySlice处于前台且具有焦点时,其生命周期状态随着所属Page的生命周期状态的变化而变化。当一个Page拥有多个AbilitySlice时,例如MyAbility下有FooAbilitySlice和BarAbilitySlice,当前FooAbilitySlice处于前台并获得焦点,并且即将导航到BarAbilitySlice时,在此期间的生命周期状态变化顺序如下:
FooAbilitySlice从ACTIVE状态变为INACTIVE状态。BarAbilitySlice则从INITIAL状态首先变为INACTIVE状态,然后变为ACTIVE状态(假定此前BarAbilitySlice未曾启动)。FooAbilitySlice从INACTIVE状态变为BACKGROUND状态。
对应两个slice的生命周期方法回调顺序,代码如下:
FooAbilitySlice.onInactive() --> BarAbilitySlice.onStart() --> BarAbilitySlice.onActive() --> FooAbilitySlice.onBackground()
在整个流程中,MyAbility始终处于ACTIVE状态,但是,当Page被系统销毁时,其所有已实例化的AbilitySlice将被联动销毁,而不仅是处于前台的AbilitySlice。
5. AbilitySlice间导航
1) 同一Page内导航
当发起导航的AbilitySlice和导航目标的AbilitySlice处于同一个Page时,可以通过present()方法实现导航。如下代码片段展示如何通过单击按钮导航到其他AbilitySlice的方法,代码如下:
//第3章/AbilitySlice间导航案例
@Override
protected void onStart(Intent intent) {
...
Button button = ...;
button.setClickedListener(listener -> present(new TargetSlice(), new Intent()));
...
}
如果开发者希望在用户从导航目标AbilitySlice返回时能够获得返回结果,则应当使用presentForResult()实现导航。用户从导航目标AbilitySlice返回时,系统将回调onResult()来接收和处理返回结果,开发者需要重写该方法。返回结果由导航目标AbilitySlice在其生命周期内通过setResult()进行设置,具体的代码如下:
//第3章/presentForResult()方法的使用
int requestCode = positiveInteger; //Any positive integer.
@Override
protected void onStart(Intent intent) {
...
Button button = ...;
button.setClickedListener(
listener -> presentForResult(new TargetSlice(), new Intent(), positiveInteger));
...
}
@Override
protected void onResult(int requestCode, Intent resultIntent) {
if (requestCode == positiveInteger) {
//Process resultIntent here.
}
}
系统为每个Page维护了一个AbilitySlice实例的栈,每个进入前台的AbilitySlice实例均会入栈。当开发者在调用present()或presentForResult()时,如果指定的AbilitySlice实例已经在栈中存在,则栈中位于此实例之上的AbilitySlice均会出栈并终止其生命周期。在前面的示例代码中,导航时指定的AbilitySlice实例均是新建的,即便重复执行此代码,此时作为导航目标的这些实例属于同一个类,所以不会导致任何AbilitySlice出栈。
2) 不同Page间导航
AbilitySlice作为Page的内部单元,以Action的形式对外暴露,因此可以通过配置Intent的Action导航到目标AbilitySlice。Page间的导航可以使用startAbility()或startAbilityForResult()方法,获得返回结果的回调为onAbilityResult()。在Ability中调用setResult()可以设置返回结果。详细用法可参考根据Operation的其他属性启动应用中的示例。
6. 设备迁移
设备迁移(下文简称“迁移”)支持将Page在同一用户的不同设备间迁移,以便支持用户无缝切换的诉求。以Page从设备A迁移到设备B为例,迁移动作主要步骤如下:
设备A上的Page请求迁移。HarmonyOS处理迁移任务,并回调设备A上Page的保存数据方法,用于保存迁移必需的数据。HarmonyOS在设备B上启动同一个Page,并回调其恢复数据方法。
7. 具有迁移功能的Page开发
第一步,实现IAbilityContinuation接口。
一个应用可能包含多个Page,仅需要在支持迁移的Page中通过以下方法实现IAbilityContinuation接口。同时,此Page所包含的所有AbilitySlice也需要实现此接口。
onStartContinuation()的使用。Page请求迁移后,系统首先回调此方法,开发者可以在此回调中决策当前是否可以执行迁移,例如,弹框让用户确认是否开始迁移。
onSaveData()的使用。如果onStartContinuation()返回值为true,则系统回调此方法,开发者在此回调中保存必须传递到另外设备上以便恢复Page状态的数据。
onRestoreData()的使用。源侧设备上Page完成保存数据后,系统在目标侧设备上回调此方法,开发者在此回调中接收用于恢复Page状态的数据。注意,在目标侧设备上的Page会重新启动其生命周期,无论其启动模式如何配置,系统回调此方法的时机都在onStart()之前。
onCompleteContinuation()的使用。在目标侧设备上恢复数据一旦完成,系统就会在源侧设备上回调Page的此方法,以便通知应用迁移流程已结束。开发者可以在此检查迁移结果是否成功,并在此处理迁移结束的动作,例如,应用可以在迁移完成后终止自身生命周期。
onFailedContinuation()的使用。如果在迁移过程中发生异常,系统则会在发起端设备上回调FA的此方法,以便通知应用迁移流程发生的异常。并不是所有异常都会回调FA的此方法,仅局限于该接口枚举的异常。开发者可以在此检查异常信息,并在此处理迁移异常发生后的动作,例如,应用可以提醒用户此时发生的异常信息。该接口从API Version 6开始提供,目前为Beta版本。
onRemoteTerminated()的使用。如果开发者使用continueAbilityReversibly()而不是continueAbility(),则此后可以在源侧设备上使用reverseContinueAbility()进行回迁。这种场景下,相当于同一个Page(的两个实例)同时在两个设备上运行,迁移完成后,如果目标侧设备上的Page因某种原因而终止,则源侧Page可通过此回调接收终止通知。
第二步,请求迁移。
实现IAbilityContinuation的Page可以在其生命周期内调用continueAbility()或continueAbilityReversibly()请求迁移。两者的区别是,通过后者发起的迁移此后可以进行回迁,实现代码如下:
//第3章/设备迁移/continueAbility()回迁
try {
continueAbility();
} catch (IllegalStateException e) {
//Maybe another continuation in progress.
...
}
以Page从设备A迁移到设备B为例,详细的流程如下:
设备A上的Page请求迁移。
系统回调设备A上的Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onStartContinuation()方法,以确认当前是否可以立即迁移。
如果可以立即迁移,则系统回调设备A上的Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onSaveData()方法,以便保存迁移后恢复状态所需的数据。
如果保存数据成功,则系统在设备B上启动同一个Page,并恢复AbilitySlice栈,然后回调IAbilityContinuation.onRestoreData()方法,传递此前保存的数据,此后设备B上此Page从onStart()开始其生命周期回调。
系统回调设备A上的Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onCompleteContinuation()方法,通知数据恢复成功与否。
如果在迁移过程中发生异常,系统则回调设备A上的Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onFailedContinuation()方法,通知在迁移过程中发生异常,并不是所有异常都会回调FA的此方法,仅局限于该接口枚举的异常。
第三步,请求回迁。
使用continueAbilityReversibly()请求迁移并完成后,源侧设备上已迁移的Page可以发起回迁,以便使用户活动重新回到此设备,代码如下:
//第3章/使用continueAbilityReversibly()请求回迁
try {
reverseContinueAbility();
} catch (IllegalStateException e) {
//Maybe another continuation in progress.
...
}
以Page从设备A迁移到设备B后并请求回迁为例,详细的流程如下:
设备A上的Page请求回迁。
系统回调设备B上的Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onStartContinuation()方法,以确认当前是否可以立即迁移。
如果可以立即迁移,则系统回调设备B上的Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onSaveData()方法,以便保存回迁后恢复状态所需的数据。
如果保存数据成功,则系统在设备A上的Page恢复AbilitySlice栈,然后回调IAbilityContinuation.onRestoreData()方法,传递此前保存的数据。
如果数据恢复成功,则系统终止设备B上的Page的生命周期。
3.3.3Service Ability基本概念
1. 综述
基于Service模板的Ability(以下简称Service)主要用于后台运行任务,如执行音乐播放、文件下载等,但不提供用户交互界面。
Service可由其他应用或Ability启动,即使用户切换到其他应用,Service仍将在后台继续运行。Service是单实例的。在一个设备上,相同的Service只会存在一个实例。如果多个Ability共用这个实例,只有当与Service绑定的所有Ability退出后,Service才能退出。由于Service是在主线程里执行的,因此,如果在Service里面的操作时间过长,开发者则必须在Service里创建新的线程来处理,这里涉及线程之间的通信,防止造成主线程阻塞,从而导致应用程序无响应。
2. 创建Service
创建Ability的子类,实现与Service相关的生命周期方法。Service也是一种Ability,Ability为Service提供了以下生命周期方法,开发者可以重写这些方法,来添加其他Ability请求与Service Ability交互时的处理方法。
(1) onStart()的使用: 该方法在创建Service的时候调用,用于Service的初始化。在Service的整个生命周期只会被调用一次,调用时传入的Intent应为空。
(2) onCommand()的使用: 在Service创建完成之后调用,该方法在客户端每次启动该Service时都会被调用,开发者可以在该方法中做一些调用统计、初始化类的操作。
(3) onConnect()的使用: 在Ability和Service连接时调用,该方法返回IRemoteObject对象,开发者可以在该回调函数中生成对应Service的IPC通信通道,以便Ability与Service交互。Ability可以多次连接同一个Service,系统会缓存该Service的IPC通信对象,只有当第1个客户端连接Service时,系统才会调用Service的onConnect()方法来生成IRemoteObject对象,而后系统会将同一个RemoteObject对象传递至其他连接同一个Service的所有客户端,而无须再次调用onConnect()方法。
(4) onDisconnect()的使用: 在Ability与绑定的Service断开连接时调用。
(5) onStop()的使用: 在Service被销毁时调用。Service应通过实现此方法来清理所有资源,如关闭线程、注册的侦听器等。
创建Service的示例代码如下:
//第3章/Service Ability 的使用案例
public class ServiceAbility extends Ability {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
}
@Override
public void onCommand(Intent intent, boolean restart, int startId) {
super.onCommand(intent, restart, startId);
}
@Override
public IRemoteObject onConnect(Intent intent) {
return super.onConnect(intent);
}
@Override
public void onDisconnect(Intent intent) {
super.onDisconnect(intent);
}
@Override
public void onStop() {
super.onStop();
}
}
注册Service,Service也需要在应用配置文件中进行注册,需要将注册类型type设置为service。注册Service的示例代码如下:
"//": "第3章注册Service"
{
"module": {
"abilities": [
{
"name": ".ServiceAbility",
"type": "service",
"visible": true
...
}
]
...
}
...
}
3. 启动Service
接下来介绍通过startAbility()启动Service及对应的停止方法。
启动Service,Ability为开发者提供了startAbility()方法来启动另外一个Ability。因为Service也是Ability的一种,开发者同样可以通过将Intent传递给该方法来启动Service。不仅支持启动本地Service,还支持启动远程Service。
开发者可以通过构造包含DeviceId、BundleName与AbilityName的Operation对象设置目标Service信息。这3个参数的含义包括以下内容。DeviceId表示设备ID。如果是本地设备,则可以直接留空; 如果是远程设备,则可以通过ohos.distributedschedule.interwork.DeviceManager提供的getDeviceList获取设备列表。BundleName表示包名称。AbilityName表示待启动的Ability名称。
启动本地设备Service的示例代码如下:
//第3章/启动本地Service Ability
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId("")
.withBundleName("com.domainname.hiworld.himusic")
.withAbilityName("com.domainname.hiworld.himusic.ServiceAbility")
.build();
intent.setOperation(operation);
startAbility(intent);
启动远程设备Service的示例代码如下:
//第3章/启动远程Service Ability
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId("deviceId")
.withBundleName("com.domainname.hiworld.himusic")
.withAbilityName("com.domainname.hiworld.himusic.ServiceAbility")
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
//设置支持分布式调度系统多设备启动的标识
.build();
intent.setOperation(operation);
startAbility(intent);
执行上述代码后,Ability将通过startAbility()方法来启动Service。
如果Service尚未运行,则系统会先调用onStart()来初始化Service,再回调Service的onCommand()方法来启动Service。
如果Service正在运行,则系统会直接回调Service的onCommand()方法来启动Service。
4. 停止Service
Service一旦创建就会一直保持在后台运行,除非必须回收内存资源,否则系统不会停止或销毁Service。开发者可以在Service中通过terminateAbility()停止本Service或在其他Ability调用stopAbility()来停止Service。
停止Service同样支持停止本地设备Service和停止远程设备Service,使用方法与启动Service一样。一旦调用停止Service的方法,系统便会尽快销毁Service。
5. 连接Service
如果Service需要与Page Ability或其他应用的Service Ability进行交互,则需创建用于连接的Connection。Service支持其他Ability通过connectAbility()方法与其进行连接。
在使用connectAbility()处理回调时,需要传入目标Service的Intent与IAbilityConnection的实例。IAbilityConnection提供了两种方法供开发者实现; onAbilityConnectDone()是用来处理连接Service成功的回调,onAbilityDisconnectDone()是用来处理Service异常死亡的回调。
创建连接Service回调实例的示例代码如下:
//第3章/创建连接Service回调实例
private IAbilityConnection connection = new IAbilityConnection() {
//连接到Service的回调
@Override
public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) {
//Client侧需要定义与Service侧相同的IRemoteObject实现类。开发者获取服务器端传
//过来IRemoteObject对象,并从中解析出服务器端传过来的信息
}
//Service异常死亡的回调
@Override
public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
}
};
连接Service的示例代码如下:
//第3章/ 连接Service
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId("deviceId")
.withBundleName("com.domainname.hiworld.himusic")
.withAbilityName("com.domainname.hiworld.himusic.ServiceAbility")
.build();
intent.setOperation(operation);
connectAbility(intent, connection);
同时,Service侧也需要在调用onConnect()时返回IRemoteObject,从而定义与Service进行通信的接口。onConnect()需要返回一个IRemoteObject对象,HarmonyOS提供了IRemoteObject的默认实现,用户可以通过继承LocalRemoteObject来创建自定义的实现类。Service侧把自身的实例返回给调用侧的示例代码如下:
//第3章/创建自定义IRemoteObject实现类
private class MyRemoteObject extends LocalRemoteObject {
MyRemoteObject(){
}
}
//把IRemoteObject返回客户端
@Override
protected IRemoteObject onConnect(Intent intent) {
return new MyRemoteObject();
}
6. Service Ability的生命周期
与Page类似,Service也拥有生命周期,如图313所示。根据调用方法的不同,其生命周期的启动与销毁有以下两种路径。
图313Service的生命周期
一是启动Service,该Service在其他Ability调用startAbility()时创建,然后保持运行。其他Ability通过调用stopAbility()来停止Service,Service停止后,系统会将其销毁。
另一种是连接Service,该Service在其他Ability调用connectAbility()时创建,客户端可通过调用disconnectAbility()断开连接。多个客户端可以绑定到相同的Service,而且当所有绑定全部取消后,系统即会销毁该Service。
connectAbility()也可以连接通过startAbility()创建的Service。
7. 前台Service
一般情况下,Service是在后台运行的,后台Service的优先级都比较低,当资源不足时,系统有可能回收正在运行的后台Service。在一些场景下(如播放音乐),用户希望应用能够一直保持运行,此时就需要使用前台Service。前台Service会始终保持正在运行的图标显示在系统状态栏上。
使用前台Service并不复杂,开发者只需在Service创建的方法里调用keepBackgroundRunning()将Service与通知绑定。调用keepBackgroundRunning()方法前需要在配置文件中声明ohos.permission.KEEP_BACKGROUND_RUNNING权限,同时还需要在配置文件中添加对应的backgroundModes参数。在onStop()方法中调用cancelBackgroundRunning()方法可停止前台Service。
使用前台Service的onStart()示例代码如下:
//第3章/前台Service的onStart()示例代码
//创建通知,其中1005为notificationId
NotificationRequest request = new NotificationRequest(1005);
NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();
content.setTitle("title").setText("text");
NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);
request.setContent(notificationContent);
//绑定通知,1005为创建通知时传入的notificationId
keepBackgroundRunning(1005, request);
在配置文件中,需要在module→abilities字段下对当前Service做如下配置,代码如下:
"//": "第3章/前台service的配置",
{
"name": ".ServiceAbility",
"type": "service",
"visible": true,
"backgroundModes": ["dataTransfer", "location"]
}
3.3.4Data Ability
1. 基本概念
使用Data模板的Ability(以下简称Data),有助于应用管理其自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。Data既可用于同设备不同应用的数据共享,也支持跨设备不同应用的数据共享。数据的存放形式多样,可以是数据库,也可以是磁盘上的文件。Data对外提供对数据的增、删、改、查操作,以及打开文件等接口,这些接口的具体实现由开发者提供。Data的提供方和使用方都通过URI(Uniform Resource Identifier)来标识一个具体的数据,例如数据库中的某个表或磁盘上的某个文件。HarmonyOS的URI仍基于URI通用标准,格式如图314所示。
图314URI格式
Scheme是协议方案名,固定为dataability,代表Data Ability所使用的协议类型。authority是设备ID。如果为跨设备场景,则为目标设备的ID; 如果为本地设备场景,则不需要填写。path是资源的路径信息,代表特定资源的位置信息。query是查询参数。fragment可以用于指示要访问的子资源。
我们用两种场景的URI进行示例说明。
跨设备场景,代码如下:
dataability://device_id/com.domainname.dataability.persondata/person/10
本地设备,代码如下:
dataability:///com.domainname.dataability.persondata/person/10
本地设备的device_id字段为空,因此在dataability:后面有3个“/”。
2. 创建Data
使用Data模板的Ability形式仍然是Ability,因此,开发者需要为应用添加一个或多个Ability的子类,来提供程序与其他应用之间的接口。Data为结构化数据和文件提供了不同API供用户使用,因此,开发者首先需要确定使用何种类型的数据。本节主要讲述创建Data的基本步骤和需要使用的接口。Data提供方可以自定义数据的增、删、改、查操作,以及文件打开等功能,并对外提供这些接口。
确定数据的存储方式,Data支持两种数据形式: 文件数据,如文本、图片、音乐等; 结构化数据,如数据库等。
实现UserDataAbility。UserDataAbility用于接收其他应用发送的请求,提供外部程序访问的入口,从而实现应用间的数据访问。实现UserDataAbility,需要在Project窗口当前工程的主目录(entry→src→main→java > com.xxx.xxx)选择File→New→Ability→Empty Data Ability,设置Data Name后完成UserDataAbility的创建。
Data提供了文件存储和数据库存储两组接口供用户使用。
1) 文件存储
开发者需要在Data中重写FileDescriptor openFile(Uri uri, String mode)方法来操作文件; uri为客户端传入的请求目标路径; mode为开发者对文件的操作选项,可选方式包含r(读)、w(写)、rw(读写)等。
开发者可通过MessageParcel静态方法dupFileDescriptor()复制待操作文件流的文件描述符,并将其返回,供远端应用访问文件。
根据传入的uri打开对应的文件示例,代码如下:
//第3章/文件存储-根据传入的uri打开对应的文件示例
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log");
@Override
public FileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File file = new File(uri.getDecodedPathList().get(0));
//get(0)用于获取URI完整字段中查询参数字段
if (mode == null || !"rw".equals(mode)) {
file.setReadOnly();
}
FileInputStream fileIs = new FileInputStream(file);
FileDescriptor fd = null;
try {
fd = fileIs.getFD();
} catch (IOException e) {
HiLog.info(LABEL_LOG, "failed to getFD");
}
//绑定文件描述符
return MessageParcel.dupFileDescriptor(fd);
}
2) 数据库存储
第一步,初始化数据库连接。系统会在应用启动时调用onStart()方法创建Data实例。在此方法中,开发者应该创建数据库连接,并获取连接对象,以便后续对数据库进行操作。为了避免影响应用启动速度,开发者应当尽可能地将非必要的耗时任务推迟到使用时执行,而不是在此方法中执行所有初始化。
初始化的时候连接数据库,示例代码如下:
//第3章/数据库存储-连接数据库示例
private static final String DATABASE_NAME = "UserDataAbility.db";
private static final String DATABASE_NAME_ALIAS = "UserDataAbility";
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log");
private OrmContext ormContext = null;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
DatabaseHelper manager = new DatabaseHelper(this);
ormContext = manager.getOrmContext(DATABASE_NAME_ALIAS, DATABASE_NAME, BookStore.class);
}
第二步,编写数据库操作方法。
Ability定义了6种方法供用户对数据库表数据进行增、删、改、查处理。这6种方法在Ability中已默认实现,开发者可按需重写。具体方法如下:
方法ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates)用来查询数据库。方法int insert(Uri uri, ValuesBucket value)用来向数据库插入单条数据。方法int batchInsert(Uri uri, ValuesBucket[] values)用来向数据库插入多条数据。方法int delete(Uri uri, DataAbilityPredicates predicates)用来删除一条或多条数据。方法int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates)用来更新数据库。方法DataAbilityResult[] executeBatch(ArrayList operations)用来批量操作数据库。
这些方法的具体使用说明如下:
query()方法接收3个参数,分别是查询的目标路径、查询的列名和查询条件。查询条件由类DataAbilityPredicates构建,根据传入的列名和查询条件查询用户表的示例代码如下:
//第3章/编写数据库操作方法-数据查询
public ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) {
if (ormContext == null) {
HiLog.error(LABEL_LOG, "failed to query, ormContext is null");
return null;
}
//查询数据库
OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
ResultSet resultSet = ormContext.query(ormPredicates, columns);
if (resultSet == null) {
HiLog.info(LABEL_LOG, "resultSet is null");
}
//返回结果
return resultSet;
}
insert()方法接收两个参数,分别是插入的目标路径和插入的数值。其中,插入的数值由ValuesBucket封装,服务器端可以从该参数中解析出对应的属性,然后插入数据库。此方法返回一个int类型的值,用于标识结果。接收到传过来的用户信息并把它保存到数据库的示例代码如下:
//第3章/编写数据库操作方法-添加数据
public int insert(Uri uri, ValuesBucket value) {
//参数校验
if (ormContext == null) {
HiLog.error(LABEL_LOG, "failed to insert, ormContext is null");
return -1;
}
//构造插入数据
User user = new User();
user.setUserId(value.getInteger("userId"));
user.setFirstName(value.getString("firstName"));
user.setLastName(value.getString("lastName"));
user.setAge(value.getInteger("age"));
user.setBalance(value.getDouble("balance"));
//编写数据库操作方法-插入数据库
boolean isSuccessful = ormContext.insert(user);
if (!isSuccessful) {
HiLog.error(LABEL_LOG, "failed to insert");
return -1;
}
isSuccessful = ormContext.flush();
if (!isSuccessful) {
HiLog.error(LABEL_LOG, "failed to insert flush");
return -1;
}
DataAbilityHelper.creator(this, uri).notifyChange(uri);
int id = Math.toIntExact(user.getRowId());
return id;
}
batchInsert()方法为批量插入方法,接收一个ValuesBucket数组,用于单次插入一组对象。它的作用是提高插入多条重复数据的效率。该方法系统已实现,开发者可以直接调用。
delete()方法用来执行删除操作。删除条件由类DataAbilityPredicates构建,服务器端在接收到该参数之后可以从中解析出要删除的数据,然后在数据库中执行。根据传入的条件删除用户表数据的示例代码如下:
//第3章/编写数据库操作方法-删除数据
public int delete(Uri uri, DataAbilityPredicates predicates) {
if (ormContext == null) {
HiLog.error(LABEL_LOG, "failed to delete, ormContext is null");
return -1;
}
OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
int value = ormContext.delete(ormPredicates);
DataAbilityHelper.creator(this, uri).notifyChange(uri);
return value;
}
update()方法用来执行更新操作。用户可以在ValuesBucket参数中指定要更新的数据,以及在DataAbilityPredicates中构建更新的条件等。更新用户表的数据的示例代码如下:
//第3章/更新用户表的数据
public int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) {
if (ormContext == null) {
HiLog.error(LABEL_LOG, "failed to update, ormContext is null");
return -1;
}
OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
int index = ormContext.update(ormPredicates, value);
HiLog.info(LABEL_LOG, "UserDataAbility update value:" + index);
DataAbilityHelper.creator(this, uri).notifyChange(uri);
return index;
}
executeBatch()方法用来批量执行操作。DataAbilityOperation中提供了设置操作类型、数据和操作条件的方法,用户可自行设置自己要执行的数据库操作。该方法系统已实现,开发者可以直接调用。
需要说明的是在上述代码示例中,初始化了数据库类BookStore.class,并通过实体类User.class对该数据库的表User进行增、删、改、查操作。
3) 注册UserDataAbility
和Service类似,开发者必须在配置文件中注册Data。配置文件中该字段在创建Data Ability时会自动创建,name与创建的Data Ability一致。
需要关注以下属性: type为类型,设置为data; uri为对外提供的访问路径,全局唯一; permissions为访问该data ability时需要申请的访问权限。
如果权限非系统权限,则需要在配置文件中进行自定义,示例代码如下:
"//":"第3章/注册UserDataAbility-自定义DataAbility访问权限"
{
"name": ".UserDataAbility",
"type": "data",
"visible": true,
"uri": "dataability://com.example.myapplication5.DataAbilityTest",
"permissions": [
"com.example.myapplication5.DataAbility.DATA"
]
}
3. 访问Data
开发者可以通过DataAbilityHelper类访问当前应用或其他应用提供的共享数据。DataAbilityHelper作为客户端,与提供方的Data进行通信。Data接收到请求后,执行相应处理,并返回结果。DataAbilityHelper提供了一系列与Data Ability对应的方法。下面介绍DataAbilityHelper具体的使用步骤。
1) 声明使用权限
如果待访问的Data声明了访问需要权限,则访问此Data时应在配置文件中声明需要此权限。声明时需要参考权限申请字段说明,示例代码如下:
"//":"第3章/声明使用权限-声明数据访问权限"
"reqPermissions": [
{
"name": "com.example.myapplication5.DataAbility.DATA"
},
//访问文件还需要添加访问存储读写权限
{
"name": "ohos.permission.READ_USER_STORAGE"
},
{
"name": "ohos.permission.WRITE_USER_STORAGE"
}
]
2) 创建DataAbilityHelper
DataAbilityHelper为开发者提供了creator()方法,以此创建DataAbilityHelper实例。该方法为静态方法,有多个重载。最常见的方法是通过传入一个context对象来创建DataAbilityHelper对象。获取helper对象,示例代码如下:
DataAbilityHelper helper = DataAbilityHelper.creator(this);
3) 访问Data Ability
DataAbilityHelper为开发者提供了一系列接口,用于访问不同类型的数据(文件、数据库等)。
DataAbilityHelper为开发者提供了FileDescriptor openFile(Uri uri, String mode)方法来操作文件。此方法需要传入两个参数,其中uri用来确定目标资源路径,mode用来指定打开文件的方式,可选方式包含r(读)、w(写)、rw(读写)、wt(覆盖写)、wa(追加写)、rwt(覆盖写且可读)。该方法返回一个目标文件的FD(文件描述符),把文件描述符封装成流,开发者就可以对文件流进行自定义处理。访问文件,示例代码如下:
//读取文件描述符
FileDescriptor fd = helper.openFile(uri, "r");
FileInputStream fis = new FileInputStream(fd);
//使用文件描述符封装成的文件流,进行文件操作
访问数据库。DataAbilityHelper为开发者提供了增、删、改、查及批量处理等方法,以此来操作数据库。对数据库的操作主要包括以下方法。
方法ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates)用来查询数据库。方法int insert(Uri uri, ValuesBucket value)用来向数据库插入单条数据。方法int batchInsert(Uri uri, ValuesBucket[] values)用来向数据库插入多条数据。方法int delete(Uri uri, DataAbilityPredicates predicates)用来删除一条或多条数据。方法int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates)用来更新数据库。方法DataAbilityResult[] executeBatch(ArrayList operations)用来批量操作数据库。
这些方法的使用说明如下:
query()查询方法,其中uri为目标资源路径,columns为想要查询的字段。开发者的查询条件可以通过DataAbilityPredicates来构建。查询用户表中id为101~103的用户,并把结果打印出来,示例代码如下:
//第3章/DataAbility数据查询
DataAbilityHelper helper = DataAbilityHelper.creator(this);
//构造查询条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.between("userId", 101, 103);
//进行查询
ResultSet resultSet = helper.query(uri, columns, predicates);
//处理结果
resultSet.goToFirstRow();
do {
//在此处理ResultSet中的记录;
} while(resultSet.goToNextRow());
insert()新增方法,其中uri为目标资源路径,ValuesBucket为要新增的对象。插入一条用户信息的示例代码如下:
//第3章/DataAbility数据添加
DataAbilityHelper helper = DataAbilityHelper.creator(this);
//构造插入数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString("name", "Tom");
valuesBucket.putInteger("age", 12);
helper.insert(uri, valuesBucket);
batchInsert()批量插入方法,和insert()类似。批量插入用户信息的示例代码如下:
//第3章/DataAbility数据批量添加
DataAbilityHelper helper = DataAbilityHelper.creator(this);
//构造插入数据
ValuesBucket[] values = new ValuesBucket[2];
values[0] = new ValuesBucket();
values[0].putString("name", "Tom");
values[0].putInteger("age", 12);
values[1] = new ValuesBucket();
values[1].putString("name", "Tom1");
values[1].putInteger("age", 16);
helper.batchInsert(uri, values);
delete()删除方法,其中删除条件可以通过DataAbilityPredicates来构建。删除用户表中id为101~103的用户,示例代码如下:
//第3章/DataAbility数据删除
DataAbilityHelper helper = DataAbilityHelper.creator(this);
//构造删除条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.between("userId", 101, 103);
helper.delete(uri, predicates);
update()更新方法,更新数据由ValuesBucket传入,更新条件由DataAbilityPredicates构建。更新id为102的用户,示例代码如下:
//第3章/DataAbility数据删除
DataAbilityHelper helper = DataAbilityHelper.creator(this);
//构造更新条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.equalTo("userId", 102);
//构造更新数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString("name", "Tom");
valuesBucket.putInteger("age", 12);
helper.update(uri, valuesBucket, predicates);
executeBatch()
DataAbilityOperation中提供了设置操作类型、数据和操作条件的方法,开发者可自行设置要执行的数据库操作。插入多条数据的示例代码如下:
//第3章/DataAbilityOperation的使用
DataAbilityHelper helper = DataAbilityHelper.creator(abilityObj, insertUri);
//构造批量操作
ValuesBucket value1 = initSingleValue();
DataAbilityOperation opt1 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value1).build();
ValuesBucket value2 = initSingleValue2();
DataAbilityOperation opt2 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value2).build();
ArrayList operations = new ArrayList();
operations.add(opt1);
operations.add(opt2);
DataAbilityResult[] result = helper.executeBatch(insertUri, operations);
3.3.5Intent
1. 基本概念
Intent是对象之间传递信息的载体。例如,当一个Ability需要启动另一个Ability时,或者当一个AbilitySlice需要导航到另一个AbilitySlice时,可以通过Intent指定启动的目标,与此同时指定携带相关数据。
Intent的构成元素包括Operation与Parameters,具体描述如下。
1) 属性Operation
子属性Action表示动作,通常使用系统预置的Action,应用也可以自定义Action。例如IntentConstants.ACTION_HOME表示返回桌面动作。子属性Entity表示类别,通常使用系统预置的Entity,应用也可以自定义Entity。例如Intent.ENTITY_HOME表示在桌面显示图标。子属性Uri表示Uri描述。如果在Intent中指定了Uri,则Intent将匹配指定的Uri信息,包括scheme、schemeSpecificPart、authority和path信息。子属性Flags表示处理Intent的方式。例如Intent.FLAG_ABILITY_CONTINUATION标记在本地的一个Ability是否可以迁移到远端设备继续运行。子属性BundleName表示包描述。如果在Intent中同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。子属性AbilityName表示待启动的Ability名称。如果在Intent中同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。子属性DeviceId 表示运行指定Ability的设备ID。
2) Parameters
Parameters是一种支持自定义的数据结构,开发者可以通过Parameters传递某些请求所需的额外信息。
2. 两种类型
当Intent用于发起请求时,根据指定元素的不同,可分为以下两种类型:
如果同时指定了BundleName与AbilityName,则根据Ability的全称(例如com.demoapp.FooAbility)来直接启动应用。
如果未同时指定BundleName和AbilityName,则根据Operation中的其他属性来启动应用。
Intent设置属性时,必须先使用Operation设置属性。如果需要新增或修改属性,则必须在设置Operation后再执行操作。关于Intent最简单的使用方法,可参见快速入门的示例代码。其中“实现页面跳转”重点描述了使用Intent实现两个页面跳转关系的操作。
1) 根据Ability的全称启动应用
通过构造包含BundleName与AbilityName的Operation对象,可以启动一个Ability并导航到该Ability,示例代码如下:
//第3章/DataAbilityOperation的使用
Intent intent = new Intent();
//通过Intent中的OperationBuilder类构造operation对象,指定设备标识(空串表示当前设备)、
//应用包名、Ability名称
Operation operation = new Intent.OperationBuilder()
.withDeviceId("")
.withBundleName("com.demoapp")
.withAbilityName("com.demoapp.FooAbility")
.build();
//把operation设置到intent中
intent.setOperation(operation);
startAbility(intent);
作为处理请求的对象,会在相应的回调方法中接收请求方传递的Intent对象。以导航到另一个Ability为例,导航的目标Ability可以在其onStart()回调的参数中获得Intent对象。
2) 根据Operation的其他属性启动应用
有些场景下,开发者需要在应用中使用其他应用提供的某种能力,而不感知提供该能力的应用具体是哪一个应用。例如如果开发者需要通过浏览器打开一个链接,而不关心用户最终选择哪一个浏览器应用,则可以通过Operation的其他属性(除BundleName与AbilityName之外的属性)描述需要的能力。如果设备上存在多个应用提供同种能力,系统则弹出候选列表,由用户选择由哪个应用处理请求。以下示例展示如何使用Intent跨Ability查询天气信息。
请求方在Ability中构造Intent及包含Action的Operation对象,并调用startAbilityForResult()方法发起请求,然后重写onAbilityResult()回调方法,对请求结果进行处理,示例代码如下:
//第3章/重写onAbilityResult()回调方法
private void queryWeather() {
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withAction(Intent.ACTION_QUERY_WEATHER)
.build();
intent.setOperation(operation);
startAbilityForResult(intent, REQ_CODE_QUERY_WEATHER);
}
@Override
protected void onAbilityResult(int requestCode, int resultCode, Intent resultData) {
switch (requestCode) {
case REQ_CODE_QUERY_WEATHER:
//Do something with result.
...
return;
default:
...
}
}
处理方作为处理请求的对象,首先需要在配置文件中声明对外提供的能力,以便系统据此找到此能力并作为候选的请求处理者,示例代码如下:
"//":"第3章/声明对外提供的能力"
{
"module": {
...
"abilities": [
{
...
"skills":[
{
"actions":[
"ability.intent.QUERY_WEATHER"
]
}
]
...
}
]
...
}
...
}
在Ability中配置路由以便支持以此action导航到对应的AbilitySlice,示例代码如下:
//第3章/action导航到对应的AbilitySlice
@Override
protected void onStart(Intent intent) {
...
addActionRoute(Intent.ACTION_QUERY_WEATHER, DemoSlice.class.getName());
...
}
在Ability中处理请求,并调用setResult()方法暂存返回结果,示例代码如下:
//第3章/请求结果处理
@Override
protected void onActive() {
...
Intent resultIntent = new Intent();
setResult(0, resultIntent); //0为当前Ability销毁后返回的resultCode
...
}