一文搞懂鸿蒙应用开发

一、鸿蒙系统的概述

1.1 鸿蒙系统是一个什么样的系统,亮点在哪里

    官网首页上一句话告诉了我们鸿蒙系统是一个什么样的系统:分布式能力造就新硬件、新交互、新服务,打开焕然一新的全场景世界。

    解释一下:分布式能力是基础,在基础上打造了三个新,由三个新构筑了一个全新的场景世界。

    分布式:由鸿蒙OS底层利用各种通信信道帮助我们构建起一个泛鸿蒙设备的没有中心的设备网络,设备与设备之间自动发现并连接彼此,形成一个华为工程师叫“软总线”分布式抽象布局。这种布局让设备都具有分布式能力,我们可以很轻松地让多设备中的数据、文件在应用中都具有一致性,在一个设备上修改数据,上传文件,其他设备自动同步,我们应用开发者不在需要去建立设备与设备之间的连接,维护这个连接去进行数据通信,我们只需要用就可以了o((≧▽≦o)!调用手机以外的设备和调用本地设备几乎没什么区别,大大简化我们的多设备协同的开发难度!赞!

鸿蒙应用开发入门(一):鸿蒙系统的概述-鸿蒙开发者社区

    对用户而言,手机不仅仅是原来的手机了,因为现在鸿蒙手机可以成为无数个设备的操作接口,它的能力和以前不可同日而语了,所以官网上说鸿蒙的手机是新设备!华为的工程师叫这个为“超级终端”!

    设备上的应用在鸿蒙OS下,不在是设备孤立的,我们可以方便地进行互转,让用户在不同设备上自如切换,形成了一个应用可以在多设备上任意切换的新交互:

鸿蒙应用开发入门(一):鸿蒙系统的概述-鸿蒙开发者社区

    鸿蒙OS打破了传统的APP安装的模式,原本的APP应用相互独立,功能“强大”,一个APP安装动辄100M以上,里边的很多功能其实我们真的是用不到的,比如使用美的微波炉,我就只需要给微波炉设置一下温度和时间而已,但APP里的东西远不止于此,鸿蒙OS将应用的能力细分为了很多颗粒,这种颗粒华为工程师叫Ability,APP在应用市场里是以Ability的形式存在的,我们需要微波炉设置一下温度和时间,用鸿蒙手机时,它只会拉取对应的Ability,不会将我们不需要的能力安装到我们的手机上了,手机终于可以不在“肥胖”了,实现按需呈现,爽不爽?这就是鸿蒙OS的新服务!

    基于上述的技术基础,鸿蒙OS还给我们解决了一个很大的痛点,原来我们手机想要控制设备,很麻烦,连接设备的步骤多,时间长,有时可能还需要网络专业方面的知识,普通用户大多只有放弃,不去连接:

鸿蒙应用开发入门(一):鸿蒙系统的概述-鸿蒙开发者社区

    使用鸿蒙OS,简单、方便、无感,我们只需手机碰一碰设备,自动连接上设备,自动从应用市场拉去下控制设备的Ability,用户感觉不到有APP的安装,只是看到控制设备的页面被打开了而已,安逸不安逸?

鸿蒙应用开发入门(一):鸿蒙系统的概述-鸿蒙开发者社区

1.2 学习鸿蒙应用开发首先要了解的一个最最基础的概念:Ability
    Ability是应用所具备能力的抽象,也是应用程序的重要组成部分。一个应用可以具备多种能力(即可以包含多个Ability),HarmonyOS支持应用以Ability为单位进行部署。Ability可以分为FA(Feature Ability)和PA(Particle Ability)两种类型,每种类型为开发者提供了不同的模板,以便实现不同的业务功能。
    1. FA支持Page Ability:
    Page模板是FA唯一支持的模板,用于提供与用户交互的能力。一个Page实例可以包含一组相关页面,每个页面用一个AbilitySlice实例表示。
    2. PA支持Service Ability和Data Ability:
    1)Service模板:用于提供后台运行任务的能力。
    2)Data模板:用于对外部提供统一的数据访问抽象。

二、开发环境搭建

2.1 工具下载和安装:
    下载地址:https://developer.harmonyos.com/cn/develop/deveco-studio

    安装下一步.....就可以,没有坑!

2.2 下载HarmonyOS SDK
    DevEco Studio提供SDK Manager统一管理SDK和这个包依赖的工具链。通过SDK Manager能自动下载各个语言对应的SDK包。SDK Manager提供多种编程语言的SDK包,各SDK包的说明请参考:
    1. Native:C/C++语言SDK包,默认不自动下载,需手动勾选下载。
    2. JS:JS语言SDK包,默认不自动下载,需手动勾选下载。
    3. Java:Java语言SDK包,首次下载SDK时默认下载。

    SDK对应的工具链(SDK Tools)和预览器:
    1. Toolchains:SDK工具链,HarmonyOS应用开发必备工具集,包括编译、打包、签名、数据库管理等工具的集合,首次下载SDK时默认下载。
    2. Previewer:Lite Wearable预览器,在开发过程中可以动态预览Lite Wearable应用的界面呈现效果,默认不自动下载,需手动勾选下载。


2.3 安装IDE时候的坑1:一直加载gradle
    1. 下载指定的gadle版本
    可以到 官网下载或者借助网友提供的下载好的版本,直接下载使用。

    2. 关闭DevEco studio,打开DevEco studio自动生成的一个目录
    默认在这个目录:C:\Users\Administrator\.gradle\wrapper\dists\gradle-6.3-all\b4awcolw...这串字符不同电脑不一样...u1obfh9i8

    3. 下载好的zip文件放进去
    将下载好的gradle文件复制在以上文件夹内(注意一定要放置在类似“b4awcolw...这串字符不同电脑不一样...u1obfh9i8”目录下),重启软件即可。


2.4 安装IDE时候的坑2:模拟器刷不出来
    开发应用的时候需要模拟器跑效果,需要在DevEco Studio菜单栏,点击Tools > HVD Manager开发模拟器,第一次打开,会自动下载相关文件。然后要求用华为开发者账号登录认证,开发者账号要求实名认证,没有自行注册认证就好。

    注意:浏览器一定要更新到最新版本,否则可能模拟器刷不出来。

2.5 推荐一个很好用的开源手机投屏工具scrcpy
    1. 获取地址:https://github.com/Genymobile/scrcpy
    2. scrcpy简介
    简单地来说,scrcpy就是通过adb调试的方式来将手机屏幕投到电脑上,并可以通过电脑控制手机设备。它可以通过USB连接,也可以通过Wifi连接(类似于隔空投屏),而且不需要任何root权限,不需要在手机里安装任何程序。scrcpy同时适用于Linux,Windows和macOS。

    3. 安装和使用
    1)绿色的下载到Windows某个目录,然后将目录添加到Path环境变量就可以了。
    2)使用USB进行连接设备
    (1)手机通过USB连接到PC上,首次连接会弹出是否信任该电脑,点击始终信任即可。
    (2)运行adb devices查看是否连接成功
    (3)运行“scrcpy -s 设备名称” ,只有一台设备直接scrcpy即可。

2.6 真机调试环境搭建步骤:

    1)在IDE中生成签名文件,以备用
    2)进入应用开发页面,进入管理中心,进入上架及推广服务 
    3)进入用户与访问,生成证书和管理设备(目前是受邀开发者可见),并且下载好证书已备用,添加设备需要UDID(获取UDID的命令,adb shell dumpsys DdmpDeviceMonitorService)
    4)在进入我的项目,添加项目,添加应用,注意应用的包名必须和你自己的包名一样
    5)在我的项目,对应的项目里,生成profile文件,下载以备用
    6)cer、p12、p7b三个文件齐了,在项目的属性,签名设置上设置上,就可以在真机上调试运行了

鸿蒙应用开发入门(二):开发环境搭建-鸿蒙开发者社区

三、开发第一个鸿蒙应用  

3.1 第一个鸿蒙应用实现需求
    编写两张页面,实现在第一张页面点击按钮跳转到第二张页面。在Java UI框架中,提供了两种编写布局的方式:在XML中声明UI布局和在代码中创建布局。这两种方式创建出的布局没有本质差别,都是我们需要熟悉方式,所以我们将通过XML的方式布局第一张页面,然后再通过代码的方式布局第二张页面。

3.2 用XML布局第一张页面

    1. 打开layout下面的“ability_main.xml”文件
    2. 在“ability_main.xml”文件中创建一个文本和一个按钮

<?xml version="1.0" encoding="utf-8"?>
<DependentLayout
        xmlns:ohos="http://schemas.huawei.com/res/ohos"
        ohos:width="match_parent"
        ohos:height="match_parent"
        ohos:background_element="#000000">
    <Text
            ohos:id="$+id:text"
            ohos:width="match_content"
            ohos:height="match_content"
            ohos:text="Hello World"
            ohos:text_color="white"
            ohos:text_size="32fp"
            ohos:center_in_parent="true"/>
    <Button
            ohos:id="$+id:button"
            ohos:width="match_content"
            ohos:height="match_content"
            ohos:text="Next"
            ohos:text_size="19fp"
            ohos:text_color="white"
            ohos:top_padding="8vp"
            ohos:bottom_padding="8vp"
            ohos:right_padding="80vp"
            ohos:left_padding="80vp"
            ohos:background_element="$graphic:background_button"
            ohos:below="$id:text"
            ohos:horizontal_center="true"
           />
</DependentLayout>

    3. 创建按钮的背景
    按钮的背景是通过“background_button”来指定的。右键点击“graphic”文件夹,选择“New > File”,命名为“background_button.xml”。

<?xml version="1.0" encoding="utf-8"?>
<shape  xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="oval">
    <solid ohos:color="#007DFF"/>
    <corners ohos:radius="20"/>
</shape>

3.3 用编程的方式布局第二张页面
    1. 创建Feature Ability
    2. 代码编写界面

public class SecondAbilitySlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        // 声明布局
        DependentLayout myLayout = new DependentLayout(this);
        // 设置页面布局大小和背景色
        myLayout.setWidth(MATCH_PARENT);
        myLayout.setHeight(MATCH_PARENT);
        ShapeElement element = new ShapeElement();
        element.setRgbColor(new RgbColor(255, 255, 255));
        myLayout.setBackground(element);
        // 创建一个文本
        Text text = new Text(this);
        text.setText("Nice to meet you.");
        text.setTextSize(55);
        text.setTextColor(Color.BLACK);
        // 设置文本的布局
        DependentLayout.LayoutConfig textConfig = 
                                    new DependentLayout.LayoutConfig(MATCH_CONTENT,MATCH_CONTENT);
        textConfig.addRule(DependentLayout.LayoutConfig.CENTER_IN_PARENT);
       
        text.setLayoutConfig(textConfig);
        myLayout.addComponent(text);
        super.setUIContent(myLayout);
    }
 
    @Override
    public void onActive() {
        super.onActive();
    }
 
    @Override
    public void onForeground(Intent intent) {
        super.onForeground(intent);
    }
}

3.4 实现页面跳转

public class MainAbilitySlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);
        Button button = (Button) findComponentById(ResourceTable.Id_button);
 
        if (button != null) {
            // 为按钮设置点击回调
            button.setClickedListener(new Component.ClickedListener() {
                @Override
                public void onClick(Component component) {
                    Intent secondIntent = new Intent();
                    // 指定待启动FA的bundleName和abilityName
                    Operation operation = new Intent.OperationBuilder()
                            .withDeviceId("")
                            .withBundleName("com.example.myapplication")
                            .withAbilityName("com.example.myapplication.SecondAbility")
                            .build();
                    secondIntent.setOperation(operation);
                    startAbility(secondIntent); // 通过AbilitySlice的startAbility接口实现启动另一个页面
                }
            });
        }
    }
 
    @Override
    public void onActive() {
        super.onActive();
    }
 
    @Override
    public void onForeground(Intent intent) {
        super.onForeground(intent);
    }
}

四、进一步了解第一个例子里的细节

4.1 开发完第一个鸿蒙应用后,下面在了解一下完整的鸿蒙应用打包发布后应该是什么样子:
   一个完整的打包后应用结构如下图所示,这里我们先了解结构,具体怎么打包很简单只要前提是要签名!

鸿蒙应用开发入门(四):进一步了解第一个例子里的细节-鸿蒙开发者社区

   1. HAP的分类
   HAP又可分为entry和feature两种模块类型:
   1)entry:应用的主模块。一个APP中,对于同一设备类型必须有且只有一个entry类型的HAP,可独立安装运行。
   2) feature:应用的动态特性模块。一个APP可以包含一个或多个feature类型的HAP,也可以不含。


   2. HAP的组成
   HAP是由代码(Ability)、第三方库、资源以及应用配置文件构成,只有包含Ability的HAP才能够独立运行。
   1)Ability
   Ability是应用所具备的能力的抽象,一个应用可以包含一个或多个Ability。Ability分为两种类型:FA(Feature Ability)和PA(Particle Ability)。FA/PA是应用的基本组成单元,能够实现特定的业务功能。FA有UI界面,而PA无UI界面。
   2)库文件
   库文件是应用依赖的第三方代码形式,存放在libs目录。
   3)资源文件
   应用的资源文件(字符串、图片、音频等)存放于resources目录下
   4)配置文件
   配置文件 (config.json) 是应用的Ability信息,用于声明应用的Ability,以及应用所需权限等信息


   3. pack.info文件
   描述应用软件包中每个HAP的属性,由IDE编译生成,应用市场根据该文件进行拆包和HAP的分类存储。 
   HAP的具体属性包括:
   1)delivery-with-install: 表示该HAP是否支持随应用安装。
   2)name:HAP文件名。
   3)module-type:模块类型,entry或feature。
   4)device-type:表示支持该HAP运行的设备类型。


4.2 config.json详细细节
   1. 应用的每个HAP的根目录下都存在一个“config.json”配置文件,主要涵盖以下三个方面:
   1)应用的全局配置信息,包含应用的包名、生产厂商、版本号等基本信息。
   2)应用在具体设备上的配置信息。
   3)HAP包的配置信息,包含每个Ability必须定义的基本属性(如包名、类名、类型以及Ability提供的能力),以及应用访问系统或其他应用受保护部分所需的权限等。


   2. config.json配置文件的内部结构
应用的配置文件“config.json”中由“app”、“deviceConfig”和“module”三个部分组成,缺一不可。


   3. “config.json”文件约定
   配置文件“config.json”采用JSON文件格式,由属性和值两部分构成:
   1)属性:属性出现顺序不分先后,且每个属性最多只允许出现一次。
   2)值:每个属性的值为JSON的基本数据类型(数值、字符串、布尔值、数组、对象或者null类型)。


   学习建议:根据官方文档自己做一个思维导图,做的过程中熟悉,用的时候查询,我学习的时候做的,附件在后面,需要一个叫MindMaster的思维导图工具打开。做完思维导图系统地熟悉一遍这个配置项后,再在下面的练习中,把每个配置项目的意思备注后面,加深印象。

    练习1:认一遍app部分,把说明注释在第一个例子的app配置后面:

"app": { //表示应用的全局配置信息,就像这样,后面的自己做......
  "bundleName": "com.example.myapplication",
  "vendor": "example", 
  "version": {
    "code": 1, 
    "name": "1.0"  
  },
  "apiVersion": { 
    "compatible": 3,  
    "target": 3 
  }
},

练习2:认一遍deviceConfig部分,把说明注释在后面:

"deviceConfig": {//表示应用在具体设备上的配置信息。
  "default": { 
    "process": "com.huawei.hiworld.example",
    "directLaunch": false,
    "supportBackup": false,
    "network": {
      "usesCleartext": true,
      "securityConfig": {
        "domainSettings": { 
          "cleartextPermitted": true, 
          "domains": [
            {
              "subDomains": true,
              "name": "example.ohos.com"
            }
          ]
        }
      }
    }
  }
}

练习3:认一遍认一下module部分,把说明注释在后面:

"module": {//表示HAP包的配置信息。该标签下的配置只对当前HAP包生效。
  "package": "com.example.myapplication", 
  "name": ".MyApplication",
  "reqCapabilities": [
    "video_support"  
  ],
  "deviceType": [
    "tv"
  ],
  "distro": {
    "deliveryWithInstall": true,
    "moduleName": "entry",
    "moduleType": "entry"
  },
  "abilities": [
    {
      "skills": [
        {
          "entities": [
            "entity.system.home"
          ],
          "actions": [
            "action.system.home"
          ]
        }
      ],
      "orientation": "landscape", 
      "formEnabled": false,
      "name": "com.example.myapplication.MainAbility",
      "icon": "$media:icon",
      "description": "$string:mainability_description",
      "label": "MyApplication",
      "type": "page",
      "launchType": "standard"
    },
    {
      "orientation": "landscape",
      "formEnabled": false,
      "name": "com.example.myapplication.Secondbility",
      "icon": "$media:icon",
      "description": "$string:secondbility_description",
      "label": "entry",
      "type": "page",
      "launchType": "standard"
    }
  ]
}

4.3 进一步了解资源目录的细节
   1. 资源文件的知识点
   应用的资源文件都在resources目录下,resources子目录分两大类目录:具体的知识点,同样我们做成思维导图,见后面附件。

   2. 资源文件的使用
   base目录与限定词目录中的资源文件:通过指定资源类型(type)和资源名称(name)来引用。
   1)Java文件引用资源文件的格式:
   (1)普通资源:ResourceTable.type_name。
   (2)系统资源,则采用:ohos.global.systemres.ResourceTable.type_name。
   (3)目前支持的系统资源文件:
      ① ic_app:表示HarmonyOS应用的默认图标,类型为媒体。
      ② request_location_reminder_title:表示“请求使用设备定位功能”的提示标题,类型为字符串。
      ③ request_location_reminder_content:表示“请求使用设备定位功能”的提示内容,类型为字符串。


   示例一:在Java文件中,引用系统资源。

ResourceManager rsManager = this.getResourceManager();
try {
    String str = rsManager
            .getElement(ohos.global.systemres.ResourceTable.
                    String_request_location_reminder_title).getString();
    Text text = (Text) findComponentById(ResourceTable.Id_text);
    text.setText(str);
} catch (Exception e) {
    terminateAbility();
}

    示例二:在Java文件中,引用string.json文件中类型为“String”、名称为“app_name”的资源。
    string.json的例子:

{
    "color":[
        {
            "name":"red",
            "value":"#ff0000"
        },
        {
            "name":"red_ref",
            "value":"$color:red"
        }
    ]
}

ohos.global.resource.ResourceManager resManager = getResourceManager();
int color = resManager.getElement(ResourceTable.Color_red).getColor();

    示例四:在Java文件中,获取profile中的文件内容。

Resource resource = null;
try {
    Text text = (Text) findComponentById(ResourceTable.Id_text);
    resource = getResourceManager().getResource(ResourceTable.Profile_test1);
    InputStreamReader inputStreamReader = new InputStreamReader(resource, "utf-8");
    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
    String lineTxt = "";
    while((lineTxt = bufferedReader.readLine()) != null){
        text.append(","+lineText);
    }
} catch (Exception e) {
    
}

   2)XML文件引用资源文件的格式:
   (1)普通资源,$type:name
   (2)系统资源,则采用:$ohos:type:name。

   在XML文件中,引用 string.json文件中类型为“String”、名称为“app_name”的资源,示例如下:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:width="match_parent"
    ohos:height="match_parent"
    ohos:orientation="vertical">
    <Text ohos:text="$string:app_name"/>
</DirectionalLayout>

    9. rawfile目录中的资源文件
    通过指定文件路径和文件名称来引用。
    在Java文件中,引用一个路径为“resources/rawfile/”、名称为“ttt.txt”的资源文件,示例如下:

Resource resource = null;
try {
    Text text = (Text) findComponentById(ResourceTable.Id_text);
    resource = getResourceManager().getRawFileEntry("resources/rawfile/ttt.txt").openRawFile();
    InputStreamReader inputStreamReader = new InputStreamReader(resource, "utf-8");
    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
    String lineTxt = "";
    while((lineTxt = bufferedReader.readLine()) != null){
        text.append(","+lineTxt );
    }
} catch (Exception e) {

}

五、日志HiLog的使用 

5.1 概述
做一个Java攻城师, 我们除了关心系统的架构这种high level的问题,还需要了解一些语言的陷阱, 异常的处理, 以及日志的输出, 这些"鸡毛蒜皮"的细节。
我们需要通过打印一条条日志来掌握程序运行的状态,下面我们就来讲解鸿蒙系统中的HiLog日志工具的具体使用方法。


5.2 HiLog使用说明
1. 使用HiLog前必须在HiLog的一个辅助类HiLogLabel中定义日志类型、服务域和标记。一般我们把它定义为常量放在类的最上面:

static final HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x00201, "MY_TAG"); 

上面有三个参数:
1)日志类型,我们的应用一般取一个常量值:HiLog.LOG_APP,表示是第三方应用。
2)服务域,16进制整数形式,取值范围是0x0 ~ 0xFFFFF。一般情况下,我们建议把这5个16进制数分成两组,前面三个数表示应用中的模块编号,后面两个表示模块中的类的编号。
3)一个字符串常量,它标识方法调用的类或服务行为。一般情况下就写类的名字,一般我可用这个标记对日志进行过滤。


2. 日志的级别,和其他日志一样,HiLog也分成了几个日志级别,由上到下信息越严重:
1)debug:调试信息
2)info:普通信息
3)warn:警告信息
4)error:错误信息
5)fatal:致命错误信息


3. 使用

String url = "www.baidu.com";
int errno = 0;
HiLog.warn(label, "Failed to visit %{private}s, reason:%{public}d.", url, errno);

按照上述格式就可用在控制台中输入日志信息了,里边还有点东西,需要进一步解释一下:
 %{private}s和%{public}d这两个符号我们可用理解为占位符,真正打印到控制台上的值是后面的变量:

鸿蒙应用开发入门(五):日志HiLog的使用-鸿蒙开发者社区

private:表示私有的,我们在开发阶段的日志中是看得见的,但是运行到手机上后,手机的控制台是隐藏的,看不见的。
public:表示共有的,哪里都看得见,不受限制。
s:表示字符串
d:表示数字

5.3. 写demo练习

public class MainAbility extends Ability {
    static final HiLogLabel HI_LOG_LABEL = new HiLogLabel(HiLog.LOG_APP,0x00101,"MainAbility");
    @Override
    public void onStart(Intent intent) {
        HiLog.info(HI_LOG_LABEL,"======MainAbility onStart is running....");
        String ss = "tesst string";
        int ii  = 11111;
        HiLog.info(HI_LOG_LABEL,"======字符串信息加上变量信息,格式化打印字符串%{public}s,整数%{private}d",ss,ii);
        super.onStart(intent);
        super.setMainRoute(MainAbilitySlice.class.getName());
    }
}

六、页面间跳转  

6.1 页面间跳转
1. 认识Intent
Intent是对象之间传递信息的载体。
例如,当一个Ability需要启动另一个Ability时,或者一个AbilitySlice需要导航到另一个AbilitySlice时,可以通过Intent指定启动的目标同时携带相关数据。Intent的构成元素包括Operation与Parameters:

鸿蒙应用开发入门(六):页面间跳转-鸿蒙开发者社区

2. 了解AbilitySlice路由配置
虽然一个Page可以包含多个AbilitySlice,但是Page进入前台时界面默认只展示一个AbilitySlice。默认展示的AbilitySlice是通过setMainRoute()方法来指定的。当有多个需要展示的AbilitySlice,可以通过addActionRoute()方法为MainAbilitySlice以外的AbilitySlice配置路由规则。此时,当其他Page实例期望导航到这些AbilitySlice时,可以通过AbilitySlice之间的跳转,显示出这张页面。

public class MyAbility extends Ability {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        // set the main route
        setMainRoute(MainSlice.class.getName());
 
        // set the action route
         addActionRoute("action.pay", PaySlice.class.getName());
        addActionRoute("action.scan", ScanSlice.class.getName());
    }
}

addActionRoute()方法中使用的动作命名,需要在应用配置文件(config.json)中注册:

{
    "module": {
        "abilities": [
            {
                "skills":[
                    {
                        "actions":[
                            "action.pay",
                            "action.scan"
                        ]
                    }
                ]
                ...
            }
        ]
        ...
    }
    ...
}

3. 同一个Page里的AbilitySlice1与AbilitySlice2间的跳转(无参,带参,回值)
1)无参数跳转

@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    super.setUIContent(ResourceTable.Layout_ability_main);

    Text text = (Text)findComponentById(ResourceTable.Id_text_helloworld);
    text.setClickedListener(component->{
        Intent intent1 = new Intent();
        present(new MainAbilitySlice1(),intent1);
    });
}

2)带参数跳转
(1)产生参数端的AbilitySlice

@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    super.setUIContent(ResourceTable.Layout_ability_main);

    Text text = (Text)findComponentById(ResourceTable.Id_text_helloworld);
    text.setClickedListener(component->{
        //有参数跳转
        Intent intent1 = new Intent();
        intent1.setParam("user","钟发发");
        present(new MainAbilitySlice1(),intent1);
    });
}

(2)接收参数端的AbilitySlice

public class MainAbilitySlice1 extends AbilitySlice {
    Text text;
    String oldText;
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main1);
        text = (Text) findComponentById(ResourceTable.Id_text_helloworld1);
        if(intent != null){
            String user = intent.getStringParam("user");
            oldText = text.getText();
            text.append("," + user);
        }
    }
    @Override
    protected void onInactive() {
        super.onInactive();
    }
....
}

3)带参数跳转+返回值
(1)参数产生端

public class MainAbilitySlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);

        Text text = (Text)findComponentById(ResourceTable.Id_text_helloworld);
        text.setClickedListener(component->{
            //有参数跳转
            Intent intent1 = new Intent();
            intent1.setParam("user","钟发发");
            presentForResult(new MainAbilitySlice1(),intent1,120);
        });
    }
...
}

(2)参数接收端

public class MainAbilitySlice1 extends AbilitySlice {
    Text text;
    String oldText;
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main1);
        text = (Text) findComponentById(ResourceTable.Id_text_helloworld1);
        if(intent != null){
            String user = intent.getStringParam("user");
            oldText = text.getText();
            text.setText(oldText + "," + user);
        }
        //参数接收端在对文字点击
        text.setClickedListener(component -> {
            //1.给跳转来的页面返回值
            Intent intent1 = new Intent();
            intent1.setParam("password","123456");
            setResult(intent1);
            //2.接收本AbilityAlice,自动返回上一页
            terminate();
        });
    }

    @Override
    protected void onInactive() {
        super.onInactive();
        text.setText(oldText);
    }
...
}

(3)回到参数产生端接收返回值

public class MainAbilitySlice extends AbilitySlice {
    Text text;
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);

        text = (Text)findComponentById(ResourceTable.Id_text_helloworld);
        text.setClickedListener(component->{
            //有参数跳转
            Intent intent1 = new Intent();
            intent1.setParam("user","钟发发");
            presentForResult(new MainAbilitySlice1(),intent1,120);
        });
    }

    @Override
    protected void onResult(int requestCode, Intent resultIntent) {
        super.onResult(requestCode, resultIntent);
        if(requestCode==120){
            String password = resultIntent.getStringParam("password");
            text.setText("返回值:" + password);
        }
    }
...
}

4. 不同的Page直接跳转,第一个鸿蒙应用例子写的就是这个,核心代码:

if (button != null) {
    // 为按钮设置点击回调
    button.setClickedListener(new Component.ClickedListener() {
        @Override
        public void onClick(Component component) {
            Intent secondIntent = new Intent();
            // 指定待启动FA的bundleName和abilityName
            Operation operation = new Intent.OperationBuilder()
                    .withDeviceId("")
                    .withBundleName("com.example.myapplication")
                    .withAbilityName("com.example.myapplication.SecondAbility")
                    .build();
            secondIntent.setOperation(operation);
            startAbility(secondIntent); // 通过AbilitySlice的startAbility接口实现启动另一个页面
        }
    });
}

5. Page1的MainAbilitySlice跳转Page2的AbilitySlice1

@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    super.setUIContent(ResourceTable.Layout_ability_main);

    text = (Text)findComponentById(ResourceTable.Id_text_helloworld);
    text.setClickedListener(component->{
        Intent intent1 = new Intent();
        intent1.setAction("abilityslice1");  //关键是配置文件里配置action和Ability里注册路由
        startAbility(intent1);
    });
}

七、实现跨设备迁移 

6.2 跨设备迁移
1. 分布式任务调度概述
在HarmonyOS中,分布式任务调度平台对搭载HarmonyOS的多设备构筑的“超级虚拟终端”提供统一的组件管理能力,为应用定义统一的能力基线、接口形式、数据结构、服务描述语言,屏蔽硬件差异;支持远程启动、远程调用、业务无缝迁移等分布式任务。


2. 实现调度的约束与限制
1)远程调用PA/FA,开发者需要在Intent中设置支持分布式的标记(例如:Intent.FLAG_ABILITYSLICE_MULTI_DEVICE表示该应用支持分布式调度),否则将无法获得分布式能力。


2)开发者通过在config.json中的reqPermissions字段里添加权限申请:
(1)以获取跨设备连接的能力和分布式数据传输的权限。
分布式数据传输的权限:
{"name": "ohos.permission.servicebus.ACCESS_SERVICE"}
三方应用使用权限:
{"name": "ohos.permission.servicebus.DISTRIBUTED_DATASYNC"}
系统应用使用权限:
{"name": "com.huawei.hwddmp.servicebus.BIND_SERVICE"}


(2)另外还有三个获取分布式设备信息需要的权限:
{"name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE"}, 
{"name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO" }, 
{ "name": "ohos.permission.GET_BUNDLE_INFO"}


注意:还需要在开发的时候,要在Ability里主动声明,要用到的权限。


3)FA(Feature Ability,Page模板的Ability)的调用支持启动和迁移行为,在进行调度时:
(1)当启动FA时,需要开发者在Intent中指定对端设备的deviceId、bundleName和abilityName。
(2)FA的迁移实现相同bundleName和abilityName的FA跨设备迁移,因此需要指定迁移设备的deviceId。


3. 实现场景介绍
下面以设备A(本地设备)和设备B(远端设备)为例,介绍下面我们要实现的场景:
1)设备A启动设备B的FA:在设备A上通过本地应用提供的启动按钮,启动设备B上对应的FA。
2)设备A的FA迁移至设备B:设备A上通过本地应用提供的迁移按钮,将设备A的业务无缝迁移到设备B中。
3)设备A的FA迁移至设备B,还可以实现主动撤回迁移。


4. 具体实现前先了解要用的接口
1)启动远程FA
startAbility(Intent intent)接口提供启动指定设备上FA和PA的能力,Intent中指定待启动FA的设备deviceId、bundleName和abilityName。
2)迁移FA
continueAbility(String deviceId)接口提供将本地FA迁移到指定设备上的能力,continueAbilityReversibly(String deviceId) 接口提供将本地FA迁移到指定设备上的能力,这种迁移可撤回, reverseContinueAbility()接口提供撤回迁移的能力。


5. 实战远程启动FA页面
1)编程实现上面场景的界面:
ability_main.xml

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:orientation="vertical">

    <Button
        ohos:id="$+id:migration_btn_01"
        ohos:height="match_content"
        ohos:width="300vp"
        ohos:text="1.启动远程设备的FA"
        ohos:text_size="20fp"
        ohos:text_color="#ffffff"
        ohos:background_element="$graphic:button_bg"
        ohos:layout_alignment="horizontal_center"
        ohos:top_padding="8vp"
        ohos:bottom_padding="8vp"
        ohos:left_padding="40vp"
        ohos:right_padding="40vp"
        ohos:top_margin="20vp"
        />

    <Button
        ohos:id="$+id:migration_btn_02"
        ohos:height="match_content"
        ohos:width="300vp"
        ohos:text="2.迁移到远程设备"
        ohos:text_size="20fp"
        ohos:text_color="#ffffff"
        ohos:background_element="$graphic:button_bg"
        ohos:layout_alignment="horizontal_center"
        ohos:top_padding="8vp"
        ohos:bottom_padding="8vp"
        ohos:left_padding="40vp"
        ohos:right_padding="40vp"
        ohos:top_margin="20vp"
        />


    <Button
        ohos:id="$+id:migration_btn_03"
        ohos:height="match_content"
        ohos:width="300vp"
        ohos:text="3.可迁回的迁移远程设备"
        ohos:text_size="20fp"
        ohos:text_color="#ffffff"
        ohos:background_element="$graphic:button_bg"
        ohos:layout_alignment="horizontal_center"
        ohos:top_padding="8vp"
        ohos:bottom_padding="8vp"
        ohos:left_padding="40vp"
        ohos:right_padding="40vp"
        ohos:top_margin="20vp"
        />
</DirectionalLayout>

button_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<shape  xmlns:ohos="http://schemas.huawei.com/res/ohos"
        ohos:shape="rectangle">
    <solid ohos:color="#007DFF"/>
    <corners ohos:radius="40"/>
</shape>

MigrationAbility和MigrationBackAbility

// 调用AbilitySlice模板实现一个用于控制基础功能的FA
// Ability和AbilitySlice类均需要实现IAbilityContinuation及其方法,才可以实现FA迁移。AbilitySlice的代码示例如下
public class SampleSlice extends AbilitySlice implements IAbilityContinuation {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
       
        super.setUIContent(layout);
    }

ability_migration.xml

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:background_element="#00ffff"
    ohos:orientation="vertical">

    <Text
        ohos:id="$+id:text_title"
        ohos:height="match_content"
        ohos:width="250vp"
        ohos:background_element="#0088bb"
        ohos:layout_alignment="horizontal_center"
        ohos:text="下面是一个可编辑的文本框"
        ohos:text_size="50"
        ohos:padding="5vp"
        ohos:top_margin="30vp"
        />

    <TextField
        ohos:id="$+id:textfield_back"
        ohos:height="250vp"
        ohos:width="250vp"
        ohos:hint="请输入..."
        ohos:layout_alignment="horizontal_center"
        ohos:background_element="#ffffff"
        ohos:text_color="#888888"
        ohos:text_size="20fp"
        ohos:padding="5vp"
        />
    <Button
        ohos:id="$+id:migration_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:text="点击迁移"
        ohos:text_size="20fp"
        ohos:text_color="#ffffff"
        ohos:background_element="$graphic:button_bg"
        ohos:top_padding="8vp"
        ohos:bottom_padding="8vp"
        ohos:left_padding="50vp"
        ohos:right_padding="50vp"
        ohos:layout_alignment="horizontal_center"
        ohos:top_margin="30vp"
        />
</DirectionalLayout>

ability_migration_back.xml比ability_migration.xml多一个迁回按钮,另外主页上点击按钮跳转等,略...

2)使用分布式能力要求开发者在Ability对应的config.json中声明多设备协同访问的权限:
(1)三方应用部署权限、分布式数据传输的权限、系统应用使用权限的申请。

{
    "reqPermissions": [
        {"name": "ohos.permission.DISTRIBUTED_DATASYNC"},
        {"name": "ohos.permission.servicebus.ACCESS_SERVICE"},
        {"name": "com.huawei.hwddmp.servicebus.BIND_SERVICE"}     
    ]
}

(2)声明分布式获取设备列表及设备信息的权限,如下所示:

{
    "reqPermissions": [
        {"name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE"}, 
        {"name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO" }, 
        {"name": "ohos.permission.GET_BUNDLE_INFO"}
    ]
}

(3)对于三方应用还要求在实现Ability的代码中显式声明需要使用的权限。

public class SampleSlice extends AbilitySlice implements IAbilityContinuation {
    @Override
    public void onStart(Intent intent) {
        // 开发者显示声明需要使用的权限
        requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC",
                                                "ohos.permission.servicebus.ACCESS_SERVICE",
                                                "com.huawei.hwddmp.servicebus.BIND_SERVICE"}, 0);
        super.onStart(intent);        
    }
}

3) 为启动远程FA的按钮添加点击事件,获取设备信息,实现启动远程FA的能力。

Button btn1 = (Button) findComponentById(ResourceTable.Id_migration_btn_01);
btn1.setClickedListener(new Component.ClickedListener() {
    @Override
    public void onClick(Component component) {
        // 调用DeviceManager的getDeviceList接口,通过FLAG_GET_ONLINE_DEVICE标记获得在线设备列表
        List<DeviceInfo> onlineDevices = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
        // 判断组网设备是否为空
        if (onlineDevices.isEmpty()) {
            return;
        }
        int numDevices = onlineDevices.size();

        ArrayList<String> deviceIds = new ArrayList<>(numDevices);
        ArrayList<String> deviceNames = new ArrayList<>(numDevices);
        onlineDevices.forEach((device) -> {
            deviceIds.add(device.getDeviceId());
            deviceNames.add(device.getDeviceName());
        });
        // 我们这里只有两个设备,所以选择首个设备作为目标设备
        // 开发者也可按照具体场景,通过别的方式进行设备选择
        String selectDeviceId = deviceIds.get(0);
        
        //获取设备ID,最好放到工具类里,很多地方要用!

        if(selectDeviceId!=null){
            Intent intent2 = new Intent();
            Operation operation = new Intent.OperationBuilder()
                    .withDeviceId(selectDeviceId)
                    .withBundleName("cn.ybzy.hmsdemo")
                    .withAbilityName("cn.ybzy.hmsdemo.RemoteAbility")
                    .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                    .build();
            intent2.setOperation(operation);
            // 通过AbilitySlice包含的startAbility接口实现跨设备启动FA
            startAbility(intent2);
        }
    }
});

6. 实战将设备A运行时的FA迁移到设备B,实现业务在设备间无缝迁移。
MigrationAbility

public class MigrationAbility extends Ability implements IAbilityContinuation  {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(MigrationAbilitySlice.class.getName());
    }

    @Override
    public boolean onStartContinuation() {
        return true;
    }

    @Override
    public boolean onSaveData(IntentParams intentParams) {
        return true;
    }

    @Override
    public boolean onRestoreData(IntentParams intentParams) {
        return true;
    }

    @Override
    public void onCompleteContinuation(int i) {

    }
}

MigrationAbilitySlice 

public class MigrationAbilitySlice extends AbilitySlice implements IAbilityContinuation {
    TextField textField;
    String textStr = "请输入数据...";
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_migration);
        textField = (TextField)findComponentById(ResourceTable.Id_textfield_migration);
        textField.setText(textStr);
        Button btn = (Button) findComponentById(ResourceTable.Id_migration_button);
        btn.setClickedListener(new Component.ClickedListener() {
            @Override
            public void onClick(Component component) {
                String deviceId = getDeviceId();
                if(deviceId!=null){
                    continueAbility(deviceId);
                }
            }
        });

    }

    private String getDeviceId(){
        // 调用DeviceManager的getDeviceList接口,通过FLAG_GET_ONLINE_DEVICE标记获得在线设备列表
        List<DeviceInfo> onlineDevices = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
        // 判断组网设备是否为空
        if (onlineDevices.isEmpty()) {
            return null;
        }
        int numDevices = onlineDevices.size();

        ArrayList<String> deviceIds = new ArrayList<>(numDevices);
        ArrayList<String> deviceNames = new ArrayList<>(numDevices);
        onlineDevices.forEach((device) -> {
            deviceIds.add(device.getDeviceId());
            deviceNames.add(device.getDeviceName());
        });
        // 我们这里只有两个设备,所以选择首个设备作为目标设备
        // 开发者也可按照具体场景,通过别的方式进行设备选择
        String selectDeviceId = deviceIds.get(0);
        return selectDeviceId;
    }

    @Override
    public boolean onStartContinuation() {
        return true;
    }

    @Override
    public boolean onSaveData(IntentParams intentParams) {
        intentParams.setParam("data",textField.getText());
        return true;
    }

    @Override
    public boolean onRestoreData(IntentParams intentParams) {
        textStr = intentParams.getParam("data").toString();
        return true;
    }

    @Override
    public void onCompleteContinuation(int i) {

    }

    @Override
    public void onRemoteTerminated() {

    }
}

此外,不同于启动行为,FA的迁移还涉及到状态数据的传递。为此,继承的IAbilityContinuation接口为开发者提供迁移过程中特定事件的管理能力。通过自定义迁移事件相关的行为,最终实现对Ability的迁移。主要以较为常用的两个事件,包括迁移发起端完成迁移的回调onCompleteContinuation(int result)以及接收到远端迁移行为传递数据的回调onRestoreData(IntentParams restoreData)。其他还包括迁移到远端设备的FA关闭的回调onRemoteTerminated()、用于本地迁移发起时保存状态数据的回调onSaveData(IntentParams saveData)和本地发起迁移的回调onStartContinuation()。

7. 请求回迁

Button btn1 = (Button) findComponentById(ResourceTable.Id_migration_button_back);
btn1.setClickedListener(new Component.ClickedListener() {
    @Override
    public void onClick(Component component) {
        String deviceId = DeviceUtils.getDeviceId();
        if(deviceId!=null){
            continueAbilityReversibly(deviceId);  //可撤回迁移
        }
    }
});

Button btn2 = (Button) findComponentById(ResourceTable.Id_migration_button_back2);
btn2.setClickedListener(new Component.ClickedListener() {
    @Override
    public void onClick(Component component) {
        reverseContinueAbility();  //撤回迁移
    }
});

1)设备A上的Page请求回迁。
2)系统回调设备B上Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onStartContinuation()方法,以确认当前是否可以立即迁移。
3)如果可以立即迁移,则系统回调设备B上Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onSaveData()方法,以便保存回迁后恢复状态必须的数据。
4)如果保存数据成功,则系统在设备A上Page恢复AbilitySlice栈,然后回调IAbilityContinuation.onRestoreData()方法,传递此前保存的数据。
5)如果数据恢复成功,则系统终止设备B上Page的生命周期。

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值