基于HarmonyOS Next(5.0.0) 简单demo app实现
Published in:2024-10-25 |
Words: 3.5k | Reading time: 18min | reading:

基于HarmonyOS Next(5.0.0) 简单demo app实现

简介

OpenHarmony:(开源)

鸿蒙底层内核系统,集成Linux内核+LiteOS,具备底层通信能力,属于鸿蒙底层的架构层。

HarmonyOS:(闭源)

基于OpenHarmony和安卓(AOSP)打造的手机系统,包含UI界面,应用生态绑定安卓,这是目前鸿蒙的主形态。

Harmony OS NEXT:(闭源)

在HarmonyOS基础上剔除安卓(AOSP)后的产品,属于全新的手机系统,是鸿蒙系统的未来形态。
Harmony OS NEXT,也被称为纯血鸿蒙。这个系统就不再兼容安卓生态。

开发简述

!!!部分网站需登录华为开发者账号查看!!!

IDE 下载链接

1
https://developer.huawei.com/consumer/cn/deveco-studio/

ArkTS学习文档

1
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-commonlibrary-overview-V5

HarmonyOS NEXT 开发文档

1
2
3
4
# ARKUI
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkui-V5
# API
https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/arkui-api-V5?catalogVersion=V5

基础概念

应用模型

应用模型是HarmonyOS为开发者提供的应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。有了应用模型,开发者可以基于一套统一的模型进行应用开发,使应用开发更简单、高效。
随着系统的演进发展,HarmonyOS先后提供了两种应用模型:

FA(Feature Ability)模型: HarmonyOS API 7开始支持的模型,已经不再主推。

Stage模型: HarmonyOS API 9开始新增的模型,是目前主推且会长期演进的模型。在该模型中,由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型。Stage模型开发可见Stage模型开发概述。快速入门以此为例提供开发指导。

项目基本框架如下

img

入门指导链接

1
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/start-with-ets-stage-V5

hdc工具(类别adb)

通过HarmonyOS SDK获取,在SDK的toolchains目录, 直接使用hdc安装、更新HAP。

1
2
3
4
5
6
7
8
// 安装、更新,多HAP可以指定多个文件路径
hdc install entry.hap feature.hap
// 执行结果
install bundle successfully.
// 卸载
hdc uninstall com.example.myapplication
// 执行结果
uninstall bundle successfully.

开发流程

  • 注册成为开发者

在华为开发者联盟网站(https://developer.huawei.com/consumer/cn/)上,注册成为开发者,并完成实名认证,从而享受联盟开放的各类能力和服务。

  • 创建应用

    在AppGallery Connect(简称AGC)上,参考创建项目和创建应用完成HarmonyOS应用的创建,从而使用各类服务。

  • 配置安装DevEco Studio

安装最新版DevEco Studio。https://developer.huawei.com/consumer/cn/deveco-studio/

  • 使用DevEco Studio创建应用工程

  • 配置签名信息

    使用模拟器和预览器调试无需配置签名信息,使用真机设备调试则需要对HAP进行签名。

实现功能

基本功能

  • 基础页面展示

  • 基础组件使用

  • 网络请求与处理

  • log处理

使用技术

实现过程

page 页面代码编写

main page.ets

以下页面定义了文本显示、文本输入、单选框、按钮等组件;用户输入指定信息,通过PreferencesManager保存数据,待需要时重新读取;定义按钮跳转至下一个页面。

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { PageClass } from '../model/PageModel';
import { BusinessError } from '@kit.BasicServicesKit';
import PreferencesManager from '../data/PreferencesManager';
import { Logger, LogManager } from '@pie/log4a';
import { fileAppender_a } from '../logs/log_const';

@Builder
export function IndexBuilder(name: string, param: Object) {
Index()
}

@Entry
@Component
struct Index {
// @State logger: Logger = LogManager.getLogger(this);// 此处直接获取Logger即可
@State logger: Logger = LogManager.getLogger(this)
.bindAppender(fileAppender_a)
// .bindAppender(socketAppender);
@State text: string = ''
@State positionInfo: CaretOffset = { index: 0, x: 0, y: 0 }
@State passwordState: boolean = false
controller: TextInputController = new TextInputController()
pageInfos: NavPathStack = new NavPathStack()
tmp = new PageClass();

build() {
Navigation(this.pageInfos) {
Column({ space: 5 }) {
Row({ space: 5 }) {
Text("链接: ")
.fontSize(10)
.fontColor(Color.Blue)
TextInput({ text: "https://image.anosu.top/pixiv/", placeholder: 'input your link...', controller: this.controller })
.placeholderColor(Color.Grey)
.placeholderFont({ size: 14, weight: 400 })
.caretColor(Color.Blue)
.width('95%')
.height(40)
.margin(20)
.fontSize(14)
.fontColor(Color.Black)// .inputFilter('[a-z]', (e) => {
// this.logger.info(JSON.stringify(e))
// })
.onChange((value: string) => {
this.tmp.link = value
})
}.margin(5)

Row({ space: 5 }) {
Text("关键字: ")
.fontSize(10)
.fontColor(Color.Blue)
TextInput({ text: this.text, placeholder: 'input your keyword...', controller: this.controller })
.placeholderColor(Color.Grey)
.placeholderFont({ size: 14, weight: 400 })
.caretColor(Color.Blue)
.width('95%')
.height(40)
.margin(20)
.fontSize(14)
.fontColor(Color.Black)// .inputFilter('[a-z]', (e) => {
// this.logger.info(JSON.stringify(e))
// })
.onChange((value: string) => {
this.tmp.keyword = value
})
}.margin(5)
Row({ space: 12 }) {
Button('start explore')
.onClick(async () => {
PreferencesManager.shared.set("keyword", this.tmp.keyword)//保存数据
PreferencesManager.shared.set("link", this.tmp.link)
this.pageInfos.pushPathByName('PixivImage', "this.tmp")
this.logger.info("jump next page")
})
}.margin(5)
}.width('100%')
.margin(5)
}.title('首页')
}
}

PixivImage.ets

本页面通过PreferencesManager获取上个页面用户输入数据通过axio(或系统网络API)发起http请求并获取结果,最终将结果渲染至页面,展示为轮播图中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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { hilog } from '@kit.PerformanceAnalysisKit';
import getHttpData, { httpRequestGet } from '../http/HttpRequest';
import { JSON } from '@kit.ArkTS';
import { ListDataSource } from '../model/ImagegModel';
import { promptAction } from '@kit.ArkUI';
import { PageClass } from '../model/PageModel';
import PreferencesManager from '../data/PreferencesManager';
import { Logger, LogManager } from '@pie/log4a';
import { fileAppender_a, socketAppender } from '../logs/log_const';

@Builder
export function PixivImageBuilder(name: string, param: Object) {
PixivImage()
}

@Entry
@Component
struct PixivImage {
@State logger: Logger = LogManager.getLogger(this)
.bindAppender(fileAppender_a)
swiperController: SwiperController = new SwiperController();
@State pixelMapImg: PixelMap | undefined = undefined;
@State data: ListDataSource<string> = new ListDataSource<string>();
tmp = new PageClass();
keyword: string = "klee" ;
link: string = 'https://image.anosu.top/pixiv/';
// pathStack: NavPathStack = new NavPathStack()
@Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()

@Builder
PagesMap(name: string) {
if (name == 'PixivImage') {
PixivImage()
}
}

private menuItems: Array<NavigationMenuItem> = [

]


async aboutToAppear() {
// this.requestImageUrl(this.src_pix);// 请填写一个具体的网络图片地址
hilog.warn(0x0000, 'testTag', '%{public}s', 'start load image');
this.keyword = PreferencesManager.shared.get("keyword")?.toString() || "klee";//读取存储数据
// 如果结果为falsy值(包括null、undefined、''、0、NaN、false),则默认为空字符串
this.link = PreferencesManager.shared.get("link")?.toString() || "https://image.anosu.top/pixiv/"
this.logger.info("result get page : " + this.keyword +" , link " + this.link)
this.data.pushData(await getHttpData(this.keyword, this.link))
this.logger.info(this.data.getData(0))
promptAction.showToast({
message: "load result: " + this.data.getData(0),
duration: 2000
});

//}
// this.list.push(await getHttpData());
}

build() {
NavDestination() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
LazyForEach(this.data, (item: string) => {
Image(item)
.alt($r('app.media.startIcon'))
.objectFit(ImageFit.None)
.width('95%')
.height('95%')

}, (item: string) => item)
}
.indicator(true)
.autoPlay(true)
.loop(true)

Row({ space: 12 }) {
Button('show next')
.onClick(async () => {
// this.data.pushData()
// this.aboutToAppear();
// this.list.push(await getHttpData());
this.data.pushData(await getHttpData(this.keyword, this.link))

// this.logger.info("list msg length: " + this.list.length)
this.logger.info("data msg length: " + this.data.totalCount())
promptAction.showToast({
message: "load result: " + this.data.getData(0),
duration: 2000
});

})
}.margin(5)
}.width('100%')
.margin({ top: 5 })
}
.title('PixivImage')
.menus(this.menuItems)
.onBackPressed(() => {
this.pageStack.pop()
return true
})
.onReady((context: NavDestinationContext) => {
this.pageStack = context.pathStack;
this.logger.info("current page config info is " + JSON.stringify(context.getConfigInRouteMap()))
})
}
}

http请求发起

以下方法使用系统@kit.NetworkKit发起http get 请求

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
import { http } from "@kit.NetworkKit";
import { JSON } from "@kit.ArkTS";

export default async function getHttpData( keyword: string, link: string): Promise<string> {
let httpRequest = http.createHttp();
if (link) {
BASE_URL = link;
console.log("BASE_URL " + BASE_URL)
}
let response = httpRequest.request(
// 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
BASE_URL + "direct" +"&keyword=" + keyword,
{
method: http.RequestMethod.GET, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
}
);
// 使用await和async,等待请求完成处理数据后返回
await response.then((data: object) => {
if (data["responseCode"] = 200) {
// 处理返回结果
res = JSON.stringify(data["header"]["location"]);
console.log("output image src: " + res);
} else {
// todo 请求失败,进行失败逻辑处理
}
}).catch((err: object) => {
// todo 请求失败,进行失败逻辑处理
console.info('error:' + JSON.stringify(err));
})
return res.replace('"', '').replace('"', "");
}

log处理

log初始化

在EntryAbility中onCreate()周期函数编写以下代码

1
2
3
4
5
6
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
LogManager.setLogFilePath(this.context.filesDir);
LogManager.interceptConsole(); //开启console拦截
}

log 基础定义

以下代码定义了log基础信息:文件名,多线程日志记录,单个log最大大小,log缓存数目,log上报信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { AbstractAppender, FileAppender, Level, TCPSocketAppender } from "@pie/log4a";
import { systemDateTime } from "@kit.BasicServicesKit";
import { getSysTime, getTimeToYYYYDDMMHHMMSS } from "../utils/time";

export const socketAppender: AbstractAppender = new TCPSocketAppender({
address: '114.xxx.xxx.xxx',
port: 1234,
name: 'socket',
level: Level.ALL
});

export const fileAppender_a: AbstractAppender =
new FileAppender('OHOS_PIXIV_log_' + getSysTime()+ '.log', 'main',
Level.ALL, {
useWorker: true,
maxFileSize: 1024 * 20, //kb 存储单个文件大小 超过会存入缓存
maxCacheCount: 10 //日志文件缓存数量
}
);

log 使用

在page页面使用如下

1
2
3
4
5
6
  @State logger: Logger = LogManager.getLogger(this)
.bindAppender(fileAppender_a)
// .bindAppender(socketAppender); //日志上报append
// 打印log
this.logger.info('Radio1 status is ' + isChecked)

log 输出示例

1
10-25 21:41:55.491   2317-2317     A03d00/JSAPP                    com.examp...lication  I     [INFO ]	2024-10-25 21:41:55,488	[PixivImage:2]	current page config info is {"name":"PixivImage","pageSourceFile":"src/main/ets/pages/PixivImage.ets","data":{"description":"this is PixivImage"}}

数据存储

  • 工具类定义

使用PreferencesManager保存读取数据,示例如下

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
// 导入包
import dataPreferences from '@ohos.data.preferences';

// 获取context
let context = getContext(this);
export default class PreferencesManager {

static shared = new PreferencesManager();
preferences?: dataPreferences.Preferences;
preferencesName: string = 'CommonPreferences';

// 初始化preferences实例
initPreferences() {
this.preferences = dataPreferences.getPreferencesSync(context, { name: this.preferencesName });
}

// 设置数据
set(key: string, value: dataPreferences.ValueType) {
if (!this.preferences) {
this.initPreferences();
}
this.preferences?.putSync(key, value);
this.preferences?.flush();
}

// 获取数据
get(key: string): dataPreferences.ValueType | null | undefined {
if (!this.preferences) {
this.initPreferences();
}
let value = this.preferences?.getSync(key, null);;
return value;
}

// 删除数据
delete(key: string) {
if (!this.preferences) {
this.initPreferences();
}
if (this.preferences?.hasSync(key)) {
this.preferences.deleteSync(key);
this.preferences.flush();
}
}
}

  • 工具类使用
1
2
3
4
// 写入数据
PreferencesManager.shared.set("keyword", this.tmp.keyword)
// 读取数据
this.keyword = PreferencesManager.shared.get("keyword")?.toString() || "klee";

轮播图数据处理

基础数据类的定义

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
65
66
// Basic implementation of IDataSource to handle data listener
export class BasicDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: T[] = [];

public totalCount(): number {
return 0;
}

public getData(index: number): T {
return this.originDataArray[index];
}

// 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}

// 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 1);
}
}

// 通知LazyForEach组件需要重载所有子组件
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}

// 通知LazyForEach组件需要在index对应索引处添加子组件
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}

// 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}

// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}

// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
}

数据模型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { BasicDataSource } from "./BasicDataSource";

export class ListDataSource<T> extends BasicDataSource<T> {
private dataArray: T[] = [];

public totalCount(): number {
return this.dataArray.length;
}

public getData(index: number): T {
return this.dataArray[index];
}

public addData(index: number, data: T): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}

public pushData(data: T): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}

通过以下方法使用

1
2
3
4
5
6
// 数据初始化
@State data: ListDataSource<string> = new ListDataSource<string>();
// 数据存储
this.data.pushData(await getHttpData(this.keyword, this.link))
// 数据获取
this.data.getData(index)

其他

toast

1
2
3
4
promptAction.showToast({
message: "load result: " + this.data.getData(0),
duration: 2000
});

页面跳转

配置问价module.json5更改

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
65
66
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"routerMap": "$profile:route_map",//路由更改
"deviceTypes": [
"phone",
"tablet",
"2in1",
"car"
],
"requestPermissions":[
{
"name" : "ohos.permission.INTERNET",//网络权限申请
"reason": "$string:permission_reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"inuse"
}
},
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
}
}

在entry/src/main/resources/base/profile处新建route_map.json并写入以下代码

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
{
"routerMap": [
{
"name": "Index",
"pageSourceFile": "src/main/ets/pages/Index.ets",
"buildFunction": "IndexBuilder",
"data": {
"description": "this is Index"
}
},
{
"name": "PixivImage",
"pageSourceFile": "src/main/ets/pages/PixivImage.ets",
"buildFunction": "PixivImageBuilder",
"data": {
"description": "this is PixivImage"
}
},
{
"name": "SaveImagge",
"pageSourceFile": "src/main/ets/pages/SaveImagge.ets",
"buildFunction": "SaveAlbumBuilder",
"data": {
"description": "this is Save Image page"
}
}
]
}

跳转代码

1
this.pageInfos.pushPathByName('PixivImage', "this.tmp")

测试效果

安装后效果如下:

img

log生成路径如下:

1
/data/app/el2/100/base/com.example.myapplication/haps/entry/files

img

See

demo 地址

1
https://gitee.com/caozhaoqi/ohos_-pixiv_-image

参考网址

1
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5

blog

1
https://caozhaoqi.github.io/
Prev:
基于ESP32-CAM的智能猫眼实现
Next:
pyqt5信号槽机制应用