鸿蒙HarmonyOS应用开发初体验

近期(4.12 ~ 4.25)鸿蒙OS正在举行开发者日活动,趁机参加并了解一下鸿蒙OS的现状和应用开发体验。

  1. 开发环境搭建
下载安装IDE(当前版本 2.1 Beta3)
华为为Harmony应用开发提供了配套的IDE:DevEco Studio(内心比较排斥这种带Eco字眼儿的命名,PPT怎么吹无所谓,开发工具咱能不能务实一点儿?)
下载IDE需要登录Huawei账号,我安装的是Mac版,下载后的安装过程还是比较顺畅的
image.png
启动界面显示DevEco Studio仍然是基于IntelliJ的定制IDE
下载SDK
跟Android一样,IDE启动第一件事情是下载Harmony SDK
image.png
每个版本的SDK中都提供了三套API用来开发Java、Js、C++代码,版本上需要保持一致。 不同的华为设备对SDK版本有不同要求,比如在测试中发现,我的API4的代码无法运行在P40上,改为API5就OK了
关于SDK源码
需要注意,目前无法通过SDKManager打包下载源码,源码需要通过gitee单独下载
【鸿蒙HarmonyOS应用开发初体验】gitee.com/openharmony
这为代码调试带来障碍,不知道后期是否可以像Andoird那样与SDK一起打包下载源码
创建项目
Harmony主打多端协同,所以很重视设备多样性,可面向不同设备创建模板项目
Screen Shot 2021-04-18 at 10.28.09 AM.png
相比AndroidStudio,Harmony提供了更加丰富的项目模板,模板中除了UI以外还提供了部分数据层代码,基本上是一个可以二次开发的APP。
Screen Shot 2021-04-18 at 5.48.55 PM.png
  1. 鸿蒙项目结构
IDE界面
试着创建了一个News Feature Ability(新闻流)的模板项目,成功在IDE中打开:
image.png
IDE窗口与AndroidStudio类似,值得一提的Harmony右边提供的Preview窗口,可以对xml或者Ablitiy文件进行预览,有点Compose的Preview的感觉,但是只能静态预览,无法交互
工程文件
image.png
工程文件和Android类似,甚至可以找到一一对应的关系
Harmony Android 说明
entry app 默认启动模块(主模块),相当于app_module
MyApplication XXXApplication 鸿蒙的MyApplication是AbilityPackage的子类
MainAbility MainActivity 入口页。鸿蒙中将四大组件的概念统一成Ability
MainAbilityListSlice XXXFragment Slice类似Fragment,UI的基本组成单元
Component View Component类相当于View,后文介绍
config.json AndroidManifest.xml 鸿蒙使用json替代xml进行Manifest配置,配置项目差不多
resources/base/... res/... 包括Layout文件在内的各种资源文件依旧使用xml
resources/rawfile/ assets/ rawfile存储任意格式原始资源,相当于assets
build.gradle build.gradle 编译脚本,两者一样
build/outpus/.../.hap build/outputs/.../.apk 鸿蒙的产物是hap(harmony application package)
解压后里面有一个同名的.apk文件,
这后续是因为鸿蒙需要同时支持apk安装的兼容方案
Ability
Ability是应用所具备能力的抽象,Harmony支持应用以Ability为单位进行部署。一个应用由一个或多个FA(Feature Ability)或PA(Particle Ability)组成。FA有UI界面,提供与用户交互的能力;而PA无UI界面,提供后台运行任务的能力以及统一的数据访问抽象
FA支持Page Ability:
Page Ability用于提供与用户交互的能力。一个Page可以由一个或多个AbilitySlice构成,AbilitySlice之间可以进行页面导航
image.png
PA支持Service Ability和Data Ability:
Service Ability:用于提供后台运行任务的能力。
Data Ability:用于对外部提供统一的数据访问抽象。
可以感觉到,各种Ability可以对照Android的四大组件来理解
Harmony Android
Page Ability (FA) Activity
Service Ability (PA) Service
Data Ability(PA) ContentProvider
AbilitySlice Fragment
代码一览
MainAbility
以预置的News Feature Ability为例子,这是一个拥有两个Slice的Page Ability,通过Router注册两个Slice
public class MainAbility extends Ability { @Override public void onStart(Intent intent) { super.onStart(intent); super.setMainRoute(MainAbilityListSlice.class.getName()); //添加路由:ListSlice addActionRoute("action.detail", MainAbilityDetailSlice.class.getName()); //DetailSlice ... } } 复制代码1.2.3.4.5.6.7.8.9.10.11.12.
1.
以下是在模拟器中运行两个Slice的页面效果
MainAbilityListSlice MainAbilityDetailSlice
image.png image.png
MainAbilityListSlice
主要看一下列表的显示逻辑
public class MainAbilityListSlice extends AbilitySlice { ... @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_news_list_layout); initView(); initData(); //加载数据 initListener(); newsListContainer.setItemProvider(newsListAdapter); //Adatper设置到View newsListAdapter.notifyDataChanged(); //刷新数据 } private void initListener() { newsListContainer.setItemClickedListener((listContainer, component, i, l) -> { //路由跳转"action.detail" LogUtil.info(TAG, "onItemClicked is called"); Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withBundleName(getBundleName()) .withAbilityName("com.example.myapplication.MainAbility") .withAction("action.detail") .build(); intent.setOperation(operation); startAbility(intent); }); } private void initData() { ... totalNewsDatas = new ArrayList<>(); newsDatas = new ArrayList<>(); initNewsData(); //填充newsDatas newsListAdapter = new NewsListAdapter(newsDatas, this); //设置到Adapter } ... } 复制代码1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.
1.
类似ListView的用法,通过Adatper加载数据; setItemClickedListener中通过路由跳转MainAbilityDetailSlice。
Layout_news_list_layout布局文件定义如下,ListContainer即ListView,是Comopnent的一个子类,Component就是HarmonyOS中的View
复制代码1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.
1.
看一下Adapter的实现, 继承自BaseItemProvider
/ News list adapter / public class NewsListAdapter extends BaseItemProvider { private List newsInfoList; private Context context; public NewsListAdapter(List listBasicInfo, Context context) { this.newsInfoList = listBasicInfo; this.context = context; } @Override public int getCount() { return newsInfoList == null ? 0 : newsInfoList.size(); } @Override public Object getItem(int position) { return Optional.of(this.newsInfoList.get(position)); } @Override public long getItemId(int position) { return position; } @Override public Component getComponent(int position, Component componentP, ComponentContainer componentContainer) { ViewHolder viewHolder = null; Component component = componentP; if (component == null) { component = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_news_layout, null, false); viewHolder = new ViewHolder(); Component componentTitle = component.findComponentById(ResourceTable.Id_item_news_title); Component componentImage = component.findComponentById(ResourceTable.Id_item_news_image); if (componentTitle instanceof Text) { viewHolder.title = (Text) componentTitle; } if (componentImage instanceof Image) { viewHolder.image = (Image) componentImage; } component.setTag(viewHolder); } else { if (component.getTag() instanceof ViewHolder) { viewHolder = (ViewHolder) component.getTag(); } } if (null != viewHolder) { viewHolder.title.setText(newsInfoList.get(position).getTitle()); viewHolder.image.setScaleMode(Image.ScaleMode.STRETCH); } return component; } / ViewHolder which has title and image / private static class ViewHolder { Text title; Image image; } } 复制代码1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.
1.
基本上就是标准的ListAdatper,把View替换成Component而已。
关于模拟器
代码完成后可以再模拟器中运行。关于模拟器有几点想说的:
Harmony的模拟器启动非常快,无需下载镜像,因为这个模拟器并非本地运行,而只是一个远端设备的VNC,因此必须在线使用,而且不够流畅时有丢帧现象。虽然真机调试效果更好,但不是人人都买得起P40的
模拟器嵌入到IDE窗口显示(像Preview窗口一样),非独立窗口,这会带来一个问题,当同时打开多个IDE时,模拟器可能会显示在另一个IDE中(就像Logcat跑偏一样)。
想使用模拟器必须进过开发者认证,官方推荐使用银行卡认证。模拟器远端链接的是一台真实设备,难道是为未来租用设备要计费??image.png记得以前看过一篇文章,如果是来自国外地区的注册账号可以免认证使用模拟器,但是懒得折腾了
  1. 开发JS应用
除了Java,鸿蒙还支持基于JS开发应用,借助前端技术完善其跨平台能力。
鸿蒙为JS工程提供了多种常用UI组件,但是没有采用当下主流的react、vue那样JS组件,仍然是基于CSS3/HTML5/JS这种传统方式进行开发。JS工程结构如下
image.png
目录 说明
common 可选,用于存放公共资源文件,如媒体资源、自定义组件和JS文档等
i18n 可选,用于存放多语言的json文件
pages/index/index.hml hml文件定义了页面的布局结构,使用到的组件,以及这些组件的层级关系
pages/index/index.css css文件定义了页面的样式与布局,包含样式选择器和各种样式属性等
pages/index/index.js js文件描述了页面的行为逻辑,此文件里定义了页面里所用到的所有的逻辑关系,比如数据、事件等
resources 可选,用于存放资源配置文件,比如:游戏全局样式、多分辨率加载等配置文件
app.js 全局的JavaScript逻辑文件和应用的生命周期管理。
  1. 跨设备迁移
通过前面的介绍,可能感觉和Android大同小异,但是HarmonyOS最牛逼之处是多端协作能力,例如可以将Page在同一用户的不同设备间迁移,实现无缝切换。
以Page从设备A迁移到设备B为例,迁移动作主要步骤如下:
设备A上的Page请求迁移。
HarmonyOS回调设备A上Page的保存数据方法,用于保存迁移必须的数据。
HarmonyOS在设备B上启动同一个Page,并回调其恢复数据方法。
通过调用continueAbility()请求迁移。如下,获取设备列表,配对成功后请求迁移
doConnectImg.setClickedListener( clickedView -> { // 通过FLAG_GET_ONLINE_DEVICE标记获得在线设备列表 List deviceInfoList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE); if (deviceInfoList.size() < 1) { WidgetHelper.showTips(this, "无在网设备"); } else { DeviceSelectDialog dialog = new DeviceSelectDialog(this); // 点击后迁移到指定设备 dialog.setListener( deviceInfo -> { LogUtil.debug(TAG, deviceInfo.getDeviceName()); LogUtil.info(TAG, "continue button click"); try { // 开始任务迁移 continueAbility(); LogUtil.info(TAG, "continue button click end"); } catch (IllegalStateException | UnsupportedOperationException e) { WidgetHelper.showTips(this, ResourceTable.String_tips_mail_continue_failed); } dialog.hide(); }); dialog.show(); } }); 复制代码1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.
1.
Page迁移涉及到数据传递,此时需要借助IAbilityContinuation进行通信。
跨设备通信 IAbilityContinuation
跨设备迁移的Page需要实现IAbilityContinuation接口。
Note: 一个应用可能包含多个Page,仅需要在支持迁移的Page中通过以下方法实现IAbilityContinuation接口。同时,此Page所包含的所有AbilitySlice也需要实现此接口。
public class MainAbility extends Ability implements IAbilityContinuation { ... @Override public void onCompleteContinuation(int code) {} @Override public boolean onRestoreData(IntentParams params) { return true; } @Override public boolean onSaveData(IntentParams params) { return true; } @Override public boolean onStartContinuation() { return true; } } public class MailEditSlice extends AbilitySlice implements IAbilityContinuation { ... @Override public boolean onStartContinuation() { LogUtil.info(TAG, "is start continue"); return true; } @Override public boolean onSaveData(IntentParams params) { ... LogUtil.info(TAG, "begin onSaveData:" + mailData); ... LogUtil.info(TAG, "end onSaveData"); return true; } @Override public boolean onRestoreData(IntentParams params) { LogUtil.info(TAG, "begin onRestoreData"); ... LogUtil.info(TAG, "end onRestoreData, mail data: " + cachedMailData); return true; } @Override public void onCompleteContinuation(int i) { LogUtil.info(TAG, "onCompleteContinuation"); terminateAbility(); } } 复制代码1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.
1.
onStartContinuation(): Page请求迁移后,系统首先回调此方法,开发者可以在此回调中决策当前是否可以执行迁移,比如,弹框让用户确认是否开始迁移。
onSaveData(): 如果onStartContinuation()返回true,则系统回调此方法,开发者在此回调中保存必须传递到另外设备上以便恢复Page状态的数据。
onRestoreData(): 源侧设备上Page完成保存数据后,系统在目标侧设备上回调此方法,开发者在此回调中接受用于恢复Page状态的数据。注意,在目标侧设备上的Page会重新启动其生命周期,无论其启动模式如何配置。且系统回调此方法的时机在onStart()之前。
onCompleteContinuation(): 目标侧设备上恢复数据一旦完成,系统就会在源侧设备上回调Page的此方法,以便通知应用迁移流程已结束。开发者可以在此检查迁移结果是否成功,并在此处理迁移结束的动作,例如,应用可以在迁移完成后终止自身生命周期。
以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()方法,通知数据恢复成功与否。
  1. 总结和感想
从SDK到IDE与Android都高度相似,任何Android开发者基本上就是一个准鸿蒙程序员
AndroidStudio的功能迭代很快,DevEco Studio在功能上还存在较大差距
需要实名认证开发者之后才能使用IDE的各种完整功能,内心抗拒
源码需要另外下载,对调试不友好
当前还不支持Kotlin。大势所趋,所以未来一定会支持,而且Kotlin是开源的问题不大
JS UI框架的技术架构同样有些过时
杀手锏是对多端协作的支持,但这可能需要更多的厂商加入才能真正发挥威力
目前人们对于鸿蒙的态度呈现两极化,有的人追捧有的人贬低,我觉得都大可不必,多给鸿蒙一些空间和耐心,静观其变、乐见其成。当然首选需要华为做到自己不主动炒作,真正静下心来打磨鸿蒙,只要华为有决心有耐心,作为开发者的我们为什么不支持呢?

    推荐阅读