鸿蒙应用架构设计基础——MVVM模式

第一章

介绍

经过前四节的学习,您已经能够完成单一页面的开发,也学习了简单的状态管理。在接下来的两节中我们将会学习如何应用MVVM模式和三层架构开发更加复杂的APP。

根据本教程【应用架构设计基础——MVVM模式】学习,您可以选择从上一教程结束时构建完成的项目开始,也可以直接运行StartingPoint文件夹中的项目。

根据教程中的提示使用Resources文件夹中的文件。

如果您想要自行探索,可以直接打开Complete文件夹中的项目并浏览代码。

项目文件下载

下载项目文件并开始构建本项目。按照以下步骤操作

05_MVVMPatternDesign.zip

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文件内。

 
  1. class BannerClass {
  2. // ...
  3. }
  4. class ArticleClass {
  5. // ...
  6. }
  7. @Entry
  8. @Component
  9. struct Index {
  10. // ...
  11. }

No Preview

由于现在BannerClass在独立的文件中,Index要使用这个文件的内容,就需要为BannerClass添加export关键字。

 
  1. // src/main/ets/model/BannerClass.ets
  2. export class BannerClass {
  3. id: string = '';
  4. imageSrc: ResourceStr = '';
  5. url: string = '';
  6. constructor(id: string, imageSrc: ResourceStr,url: string) {
  7. this.id = id;
  8. this.imageSrc = imageSrc;
  9. this.url = url;
  10. }
  11. }

No Preview

同理,对ArticleClass也需要增加export关键字。

 
  1. // src/main/ets/model/ArticleClass.ets
  2. export class ArticleClass {
  3. id: string = '';
  4. imageSrc: ResourceStr = '';
  5. title: string = '';
  6. brief: string = '';
  7. webUrl: string = '';
  8. constructor(id: string, imageSrc: ResourceStr, title: string, brief: string, webUrl: string) {
  9. this.id = id;
  10. this.imageSrc = imageSrc;
  11. this.title = title;
  12. this.brief = brief;
  13. this.webUrl = webUrl;
  14. }
  15. }

No Preview

在Index.ets文件内使用import导入BannerClass和ArticleClass,并移除之前Index.ets中声明的BannerClass和ArticleClass的内容。

 
  1. // src/main/ets/page/Index.ets
  2. import {BannerClass} from '../model/BannerClass';
  3. 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文件类型。

 
  1. // src/main/ets/view/EnablementView.ets
  2. import { ArticleClass } from '../model/ArticleClass';
  3. @Component
  4. struct EnablementView {
  5. // ...
  6. }
  7. @Preview
  8. @Component
  9. struct EnablementItem {
  10. // ...
  11. }

No Preview

需要注意,由于需要在Index文件中引入使用,在EnablementVIew声明时需要使用export导出。

 
  1. // src/main/ets/view/EnablementView.ets
  2. import { ArticleClass } from '../model/ArticleClass';
  3. @Component
  4. export struct EnablementView {
  5. // ...
  6. }
  7. @Component
  8. struct EnablementItem {
  9. // ...
  10. }

No Preview

将TutorialView和TutorialItem的声明从Index.ets移入TutorialView.ets文件中,并注意引入对应的ArticleClass文件类型。同样需要注意使用export导出TutorialView。

 
  1. // src/main/ets/view/TutorialView.ets
  2. import { ArticleClass } from '../model/ArticleClass';
  3. @Component
  4. export struct TutorialView {
  5. // ...
  6. }
  7. @Component
  8. struct TutorialItem {
  9. // ...
  10. }

No Preview

同样将Banner移入Banner文件中,并注意引入对应的BannerClass文件类型。需要导出Banner供其他页面使用。

 
  1. // src/main/ets/view/Banner.ets
  2. import { BannerClass } from '../model/BannerClass';
  3. @Component
  4. export struct Banner {
  5. // ...
  6. }

No Preview

在Index文件中,引入Banner.ets,EnablementView.ets,TutorialView.ets文件。此时由于不再需要用到BannerClass和ArticleClass,所以对这两个类的import语句可以删掉。

 
  1. // src/main/ets/pages/Index.ets
  2. // 注释的import语句可以移除
  3. // import {BannerClass} from '../model/BannerClass';
  4. // import {ArticleClass} from '../model/ArticleClass';
  5. import { TutorialView } from '../view/TutorialView';
  6. import { Banner } from '../view/Banner';
  7. import { EnablementView } from '../view/EnablementView';
  8. @Component
  9. export struct Index {
  10. @State message: string = '快速入门';
  11. build() {
  12. Column() {
  13. Text(this.message)
  14. .fontSize(24)
  15. .fontWeight(700)
  16. .width('100%')
  17. .textAlign(TextAlign.Start)
  18. .margin({ left: 16 })
  19. .fontFamily('HarmonyHeiTi-Bold')
  20. .lineHeight(33)
  21. Scroll() {
  22. Column() {
  23. Banner()
  24. EnablementView()
  25. TutorialView()
  26. }
  27. }
  28. .layoutWeight(1)
  29. .scrollBar(BarState.Off)
  30. .align(Alignment.TopStart)
  31. }
  32. .width('100%')
  33. .height('100%')
  34. .backgroundColor('#F1F3F5')
  35. }
  36. }

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中读取。

 
  1. // src/main/ets/view/Banner.ets
  2. @Preview
  3. @Component
  4. export struct Banner {
  5. @State bannerList: Array<BannerClass> = [
  6. new BannerClass('pic0', $r('app.media.banner_pic0'),
  7. 'https://developer.huawei.com/consumer/cn/training/course/video/C101718352529709527'),
  8. new BannerClass('pic1', $r('app.media.banner_pic1'),
  9. 'https://developer.huawei.com/consumer/cn/'),
  10. new BannerClass('pic2', $r('app.media.banner_pic2'),
  11. 'https://developer.huawei.com/consumer/cn/deveco-studio/'),
  12. new BannerClass('pic3', $r('app.media.banner_pic3'),
  13. 'https://developer.huawei.com/consumer/cn/arkts/'),
  14. new BannerClass('pic4', $r('app.media.banner_pic4'),
  15. 'https://developer.huawei.com/consumer/cn/arkui/'),
  16. new BannerClass('pic5', $r('app.media.banner_pic5'),
  17. 'https://developer.huawei.com/consumer/cn/sdk/')
  18. ];
  19. build() {
  20. // ...
  21. }
  22. }

No Preview

读取json文件内容

读取json文件内容需要使用到ResourceManager的getRawfileContent方法,从rawfile文件中读取对应的JSON文件

在Banner文件中,删除bannerList对应初始化的内容,并赋值一个空数组[];

 
  1. // src/main/ets/view/Banner.ets
  2. @Component
  3. export struct Banner {
  4. @State bannerList: BannerClass[] = [];
  5. build() {
  6. // ...
  7. }
  8. }

No Preview

在Banner中,定义一个方法getBannerDataFromJson,并通过ResourceManager获取当前工程目录下rawfile中的json文件内容。

转换内容需要两个步骤:

1、将获取的buffer内容转换为字符串

2、将字符串转换为页面数据结构

 
  1. // src/main/ets/view/Banner.ets
  2. @Component
  3. export struct Banner {
  4. @State bannerList: BannerClass[] = [];
  5. getBannerDataFromJSON() {
  6. getContext(this).resourceManager.getRawFileContent('BannerData.json').then(value => {
  7. // 获取buffer内容
  8. // 转换为字符串
  9. // 解析为数据结构
  10. })
  11. }
  12. build() {
  13. // ...
  14. }
  15. }

No Preview

转换为字符串

由于ResourceManager获取到的是Uint8Array类型的内容,所以需要将对应的内容转换为字符串,并将字符串解析为对应的数据结构。考虑到其他的文件也会使用这个公共方法,可以新建一个util文件夹,并创建一个BufferUtil文件,实现这个字符串转换方法。

具体的实现内容如右所示,由于需要给其他组件使用,所以函数前面需要使用export导出

 
  1. // src/main/ets/util/BufferUtil.ets
  2. import { util } from '@kit.ArkTS';
  3. export function bufferToString(buffer: Uint8Array): string {
  4. let textDecoder = util.TextDecoder.create('utf-8', {
  5. ignoreBOM: true
  6. });
  7. let resultPut = textDecoder.decodeToString(buffer);
  8. return resultPut;
  9. }

No Preview

将字符串转换为页面数据结构

这里需要注意,如果要使用刚刚的工具函数,需要从util导入BufferUtil进行使用。

拿到了对应字符串后,通过JSON.parse可以将对应的数据转换为Object类型的数据结构,由于我们读取的内容是BannerClass[]的类型,所以可以通过 as 将其断言为BannerClass[]。

 
  1. // src/main/ets/view/Banner.ets
  2. import { bufferToString } from '../util/BufferUtil';
  3. @Component
  4. export struct Banner {
  5. @State bannerList: BannerClass[] = [];
  6. getBannerDataFromJSON() {
  7. getContext(this).resourceManager.getRawFileContent('BannerData.json').then(value => {
  8. // 转换为字符串
  9. let res: string = bufferToString(value);
  10. // 解析为数据结构
  11. this.bannerList = JSON.parse(res) as BannerClass[];
  12. })
  13. }
  14. build() {
  15. // ...
  16. }
  17. }

No Preview

以上代码为了方便讲解步骤,可以简写为如右代码

 
  1. // src/main/ets/view/Banner.ets
  2. @Component
  3. export struct Banner {
  4. @State bannerList: BannerClass[] = [];
  5. getBannerDataFromJSON() {
  6. getContext(this).resourceManager.getRawFileContent('BannerData.json').then(value => {
  7. this.bannerList = JSON.parse(bufferToString(value)) as BannerClass[];
  8. })
  9. }
  10. build() {
  11. // ...
  12. }
  13. }

No Preview

为了让页面在启动时能够获取到数据,我们需要了解页面的生命周期执行流程,并决定页面获取数据的时机。

Step 7

为Banner组件添加aboutToAppear生命周期回调函数。

aboutToAppear函数在创建自定义组件的新实例后,在执行其build()函数之前执行。

 
  1. // src/main/ets/view/Banner.ets
  2. @Component
  3. struct Banner {
  4. @State bannerList : BannerClass[] = [] ;
  5. aboutToAppear(): void {
  6. }
  7. getBannerDataFromJSON() {
  8. // ...
  9. }
  10. build() {
  11. //...
  12. }
  13. }

No Preview

在该回调函数中调用上一步骤中创建的getBannerDataFromJSON方法,表示在该时间调用方法进行json文件的读取,并存入bannerList 。

 
  1. // src/main/ets/view/Banner.ets
  2. @Component
  3. struct Banner {
  4. @State bannerList : Array<BannerClass> = [];
  5. aboutToAppear(): void {
  6. this.getBannerDataFromJSON();
  7. }
  8. getBannerDataFromJSON() {
  9. // ...
  10. }
  11. build() {
  12. // ...
  13. }
  14. }

No Preview

如果您已经完成了上面的步骤,可能会发现,还是无法正确获取内容,通过观察可以发现,我们在json中存储的数据如右所示

 
  1. // src/main/resources/rawfile/BannerData.json
  2. [
  3. {
  4. "id": 0,
  5. "imageSrc": "app.media.banner_pic0",
  6. "url": "https://developer.huawei.com/consumer/cn/training/course/video/C101718352529709527"
  7. },
  8. ...
  9. ]

No Preview

但是我们在之前页面开发时,所使用的数据结构如右图所示

 
  1. // 修改前的Banner代码
  2. @Component
  3. export struct Banner {
  4. @State bannerList: Array<BannerClass> = [
  5. new BannerClass('pic0', $r('app.media.banner_pic0'),
  6. 'https://developer.huawei.com/consumer/cn/training/course/video/C101718352529709527'),
  7. // ...
  8. ];
  9. build() {
  10. // ...
  11. }
  12. }

No Preview

可以发现,不同点在于,json中数据由于无法使用$r()进行资源访问,所以使用的是字符串"app.media.banner_pic0",而在页面中直接声明时,使用的是$r('app.media.banner_pic0'),而Image组件是无法直接读取字符串"app.media.banner_pic0"的,所以这里需要进行内容调整。

 
  1. // src/main/ets/view/Banner.ets
  2. @Component
  3. export struct Banner {
  4. @State bannerList: BannerClass[] = [];
  5. aboutToAppear(): void {
  6. this.getBannerDataFromJSON()
  7. }
  8. getBannerDataFromJSON() {
  9. //...
  10. }
  11. build() {
  12. Swiper() {
  13. ForEach(this.bannerList, (item: BannerClass) => {
  14. Image(item.imageSrc)
  15. .objectFit(ImageFit.Cover)
  16. .width('100%')
  17. .aspectRatio(656 / 288)
  18. .borderRadius(16)
  19. .padding({ top: 11, left: 16, right: 16 })
  20. }, (item: BannerClass) => item.id)
  21. }
  22. .autoPlay(true)
  23. .loop(true)
  24. .indicator(
  25. new DotIndicator()
  26. .color('#1a000000')
  27. .selectedColor('#0A59F7'))
  28. }
  29. }

No Preview

1、调整BannerClass的定义,将imageSrc的类型修改为string,以及constructor内参数imageSrc也需要修改为string类型

 
  1. // src/main/ets/model/BannerClass.ets
  2. export class BannerClass {
  3. id: string = '';
  4. imageSrc: string = '';
  5. url: string = ''
  6. constructor(id: string, imageSrc: string, url: string) {
  7. this.id = id
  8. this.imageSrc = imageSrc;
  9. this.url = url;
  10. }
  11. }

No Preview

页面上Image组件使用方式也需要修改为通过$r()来读取"app.media.banner_pic0"内容

 
  1. // src/main/ets/view/Banner.ets
  2. @Component
  3. export struct Banner {
  4. @State bannerList: BannerClass[] = [];
  5. aboutToAppear(): void {
  6. this.getBannerDataFromJSON()
  7. }
  8. getBannerDataFromJSON() {
  9. // ...
  10. }
  11. build() {
  12. Swiper() {
  13. ForEach(this.bannerList, (item: BannerClass) => {
  14. Image($r(item.imageSrc))
  15. // ...
  16. }, (item: BannerClass) => item.id)
  17. }
  18. // ...
  19. }
  20. }

No Preview

Step 8

将EnablementView中enablementList的数据改为空数组,并尝试参照步骤7,在EnablementView中获取EnablementData.json数据,此处需要改动的文件为EnablementView和ArticleClass。

 
  1. // src/main/ets/view/EnablementView.ets
  2. @Component
  3. struct EnablementView {
  4. @State enablementList: Array<ArticleClass> = [];
  5. build() {
  6. // ...
  7. }
  8. }

No Preview

注意要修改ArticleClass内imageSrc的类型,以及EnablementItem中Image的读取方式!

Step 9

将TutorialView中tutorialList的数据改为空数组,并尝试参照步骤7,在TutorialView中获取TutorialData.json数据,此处需要改动的文件为TutorialView.ets。

 
  1. // src/main/ets/view/TutorialView.ets
  2. @Component
  3. struct TutorialView {
  4. @State tutorialList: Array<ArticleClass> = [];
  5. build() {
  6. // ...
  7. }
  8. }

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文件里的内容。请用真机/模拟器测试,预览器仅支持简单页面的预览。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_shenbing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值