第一章
介绍
经过前四节的学习,您已经能够完成单一页面的开发,也学习了简单的状态管理。在接下来的两节中我们将会学习如何应用MVVM模式和三层架构开发更加复杂的APP。
根据本教程【应用架构设计基础——MVVM模式】学习,您可以选择从上一教程结束时构建完成的项目开始,也可以直接运行StartingPoint文件夹中的项目。
根据教程中的提示使用Resources文件夹中的文件。
如果您想要自行探索,可以直接打开Complete文件夹中的项目并浏览代码。
项目文件下载
下载项目文件并开始构建本项目。按照以下步骤操作
MVVM模式
ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图。
ArkUI中,model为我们定义的数据结构和数据来源,通过ArkUI提供的装饰器@State等装饰对应的数据,就提供了响应式能力,model数据的变化能够触发UI的更新。
Step 1
目录结构调整
为了让代码更加清晰,容易维护,我们需要对代码进行分层管理,常见的数据结构放置在model文件夹中,UI组件放置在view文件夹中,并以对应的组件名命名。
Step 2
建立model文件夹
在entry/src/main/ets文件夹下点击右键 - > new - > Directory。
文件夹命名为model。
model文件夹用于存储数据模型。它表示组件或其他相关业务逻辑之间传输的数据,是对原始数据的进一步处理。
Step 3
迁移对应的Class文件
在刚才新建的model文件夹下新建两个ets文件,分别命名为BannerClass和ArticleClass。
将Index文件内的BannerClass声明移到BannerClass文件内,ArticleClass声明移到ArticleClass文件内。
class BannerClass {
// ...
}
class ArticleClass {
// ...
}
@Entry
@Component
struct Index {
// ...
}
No Preview
由于现在BannerClass在独立的文件中,Index要使用这个文件的内容,就需要为BannerClass添加export关键字。
// src/main/ets/model/BannerClass.ets
export class BannerClass {
id: string = '';
imageSrc: ResourceStr = '';
url: string = '';
constructor(id: string, imageSrc: ResourceStr,url: string) {
this.id = id;
this.imageSrc = imageSrc;
this.url = url;
}
}
No Preview
同理,对ArticleClass也需要增加export关键字。
// src/main/ets/model/ArticleClass.ets
export class ArticleClass {
id: string = '';
imageSrc: ResourceStr = '';
title: string = '';
brief: string = '';
webUrl: string = '';
constructor(id: string, imageSrc: ResourceStr, title: string, brief: string, webUrl: string) {
this.id = id;
this.imageSrc = imageSrc;
this.title = title;
this.brief = brief;
this.webUrl = webUrl;
}
}
No Preview
在Index.ets文件内使用import导入BannerClass和ArticleClass,并移除之前Index.ets中声明的BannerClass和ArticleClass的内容。
// src/main/ets/page/Index.ets
import {BannerClass} from '../model/BannerClass';
import {ArticleClass} from '../model/ArticleClass';
No Preview
Step 4
创建view文件夹,用于存储UI组件
在entry/src/main/ets文件夹下点击右键 - > new - > Directory,命名为view,用于存放页面相关的自定义组件。
通过右键单击view文件夹,选择New-> ArkTS file,分别创建EnablementView.ets和TutorialView.ets以及Banner.ets文件。
将Index中EnablementView和EnablementItem移动到EnablementView文件中,并注意引入对应的ArticleClass文件类型。
// src/main/ets/view/EnablementView.ets
import { ArticleClass } from '../model/ArticleClass';
@Component
struct EnablementView {
// ...
}
@Preview
@Component
struct EnablementItem {
// ...
}
No Preview
需要注意,由于需要在Index文件中引入使用,在EnablementVIew声明时需要使用export导出。
// src/main/ets/view/EnablementView.ets
import { ArticleClass } from '../model/ArticleClass';
@Component
export struct EnablementView {
// ...
}
@Component
struct EnablementItem {
// ...
}
No Preview
将TutorialView和TutorialItem的声明从Index.ets移入TutorialView.ets文件中,并注意引入对应的ArticleClass文件类型。同样需要注意使用export导出TutorialView。
// src/main/ets/view/TutorialView.ets
import { ArticleClass } from '../model/ArticleClass';
@Component
export struct TutorialView {
// ...
}
@Component
struct TutorialItem {
// ...
}
No Preview
同样将Banner移入Banner文件中,并注意引入对应的BannerClass文件类型。需要导出Banner供其他页面使用。
// src/main/ets/view/Banner.ets
import { BannerClass } from '../model/BannerClass';
@Component
export struct Banner {
// ...
}
No Preview
在Index文件中,引入Banner.ets,EnablementView.ets,TutorialView.ets文件。此时由于不再需要用到BannerClass和ArticleClass,所以对这两个类的import语句可以删掉。
// src/main/ets/pages/Index.ets
// 注释的import语句可以移除
// import {BannerClass} from '../model/BannerClass';
// import {ArticleClass} from '../model/ArticleClass';
import { TutorialView } from '../view/TutorialView';
import { Banner } from '../view/Banner';
import { EnablementView } from '../view/EnablementView';
@Component
export struct Index {
@State message: string = '快速入门';
build() {
Column() {
Text(this.message)
.fontSize(24)
.fontWeight(700)
.width('100%')
.textAlign(TextAlign.Start)
.margin({ left: 16 })
.fontFamily('HarmonyHeiTi-Bold')
.lineHeight(33)
Scroll() {
Column() {
Banner()
EnablementView()
TutorialView()
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
}
No Preview
Step 5
加载json数据
现在目录结构已经调整完成,之前的开发过程中,我们的数据是固定写在组件声明中的,实际场景中,我们的数据可能是来自网络或者本地数据库,这里我们使用json文件来模拟数据来源,来实现数据请求。
Step 6
在main/resources资源目录下找到rawfile文件夹,用于存储json数据。(若不存在,需要手动创建一个rawfile文件夹)
rawfile目录中的资源文件会被直接打包进应用,不经过编译,也不会被赋予资源文件ID。通过指定文件路径和文件名引用。
添加数据文件
将下载的初始包Resources文件夹中的BannerData.json文件、EnablementData.json文件和TutorialData.json文件放在rawfile文件夹下。
在之前的内容中,页面上bannerList的内容是已经创建好的,现在这部分内容需要在页面加载的时候,从json中读取。
// src/main/ets/view/Banner.ets
@Preview
@Component
export struct Banner {
@State bannerList: Array<BannerClass> = [
new BannerClass('pic0', $r('app.media.banner_pic0'),
'https://developer.huawei.com/consumer/cn/training/course/video/C101718352529709527'),
new BannerClass('pic1', $r('app.media.banner_pic1'),
'https://developer.huawei.com/consumer/cn/'),
new BannerClass('pic2', $r('app.media.banner_pic2'),
'https://developer.huawei.com/consumer/cn/deveco-studio/'),
new BannerClass('pic3', $r('app.media.banner_pic3'),
'https://developer.huawei.com/consumer/cn/arkts/'),
new BannerClass('pic4', $r('app.media.banner_pic4'),
'https://developer.huawei.com/consumer/cn/arkui/'),
new BannerClass('pic5', $r('app.media.banner_pic5'),
'https://developer.huawei.com/consumer/cn/sdk/')
];
build() {
// ...
}
}
No Preview
读取json文件内容
读取json文件内容需要使用到ResourceManager的getRawfileContent方法,从rawfile文件中读取对应的JSON文件
在Banner文件中,删除bannerList对应初始化的内容,并赋值一个空数组[];
// src/main/ets/view/Banner.ets
@Component
export struct Banner {
@State bannerList: BannerClass[] = [];
build() {
// ...
}
}
No Preview
在Banner中,定义一个方法getBannerDataFromJson,并通过ResourceManager获取当前工程目录下rawfile中的json文件内容。
转换内容需要两个步骤:
1、将获取的buffer内容转换为字符串
2、将字符串转换为页面数据结构
// src/main/ets/view/Banner.ets
@Component
export struct Banner {
@State bannerList: BannerClass[] = [];
getBannerDataFromJSON() {
getContext(this).resourceManager.getRawFileContent('BannerData.json').then(value => {
// 获取buffer内容
// 转换为字符串
// 解析为数据结构
})
}
build() {
// ...
}
}
No Preview
转换为字符串
由于ResourceManager获取到的是Uint8Array类型的内容,所以需要将对应的内容转换为字符串,并将字符串解析为对应的数据结构。考虑到其他的文件也会使用这个公共方法,可以新建一个util文件夹,并创建一个BufferUtil文件,实现这个字符串转换方法。
具体的实现内容如右所示,由于需要给其他组件使用,所以函数前面需要使用export导出
// src/main/ets/util/BufferUtil.ets
import { util } from '@kit.ArkTS';
export function bufferToString(buffer: Uint8Array): string {
let textDecoder = util.TextDecoder.create('utf-8', {
ignoreBOM: true
});
let resultPut = textDecoder.decodeToString(buffer);
return resultPut;
}
No Preview
将字符串转换为页面数据结构
这里需要注意,如果要使用刚刚的工具函数,需要从util导入BufferUtil进行使用。
拿到了对应字符串后,通过JSON.parse可以将对应的数据转换为Object类型的数据结构,由于我们读取的内容是BannerClass[]的类型,所以可以通过 as 将其断言为BannerClass[]。
// src/main/ets/view/Banner.ets
import { bufferToString } from '../util/BufferUtil';
@Component
export struct Banner {
@State bannerList: BannerClass[] = [];
getBannerDataFromJSON() {
getContext(this).resourceManager.getRawFileContent('BannerData.json').then(value => {
// 转换为字符串
let res: string = bufferToString(value);
// 解析为数据结构
this.bannerList = JSON.parse(res) as BannerClass[];
})
}
build() {
// ...
}
}
No Preview
以上代码为了方便讲解步骤,可以简写为如右代码
// src/main/ets/view/Banner.ets
@Component
export struct Banner {
@State bannerList: BannerClass[] = [];
getBannerDataFromJSON() {
getContext(this).resourceManager.getRawFileContent('BannerData.json').then(value => {
this.bannerList = JSON.parse(bufferToString(value)) as BannerClass[];
})
}
build() {
// ...
}
}
No Preview
为了让页面在启动时能够获取到数据,我们需要了解页面的生命周期执行流程,并决定页面获取数据的时机。
Step 7
为Banner组件添加aboutToAppear生命周期回调函数。
aboutToAppear函数在创建自定义组件的新实例后,在执行其build()函数之前执行。
// src/main/ets/view/Banner.ets
@Component
struct Banner {
@State bannerList : BannerClass[] = [] ;
aboutToAppear(): void {
}
getBannerDataFromJSON() {
// ...
}
build() {
//...
}
}
No Preview
在该回调函数中调用上一步骤中创建的getBannerDataFromJSON方法,表示在该时间调用方法进行json文件的读取,并存入bannerList 。
// src/main/ets/view/Banner.ets
@Component
struct Banner {
@State bannerList : Array<BannerClass> = [];
aboutToAppear(): void {
this.getBannerDataFromJSON();
}
getBannerDataFromJSON() {
// ...
}
build() {
// ...
}
}
No Preview
如果您已经完成了上面的步骤,可能会发现,还是无法正确获取内容,通过观察可以发现,我们在json中存储的数据如右所示
// src/main/resources/rawfile/BannerData.json
[
{
"id": 0,
"imageSrc": "app.media.banner_pic0",
"url": "https://developer.huawei.com/consumer/cn/training/course/video/C101718352529709527"
},
...
]
No Preview
但是我们在之前页面开发时,所使用的数据结构如右图所示
// 修改前的Banner代码
@Component
export struct Banner {
@State bannerList: Array<BannerClass> = [
new BannerClass('pic0', $r('app.media.banner_pic0'),
'https://developer.huawei.com/consumer/cn/training/course/video/C101718352529709527'),
// ...
];
build() {
// ...
}
}
No Preview
可以发现,不同点在于,json中数据由于无法使用$r()进行资源访问,所以使用的是字符串"app.media.banner_pic0",而在页面中直接声明时,使用的是$r('app.media.banner_pic0'),而Image组件是无法直接读取字符串"app.media.banner_pic0"的,所以这里需要进行内容调整。
// src/main/ets/view/Banner.ets
@Component
export struct Banner {
@State bannerList: BannerClass[] = [];
aboutToAppear(): void {
this.getBannerDataFromJSON()
}
getBannerDataFromJSON() {
//...
}
build() {
Swiper() {
ForEach(this.bannerList, (item: BannerClass) => {
Image(item.imageSrc)
.objectFit(ImageFit.Cover)
.width('100%')
.aspectRatio(656 / 288)
.borderRadius(16)
.padding({ top: 11, left: 16, right: 16 })
}, (item: BannerClass) => item.id)
}
.autoPlay(true)
.loop(true)
.indicator(
new DotIndicator()
.color('#1a000000')
.selectedColor('#0A59F7'))
}
}
No Preview
1、调整BannerClass的定义,将imageSrc的类型修改为string,以及constructor内参数imageSrc也需要修改为string类型
// src/main/ets/model/BannerClass.ets
export class BannerClass {
id: string = '';
imageSrc: string = '';
url: string = ''
constructor(id: string, imageSrc: string, url: string) {
this.id = id
this.imageSrc = imageSrc;
this.url = url;
}
}
No Preview
页面上Image组件使用方式也需要修改为通过$r()来读取"app.media.banner_pic0"内容
// src/main/ets/view/Banner.ets
@Component
export struct Banner {
@State bannerList: BannerClass[] = [];
aboutToAppear(): void {
this.getBannerDataFromJSON()
}
getBannerDataFromJSON() {
// ...
}
build() {
Swiper() {
ForEach(this.bannerList, (item: BannerClass) => {
Image($r(item.imageSrc))
// ...
}, (item: BannerClass) => item.id)
}
// ...
}
}
No Preview
Step 8
将EnablementView中enablementList的数据改为空数组,并尝试参照步骤7,在EnablementView中获取EnablementData.json数据,此处需要改动的文件为EnablementView和ArticleClass。
// src/main/ets/view/EnablementView.ets
@Component
struct EnablementView {
@State enablementList: Array<ArticleClass> = [];
build() {
// ...
}
}
No Preview
注意要修改ArticleClass内imageSrc的类型,以及EnablementItem中Image的读取方式!
Step 9
将TutorialView中tutorialList的数据改为空数组,并尝试参照步骤7,在TutorialView中获取TutorialData.json数据,此处需要改动的文件为TutorialView.ets。
// src/main/ets/view/TutorialView.ets
@Component
struct TutorialView {
@State tutorialList: Array<ArticleClass> = [];
build() {
// ...
}
}
No Preview
如果你已经掌握了该内容,可以跳过第8、9步骤,直接参考Complete文件夹内的工程文件代码。
接下来连接真机设备,确保DevEco Studio与真机设备已连接,真机连接成功后如下图所示:
由于json文件解析限制,当前不支持预览器模式,开发者可以选择模拟器或真机进行调试。
模拟器使用教程请参考官网指南:使用模拟器运行应用/服务
Step 9
开启自动签名
进入File > Project Structure... > Project > Signing Configs界面,勾选“Automatically generate signature”,即可完成签名。如果未登录,请先单击Sign In进行登录,然后自动完成签名。
签名完成后,如图所示
Step 10
在真机/模拟器上运行。
在菜单栏中,单击Run>Run'模块名称',或使用默认快捷键Shift+F10(macOS为Control+R)运行应用/服务。
DevEco Studio启动HAP的编译构建和安装。安装成功后,设备会自动运行安装的应用。运行效果如下:
因为预览器并不支持获取rawfile目录下的文件,所以无法成功获取到保存在rawfile目录下的json文件里的内容。请用真机/模拟器测试,预览器仅支持简单页面的预览。