本篇Codelab使用ArkTS语言实现了应用的一次开发、多端部署,效果预览如下:

分布式新闻客户端(ArkTS)应用包含两个页面:主页面和详情页面。两个页面都展示了丰富的组件,其中详情页还展示了跨设备拉起FA的功能。通过本篇Codelab我们将会一起完成这个应用,实现以下功能:

页面布局和展示效果如下图所示:
分布式拉起效果如下图所示:

我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。

开发者可以通过如下设备完成Codelab:

在本篇Codelab中我们只对核心代码进行讲解,您可以在后面12 参考章节中下载完整代码,首先来介绍下整个工程的代码结构:

  1. 新建model文件夹,在model目录下创建NewsData.ets和NewsDataModel.ets。
  2. 在NewsData.ets中定义新闻数据的存储模型NewsData。
    export class NewsData { newsId: string; ... // 构造方法,用于初始化新闻对象 constructor(newsId: string, title: string, newsType: string, imgUrl: Resource, reads: string, likes: string, content: string) { this.newsId = newsId; ... } }
  3. 在rawfile目录下存入新闻图片资源。
  4. 在NewsDataModel.ets中定义getNewsDatas方法用于初始化NewsData的数组,定义getDefaultData方法用于获取默认新闻条目数据。
    // 新闻数据源 const NewsComposition: any[] = [ { "newsId": "1", "title": "Best Enterprise Wi-Fi Network Award of the Wireless Broadband Alliance 2020", ... }, ... ] // 用于初始化NewsData的数组 export function getNewsDatas(): Array<NewsData> { ... } // 获取默认新闻(第一条新闻数据) export function getDefaultData(): NewsData { ... }

本节我们将完成主页面的开发,效果图如下:

从上面效果图可以看出,主页面主要由两部分构成:顶部的新闻分类页签和下方的新闻列表。我们可以使用Tabs和TabContent组件来实现。

  1. 将index.ets重命名为NewsMain.ets,新建NewsMain组件作为页面入口组件,NewsTypeComponent为其子组件。
    @Entry @Component struct NewsMain { build() { Row() { NewsTypeComponent() } } }
  2. 引入NewsData类和getNewsDatas方法。
    import {NewsData} from '../model/NewsData' import {getNewsDatas} from '../model/NewsDataModel'
  3. 创建NewsTypeComponent组件,该组件中使用了Tabs和TabContent组件。在NewsTypeComponent组件内创建类型为NewsData[]成员变量newsItems,调用getNewsDatas方法为其赋值。通过newsType 过滤后,将不同类型的新闻数据传递给子组件NewsListPage。
    @Component struct NewsTypeComponent { private newsItems: NewsData[] = getNewsDatas() build() { Tabs() { TabContent() { NewsListPage({ newsItems: this.newsItems }) }.tabBar('All') ... } }
  4. 创建NewsListItem组件,创建类型为NewsData成员变量newsItem,用于获取父组件传递过来的数据。
    @Component struct NewsListItem { private newsItem: NewsData build() { ... } }
  5. 创建NewsListPage组件,在其中添加List组件。在NewsListPage组件内创建类型为NewsData[]成员变量newsItems,用于获取父组件传递过来的数据。通过ForEach循环渲染ListItem组件,在ListItem中添加NewsListItem组件。
    @Component struct NewsListPage { private newsItems: NewsData[] build() { Column() { List() { ForEach(this.newsItems, item => { ListItem() { NewsListItem({ newsItem: item }) } }, item => item.newsId.toString()) } } } }

本节我们将完成新闻详情页面的开发,效果图如下:

根据以上效果图,我们可以将详情页分为两个部分:新闻信息和底部操作栏。新闻信息包含新闻标题、阅读量和喜好数、新闻图片、新闻内容;底部状态栏包含1个文本框和4个功能按键。详情页实现步骤如下:

  1. 在pages目录下新建页面NewsDetail.ets,并将其添加到config.json文件下的pages标签。
    "pages": [ "pages/NewsMain", "pages/page/NewsDetail" ]
  2. 在NewsDetail.ets中创建NewsDetail组件,在Column组件中添加新闻信息组件NewsInfo和底部操作栏组件NewsBottom。用Scroll组件包裹NewsInfo组件,这样当新闻内容文本较多时页面可以滚动。

    @Component export struct NewsDetail{ build() { Column() { Scroll() { NewsInfo() } .flexGrow(1).width("100%") NewsBottom() }.height("100%").width("100%") } }

实现新闻列表页面和详情页面的布局完成以后,本节将实现页面跳转的功能。

  1. 使用router路由API接口,需要先引入router。
    import router from '@system.router';
  2. 创建NewsInfo组件,创建类型为NewsData成员变量newsItem,用于展示新闻数据。
    @Component struct NewsInfo { private newsItem: NewsData; build() { Column() { ... } }
  3. 创建NewsBottom组件。
    @Component struct NewsBottom { build() { ... } }
  4. 在NewsMain.ets的NewsListItem绑定点击事件,设置要跳转的页面的uri地址以及携带的参数newsItem。
    @Component struct NewsListItem { private newsItem: NewsData build() { Column() { ... } .height(101) .width("100%") .onClick(() => { router.push( { uri: 'pages/NewsDetail', params: { newsItem: this.newsItem } }) }) } }
  5. 在NewsDetail.ets的NewsInfo中,调用router.getParams().newsItem来获取到NewsMain页面跳转来时携带的newsItem对应的数据。NewsDetail.ets中也需要引入router。
    @Component struct NewsInfo { private newsItem: NewsData = router.getParams().newsItem; build() { ... } }

点击详情页底部操作栏最右侧的分享按钮,弹出当前组网下可供选择的设备列表。此时需要调用JS API设备管理接口的getTrustedDeviceListSync方法,获取网络中的设备信息列表。

  1. config.json文件中的"reqPermissions"字段中声明所需要的权限。
    "module": { ... "reqPermissions": [ { "name": "ohos.permission.DISTRIBUTED_DATASYNC" }, { "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO" } ] }
  2. 在MainAbility动态申请分布式权限。
    public class MainAbility extends AceAbility { @Override public void onStart(Intent intent) { super.onStart(intent); requestPermissions(); } private void requestPermissions() { if (verifySelfPermission(SystemPermission.DISTRIBUTED_DATASYNC) != 0) { if (canRequestPermission(SystemPermission.DISTRIBUTED_DATASYNC)) { requestPermissionsFromUser(new String[] {SystemPermission.DISTRIBUTED_DATASYNC}, 0); } } } }
  3. 导入设备管理模块。
    import deviceManager from '@ohos.distributedHardware.deviceManager';
  4. 构建设备列表弹窗组件,设备列表弹窗组件使用到了自定义弹窗
    @CustomDialog @Component export struct DeviceListDialog { // 获取新闻条目数据 @Consume("newsItem") newsData: NewsData; ... } }
  5. 在NewsDetail.ets中给CustomDialogController 对象绑定DeviceListDialog 对象,给分享图标组件绑定点击事件。当点击分享图标时调用getDeviceList方法获取设备列表数据,同时会调用dialogController.open()方法显示弹窗。
    import {NewsData} from '../model/NewsData' import deviceManager from '@ohos.distributedHardware.deviceManager'; // 引入DeviceListDialog组件 import {DeviceListDialog} from '../component/DeviceListDialog' ... @Component struct NewsBottom { @Consume("newsItem") newsData: NewsData; @Provide deviceList: any[]= []; private deviceMag; dialogController: CustomDialogController = new CustomDialogController({ builder: DeviceListDialog(), autoCancel: true, alignment: DialogAlignment.Bottom }); // 释放DeviceManager实例 aboutToDisappear() { this.deviceMag.release(); } // 获取设备列表 getDeviceList() { ... deviceManager.createDeviceManager("com.huawei.codelab", (err, data) => { if (err) { console.error("createDeviceManager failed err: " + JSON.stringify(err)); return; } console.info('createDeviceManager successful. Data: ' + JSON.stringify(data)) this.deviceMag = data; this.deviceList = this.deviceMag.getTrustedDeviceListSync(); }); this.dialogController.open() } build() { ... Image($rawfile('icon_share.png')).width(25).height(21).margin({ right: 10 }) .onClick(() => { this.getDeviceList() }) }

    设备列表弹窗组件效果图如下:

在上一章节,我们获取到了设备deviceId,本章节我们将实现在新闻详情页分布式拉起远程FA。

  1. 在model目录下创建Utils.ets文件中,定义startRemoteAbilities方法用于拉起远程FA。
    import featureAbility from '@ohos.ability.featureAbility'; import wantConstant from '@ohos.ability.wantConstant'; export function startRemoteAbilities(deviceIds, newsId) { for (var i = 0; i < deviceIds.length; i++) { var want = { "want": { // 远程设备的deviceId "deviceId": deviceIds[i], "bundleName": "com.huawei.codelab", "abilityName": "com.huawei.codelab.MainAbility", // 分布式任务flag "flags": wantConstant.Flags.FLAG_ABILITYSLICE_MULTI_DEVICE, "parameters": { // 指定跳转的页面 "url": 'pages/NewsDetail', // 跳转携带的参数 "newsId": newsId }, } }; featureAbility.startAbility(want, (err, data) => { ... }); } }
  2. 在DeviceListDialog.ets文件中,给"确定"按钮设置点击事件调用startRemoteAbilities方法。
    Button("确定") ... .onClick(() => { // 关闭弹窗 this.controller.close(); startRemoteAbilities(this.selectedDevices, this.newsData.newsId) })
  3. 在NewsDetail.ets中获取远程FA传递过来的数据newsId,根据newId过滤对应的新闻信息。因为数据也有可能是从本地新闻列表页传递过来,所以我们做了如下判断:
    @Entry @Component struct NewsDetail { // 使用@Provide注解,方便把数据传递给其子孙组件。 @Provide newsData: NewsData= getDefaultData() private newsItems: NewsData[] = getNewsDatas() aboutToAppear() { if (router.getParams()) { // 从本机获取数据 this.newsData = router.getParams().newsItem } else { // 从远端获取数据 featureAbility.getWant() .then((want) => { console.info('getWant successful. Data: ' + JSON.stringify(want)); // 获取从远端 let newsId = want.parameters.newsId // 通过newsId获取新闻信息 this.newsData = this.newsItems.filter(item => (item.newsId === newsId))[0] }).catch((error) => { console.error('getWant failed. Cause: ' + JSON.stringify(error)); }) } } ... }

设备列表支持多选,可以拉起多台设备,最终分布式拉起效果如下图所示:

本篇Codelab使用方舟开发框架适配多种不同的屏幕形态,以达到一次开发,多端部署的目的。
工程部署在平板上,横屏状态下主界面分为左侧新闻列表和右侧新闻详情两部分。效果图如下所示:

上面效果图有新闻详情信息,和6 构建新闻详情页章节我们创建的新闻详情页内容是一样的,不同的是前面的详情页是一个单独的页面。这时候我们可以将详情页布局单独抽取为一个组件,在平板上复用。

  1. 在component目录下新建DetailComponent.ets,用来抽取NewsDetail.ets中的内容。
    ... @Component export struct DetailComponent { build() { Column() { Scroll() { NewsInfo() } .flexGrow(1).width("100%") NewsBottom() }.height("100%").width("100%") } ... }
  2. 在NewsMain.ets中使用媒体查询API,当屏幕宽度大于1500并且为横屏时,左侧显示新闻列表右侧显示新闻详情左侧和右侧的宽度比例为2:3。
    ... @Entry @Component struct NewsMain { @Provide("landscape") isLandscape: boolean = false; private newsItems: NewsData[] = getNewsDatas() // 横屏时,详情页默认展示第一条数据 @Provide("newsItem") newsData: NewsData= this.newsItems[0]; // 监听设备横竖屏切换 orientationListener = mediaquery.matchMediaSync('screen and (1500 < width) and (orientation: landscape)') build() { Row() { Column() { NewsTypeComponent({ newsItems: this.newsItems }) } .layoutWeight(2) if (this.isLandscape) { Column() { DetailComponent() } .layoutWeight(3) } } } onPortrait(mediaQueryResult) { console.log("isLandscape:" + mediaQueryResult.matches); this.isLandscape = mediaQueryResult.matches; } ... } ...

目前你已经成功完成了Codelab并且学到了:

希望通过本教程,各位开发者可以对基于TS扩展的声明式开发范式的方舟开发框架有更深的理解。

gitee:

github:
Code copied