分布式新聞客戶端(ArkTS)
介紹
本篇Codelab基于柵格布局、設(shè)備管理和多端協(xié)同,實(shí)現(xiàn)一次開發(fā),多端部署的分布式新聞客戶端頁面。主要包含以下功能:
- 展示新聞列表以及左右滑動(dòng)切換新聞Tab。
- 點(diǎn)擊新聞?wù)故拘侣勗斍轫摗?/li>
- 點(diǎn)擊新聞詳情頁底部的分享按鈕,發(fā)現(xiàn)周邊處在同一無線網(wǎng)絡(luò)下的設(shè)備并進(jìn)行可信認(rèn)證連接。
- 可信認(rèn)證后,再次點(diǎn)擊分享按鈕,選擇已連接的設(shè)備進(jìn)行跨設(shè)備啟動(dòng)UIAbility。
最終效果圖如下:
相關(guān)概念
- 柵格布局:一種通用的輔助定位工具,解決多尺寸多設(shè)備的動(dòng)態(tài)布局問題。
- 設(shè)備管理:模塊提供分布式設(shè)備管理能力。
- 跨設(shè)備啟動(dòng)UIAbility:多端上的不同UIAbility/ServiceExtensionAbility同時(shí)運(yùn)行、或者交替運(yùn)行實(shí)現(xiàn)完整的業(yè)務(wù)。
- Tabs組件:通過頁簽進(jìn)行內(nèi)容視圖切換的容器組件,每個(gè)頁簽對(duì)應(yīng)一個(gè)內(nèi)容視圖。
相關(guān)權(quán)限
本篇Codelab使用了設(shè)備管理及跨設(shè)備實(shí)現(xiàn)多端協(xié)同能力,需要手動(dòng)替換full-SDK,并在配置文件module.json5文件requestPermissions屬性中添加如下權(quán)限:
- 分布式設(shè)備認(rèn)證組網(wǎng)權(quán)限:ohos.permission.ACCESS_SERVICE_DM。
- 設(shè)備間的數(shù)據(jù)交換權(quán)限:ohos.permission.DISTRIBUTED_DATASYNC。
約束與限制
- 本篇Codelab部分能力依賴于系統(tǒng)API,需下載full-SDK并替換DevEco Studio自動(dòng)下載的public-SDK。具體操作可參考指南《如何替換full-SDK》。
- 本篇Codelab使用的部分API僅系統(tǒng)應(yīng)用可用,需要提升應(yīng)用等級(jí)。具體可參考指南《訪問控制授權(quán)申請(qǐng)指導(dǎo)》。
環(huán)境搭建
軟件要求
- DevEco Studio版本:DevEco Studio 4.0 Beta2。
- OpenHarmony SDK版本:API version 10。
硬件要求
- 開發(fā)板類型:潤和RK3568開發(fā)板。
- OpenHarmony系統(tǒng):4.0 Beta1。
環(huán)境搭建
完成本篇Codelab我們首先要完成開發(fā)環(huán)境的搭建,本示例以RK3568開發(fā)板為例,參照以下步驟進(jìn)行:
- 獲取OpenHarmony系統(tǒng)版本:標(biāo)準(zhǔn)系統(tǒng)解決方案(二進(jìn)制)。以4.0 Beta1版本為例:
- 搭建燒錄環(huán)境。
- 完成DevEco Device Tool的安裝
- 完成RK3568開發(fā)板的燒錄
- 搭建開發(fā)環(huán)境。
- 開始前請(qǐng)參考工具準(zhǔn)備,完成DevEco Studio的安裝和開發(fā)環(huán)境配置。
- 開發(fā)環(huán)境配置完成后,請(qǐng)參考使用工程向?qū)?chuàng)建工程(模板選擇“Empty Ability”)。
- 工程創(chuàng)建完成后,選擇使用真機(jī)進(jìn)行調(diào)測。
代碼結(jié)構(gòu)解讀
本篇Codelab只對(duì)核心代碼進(jìn)行講解。
├──entry/src/main/ets // 代碼區(qū)
│ ├──common
│ │ ├──constants
│ │ │ └──CommonConstants.ets // 常量類
│ │ └──utils
│ │ └──Logger.ets // 日志工具類
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口類
│ ├──model
│ │ └──RemoteDeviceModel.ets // 設(shè)備管理類
│ ├──pages
│ │ ├──Index.ets // 新聞列表頁
│ │ └──NewsDetail.ets // 新聞詳情頁
│ ├──view
│ │ ├──DetailFooter.ets // 詳情頁頁腳
│ │ ├──DetailHeadContent.ets // 新聞詳情
│ │ ├──DeviceListDialog.ets // 設(shè)備列表彈窗
│ │ ├──NewsList.ets // 新聞列表
│ │ └──NewsTab.ets // 新聞頁簽
│ └──viewmodel
│ └──NewsDataModel.ets // 新聞數(shù)據(jù)處理
└──entry/src/main/resources // 資源文件目錄
構(gòu)建新聞列表頁
新聞列表頁由頁簽區(qū)域和新聞列表區(qū)域組成,頁簽區(qū)域?yàn)樽远x布局TabBuilder,新聞列表區(qū)域?yàn)門abs組件嵌套List組件,并適配不同尺寸設(shè)備對(duì)應(yīng)的柵格。新聞列表頁能夠左右滑動(dòng)或點(diǎn)擊頁簽切換新聞Tab,并設(shè)置點(diǎn)擊新聞跳轉(zhuǎn)至新聞詳情頁。
// NewsTab.ets
@Component
export default struct NewsTab {
@State currentIndex: number = 0;
@State currentBreakpoint: string = CommonConstants.BREAKPOINT_SM;
private newsItems: NewsData[] = [];
// 自定義頁簽欄
@Builder TabBuilder(title: Resource, index: number) {
Row() {
Text(title)
.fontSize(this.currentIndex === index ? $r('app.float.lager_font_size') : $r('app.float.middle_font_size'))
.fontWeight(this.currentIndex === index ? CommonConstants.FONT_WEIGHT_500 : FontWeight.Normal)
.fontColor(this.currentIndex === index ? $r('app.color.tab_font_select') : $r('app.color.font_color_gray'))
}
.layoutWeight(1)
.margin({
right: $r('app.float.news_tab_margin_right'),
left: (this.currentBreakpoint === CommonConstants.BREAKPOINT_SM && index === 0) ?
$r('app.float.news_tab_margin_left') : 0
})
.height(this.currentIndex === index ? $r('app.float.news_tab_current_height') : $r('app.float.news_tab_height'))
}
build() {
...
Tabs() {
ForEach(CommonConstants.ALL_TITLE, (title: string, index: number) => {
TabContent() {
// 新聞內(nèi)容列表
NewsList({ newsItems: NewsDataModel.getNewsByType(this.newsItems, title) })
}
.tabBar(this.TabBuilder(NewsDataModel.getTypeByStr(title), index))
}, (title: string, index: number) => index + JSON.stringify(title))
}
.barHeight($r('app.float.news_tab_bar_height'))
.barWidth(CommonConstants.FULL_COMPONENT)
.barMode(this.currentBreakpoint === CommonConstants.BREAKPOINT_SM ? BarMode.Scrollable : BarMode.Fixed)
.onChange((index: number) => {
this.currentIndex = index;
})
...
}
}
// NewsList.ets
@Component
export default struct NewsList {
private newsItems: NewsData[] = [];
build() {
List() {
ForEach(this.newsItems, (item: NewsData, index: number) => {
ListItem() {
// 柵格布局
GridRow({
columns: {
sm: CommonConstants.FOUR_COLUMN,
md: CommonConstants.EIGHT_COLUMN,
lg: CommonConstants.TWELVE_COLUMN
},
breakpoints: {
value: [
CommonConstants.SMALL_DEVICE_TYPE,
CommonConstants.MIDDLE_DEVICE_TYPE,
CommonConstants.LARGE_DEVICE_TYPE
]
},
gutter: { x: $r('app.float.grid_row_gutter') }
}) {
GridCol({
span: {
sm: CommonConstants.FOUR_COLUMN,
md: CommonConstants.EIGHT_COLUMN,
lg: CommonConstants.EIGHT_COLUMN
},
offset: {
sm: CommonConstants.ZERO_COLUMN,
md: CommonConstants.ZERO_COLUMN,
lg: CommonConstants.TWO_COLUMN
}
}) {
NewsItem({ newsItem: item, isLast: index === this.newsItems.length - 1 })
}
}
}
}, (item: NewsData, index: number) => index + JSON.stringify(item))
}
.height(CommonConstants.FULL_COMPONENT)
}
}
構(gòu)建新聞詳情頁
新聞詳情頁
新聞詳情頁由新聞內(nèi)容區(qū)域和頁腳區(qū)域組成,其中新聞內(nèi)容區(qū)域?yàn)镾croll組件嵌套柵格組件展示新聞詳情,頁腳區(qū)域?yàn)闁鸥癫季?,包含TextInput組件和三個(gè)按鈕圖標(biāo)。
// DetailHeadContent.ets
build() {
Column() {
...
// 可滾動(dòng)的容器組件
Scroll() {
// 柵格布局
GridRow({
columns: {
sm: CommonConstants.FOUR_COLUMN,
md: CommonConstants.EIGHT_COLUMN,
lg: CommonConstants.TWELVE_COLUMN
},
breakpoints: {
value: [
CommonConstants.SMALL_DEVICE_TYPE,
CommonConstants.MIDDLE_DEVICE_TYPE,
CommonConstants.LARGE_DEVICE_TYPE
]
},
gutter: { x: $r('app.float.grid_row_gutter') }
}) {
GridCol({
span: {
sm: CommonConstants.FOUR_COLUMN,
md: CommonConstants.EIGHT_COLUMN,
lg: CommonConstants.EIGHT_COLUMN
},
offset: {
sm: CommonConstants.ZERO_COLUMN,
md: CommonConstants.ZERO_COLUMN,
lg: CommonConstants.TWO_COLUMN
}
}) {
...
}
...
}
}
.padding({
bottom: $r('app.float.news_detail_padding_bottom')
})
.scrollBar(BarState.Off)
}
.margin({
left: $r('app.float.news_detail_margin'),
right: $r('app.float.news_detail_margin')
})
.height(CommonConstants.FULL_COMPONENT)
.alignItems(HorizontalAlign.Start)
}
// DetailFooter.ets
build() {
Column() {
// 分割線
Divider()
.color($r('app.color.detail_divider_color'))
.width(CommonConstants.FULL_COMPONENT)
// 柵格布局
GridRow({
columns: {
sm: CommonConstants.FOUR_COLUMN,
md: CommonConstants.EIGHT_COLUMN,
lg: CommonConstants.TWELVE_COLUMN
},
breakpoints: {
value: [
CommonConstants.SMALL_DEVICE_TYPE,
CommonConstants.MIDDLE_DEVICE_TYPE,
CommonConstants.LARGE_DEVICE_TYPE
]
},
gutter: { x: $r('app.float.grid_row_gutter') }
}) {
GridCol({
span: {
sm: CommonConstants.FOUR_COLUMN,
md: CommonConstants.EIGHT_COLUMN,
lg: CommonConstants.EIGHT_COLUMN
},
offset: {
sm: CommonConstants.ZERO_COLUMN,
md: CommonConstants.ZERO_COLUMN,
lg: CommonConstants.TWO_COLUMN
}
}) {
...
}
.margin({
left: this.currentBreakpoint === CommonConstants.BREAKPOINT_SM ? $r('app.float.footer_margin_sm') :
$r('app.float.footer_margin_other'),
right: this.currentBreakpoint === CommonConstants.BREAKPOINT_SM ? $r('app.float.footer_margin_sm') :
$r('app.float.footer_margin_other')
})
}
.backgroundColor($r('app.color.bg_color_gray'))
.height($r('app.float.footer_height'))
.width(CommonConstants.FULL_COMPONENT)
.onBreakpointChange((breakpoints) => {
...
})
}
}
分享按鈕彈窗
頁腳點(diǎn)擊分享按鈕,彈出自定義彈窗DeviceListDialog,用于多端協(xié)同拉起應(yīng)用。DeviceListDialog由兩個(gè)標(biāo)題欄和兩個(gè)List組件構(gòu)成,其中List組件使用ForEach循環(huán)渲染設(shè)備數(shù)據(jù)。
// DeviceListDialog.ets
build() {
Column() {
Row() {
...
}
.height($r('app.float.choose_device_row_height'))
.width(CommonConstants.FULL_COMPONENT)
.padding({
left: $r('app.float.dialog_padding'),
right: $r('app.float.dialog_padding')
})
// 信任設(shè)備列表
List() {
ForEach(this.trustedDeviceList, (item: deviceManager.DeviceInfo, index: number) => {
ListItem() {
...
}
}, (item: deviceManager.DeviceInfo) => JSON.stringify(item.deviceId))
}
Row() {
...
}
.height($r('app.float.choose_device_row_height'))
.width(CommonConstants.FULL_COMPONENT)
.padding({
left: $r('app.float.dialog_padding'),
right: $r('app.float.dialog_padding')
})
// 發(fā)現(xiàn)設(shè)備列表
List() {
ForEach(this.discoverDeviceList, (item: deviceManager.DeviceInfo, index: number) => {
ListItem() {
...
}
}, (item: deviceManager.DeviceInfo) => JSON.stringify(item.deviceId))
}
Row() {
...
}
.height($r('app.float.dialog_button_row_height'))
.padding({
top: $r('app.float.dialog_button_padding_top'),
bottom: $r('app.float.dialog_button_padding_bottom'),
left: $r('app.float.dialog_padding'),
right: $r('app.float.dialog_padding')
})
.width(CommonConstants.FULL_COMPONENT)
}
.borderRadius($r('app.float.dialog_border_radius'))
.backgroundColor($r('app.color.device_dialog_background'))
.width(CommonConstants.FULL_COMPONENT)
}
多端協(xié)同拉起應(yīng)用
創(chuàng)建設(shè)備管理器
應(yīng)用創(chuàng)建時(shí)創(chuàng)建一個(gè)設(shè)備管理器實(shí)例,注冊(cè)設(shè)備狀態(tài)監(jiān)聽和獲取信任的設(shè)備列表。其中deviceManager類需使用full-SDK。
// EntryAbility.ets
onCreate(want: Want) {
...
// 創(chuàng)建設(shè)備管理器
RemoteDeviceModel.createDeviceManager(this.context);
}
// RemoteDeviceModel.ets
async createDeviceManager(context: common.UIAbilityContext): Promise<void> {
if (this.deviceManager !== undefined) {
return;
}
await new Promise((resolve: (value: Object | PromiseLike<Object>) => void, reject:
((reason?: RejectError) => void)) => {
deviceManager.createDeviceManager(context.abilityInfo.bundleName, (err, value) => {
if (err) {
reject(err);
logger.error('createDeviceManager failed.');
return;
}
this.deviceManager = value;
// 注冊(cè)設(shè)備狀態(tài)監(jiān)聽
this.registerDeviceStateListener();
// 獲取信任設(shè)備列表
this.getTrustedDeviceList();
resolve(value);
})
})
}
發(fā)現(xiàn)設(shè)備
用戶點(diǎn)擊新聞詳情頁底部的分享按鈕,調(diào)用startDeviceDiscovery()方法,發(fā)現(xiàn)周邊處在同一無線網(wǎng)絡(luò)下的設(shè)備并添加設(shè)備至已發(fā)現(xiàn)的設(shè)備列表。
// RemoteDeviceModel.ets
startDeviceDiscovery(): void {
if (this.deviceManager === undefined) {
logger.error('deviceManager has not initialized');
this.showToast($r('app.string.no_device_manager'));
return;
}
this.deviceManager.on('deviceFound', (data) => {
if (data === null) {
return;
}
// 監(jiān)聽設(shè)備發(fā)現(xiàn)
this.deviceFound(data);
})
this.deviceManager.on('discoverFail', (data) => {
logger.error(`discoverFail data = ${JSON.stringify(data)}`);
})
this.deviceManager.on('serviceDie', () => {
logger.error('serviceDie');
})
let info: deviceManager.SubscribeInfo = {
subscribeId: SUBSCRIBE_ID,
mode: CommonConstants.INFO_MODE,
medium: 0,
freq: CommonConstants.INFO_FREQ,
isSameAccount: false,
isWakeRemote: true,
capability: 0
};
// 添加設(shè)備至發(fā)現(xiàn)列表
this.discoverList = [];
AppStorage.setOrCreate(CommonConstants.DISCOVER_DEVICE_LIST, this.discoverList);
try {
this.deviceManager.startDeviceDiscovery(info);
} catch (err) {
logger.error(`startDeviceDiscovery failed error = ${JSON.stringify(err)}`);
}
}
進(jìn)行可信認(rèn)證連接
在已發(fā)現(xiàn)的設(shè)備列表中選擇設(shè)備,調(diào)用authenticateDevice()方法進(jìn)行可信認(rèn)證,輸入PIN碼,連接設(shè)備,將設(shè)備改為信任狀態(tài),添加至已信任設(shè)備列表。
// RemoteDeviceModel.ets
authenticateDevice(device: deviceManager.DeviceInfo, context: common.UIAbilityContext): void {
if (this.deviceManager === undefined) {
logger.error('deviceManager has not initialized');
this.showToast($r('app.string.no_device_manager'));
return;
}
for (let i: number = 0; i < this.discoverList.length; i++) {
if (this.discoverList[i].deviceId !== device.deviceId) {
continue;
}
let extraInfo: AuthExtraInfoInterface = {
targetPkgName: context.abilityInfo.bundleName,
appName: context.applicationInfo.name,
appDescription: context.applicationInfo.description,
business: CommonConstants.ZERO
};
let authParam: deviceManager.AuthParam = {
'authType': CommonConstants.ONE,
'extraInfo': extraInfo
};
try {
// 可信認(rèn)證
this.deviceManager.authenticateDevice(device, authParam, (err) => {
if (err) {
logger.error(`authenticateDevice error. Code is ${err.code}, message is ${err.message}`);
return;
}
})
} catch (err) {
logger.error(`authenticateDevice failed error = ${JSON.stringify(err)}`);
}
}
}
跨設(shè)備啟動(dòng)UIAbility
可信認(rèn)證后,用戶再次點(diǎn)擊分享按鈕,選擇已信任設(shè)備列表中的設(shè)備,調(diào)用startAbilityContinuation()方法進(jìn)行拉起應(yīng)用,在另一設(shè)備中觸發(fā)aboutToAppear()方法渲染當(dāng)前的新聞詳情頁,實(shí)現(xiàn)跨設(shè)備啟動(dòng)UIAbility。
// DeviceListDialog.ets
function startAbilityContinuation(deviceId: string, newsId: string, context: common.UIAbilityContext): void {
let want: Want = {
deviceId: deviceId,
bundleName: context.abilityInfo.bundleName,
abilityName: CommonConstants.ABILITY_NAME,
parameters: {
newsId: newsId
}
};
// 拉起應(yīng)用
context.startAbility(want).catch((err: Error) => {
Logger.error(`startAbilityContinuation failed error = ${JSON.stringify(err)}`);
prompt.showToast({
message: $r('app.string.start_ability_continuation_error')
});
})
}
// NewsDetail.ets
aboutToAppear() {
let newsId: string | undefined = AppStorage.get<string>('wantNewsId');
if (newsId === undefined) {
this.newsData = (router.getParams() as Record<string, NewsData>)['newsItem'];
return;
}
// 讀取跨設(shè)備傳遞的參數(shù)信息
this.newsData = this.newsItems.filter((item: NewsData) => (item.newsId === newsId))[0];
}
總結(jié)
您已經(jīng)完成了本次Codelab的學(xué)習(xí),并了解到以下知識(shí)點(diǎn):
- 構(gòu)建分布式新聞客戶端頁面。
- 實(shí)現(xiàn)應(yīng)用的一次開發(fā),多端部署。
- 使用跨設(shè)備啟動(dòng)UIAbility拉起應(yīng)用。
為了幫助大家更深入有效的學(xué)習(xí)到鴻蒙開發(fā)知識(shí)點(diǎn),小編特意給大家準(zhǔn)備了一份全套最新版的HarmonyOS NEXT學(xué)習(xí)資源,獲取完整版方式請(qǐng)點(diǎn)擊→《HarmonyOS教學(xué)視頻》
HarmonyOS教學(xué)視頻
鴻蒙語法ArkTS、TypeScript、ArkUI等…視頻教程
鴻蒙生態(tài)應(yīng)用開發(fā)白皮書V2.0PDF:
獲取完整版白皮書方式請(qǐng)點(diǎn)擊→《鴻蒙生態(tài)應(yīng)用開發(fā)白皮書V2.0PDF》
鴻蒙 (Harmony OS)開發(fā)學(xué)習(xí)手冊(cè)
一、入門必看
- 應(yīng)用開發(fā)導(dǎo)讀(ArkTS)
- .……
二、HarmonyOS 概念
- 系統(tǒng)定義
- 技術(shù)架構(gòu)
- 技術(shù)特性
- 系統(tǒng)安全
- …
三、如何快速入門?《鴻蒙基礎(chǔ)入門學(xué)習(xí)指南》
- 基本概念
- 構(gòu)建第一個(gè)ArkTS應(yīng)用
- .……
四、開發(fā)基礎(chǔ)知識(shí)
- 應(yīng)用基礎(chǔ)知識(shí)
- 配置文件
- 應(yīng)用數(shù)據(jù)管理
- 應(yīng)用安全管理
- 應(yīng)用隱私保護(hù)
- 三方應(yīng)用調(diào)用管控機(jī)制
- 資源分類與訪問
- 學(xué)習(xí)ArkTS語言
- .……
五、基于ArkTS 開發(fā)
- Ability開發(fā)
- UI開發(fā)
- 公共事件與通知
- 窗口管理
- 媒體
- 安全
- 7.網(wǎng)絡(luò)與鏈接
- 電話服務(wù)
- 數(shù)據(jù)管理
- 后臺(tái)任務(wù)(Background Task)管理
- 設(shè)備管理
- 設(shè)備使用信息統(tǒng)計(jì)
- DFX
- 國際化開發(fā)
- 折疊屏系列
- .……
文章來源:http://www.zghlxwxcb.cn/news/detail-843430.html
更多了解更多鴻蒙開發(fā)的相關(guān)知識(shí)可以參考:《鴻蒙 (Harmony OS)開發(fā)學(xué)習(xí)手冊(cè)》文章來源地址http://www.zghlxwxcb.cn/news/detail-843430.html
到了這里,關(guān)于OpenHarmony實(shí)現(xiàn)一次開發(fā)多端部署分布式新聞客戶端頁面的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!