鸿蒙OS之UI架构解析

news/发布时间2024/5/14 10:43:32

初步布局Index

当我们新建一个工程之后,首先会进入Index页。我们先简单的做一个文章列表的显示

class Article {title?: stringdesc?: stringlink?: string
}@Entry
@Component
struct Index {@State articles: Article[] = []build() {Row() {Scroll() {Column() {ForEach(this.articles, (item: Article) => {Column() {Text(item.title).fontWeight(FontWeight.Bold)Text(item.desc)Text("----------")}}, (item: Article) => {return item.link})}.width('100%')}}.height('100%')}
}

这样,我们只要把articles里面填充数据,就能正常显示一个列表了。

数据从哪来

可以看到上面的代码里是没有数据的,只有一个空数组。我们想要从网络获取数据。那么,数据怎么来呢?最简单粗暴的写法就是在aboutToAppear()中异步发送get请求,然后更新articles数组。

aboutToAppear() {// 请求网络数据axios.get(url).then(response => {  // 更新this.articles}
}

好,现在Index界面依赖了网络库,甚至会依赖三方的axios库。在我之前一个项目中,还依赖过端云的agconnect库。于是Previewer直接报错,说因为有agconnect的依赖,Previewer编译失败。

我们可以看到Index和数据获取的逻辑强耦合在了一起。没有专注于他自身的UI布局的功能。

数据请求扔给另一个类IndexViewModel

那一堆网络请求和处理response的代码,看了就头疼。于是我们初步的设想就是把他完全丢给另一个类去处理,IndexViewModel

@Observed // 这个不能漏,当类成员变化时可以被UI监听到
export default class IndexViewModel {articles?: Array<Article>refreshData() {// 请求网络数据// 更新this.articles}
}

那么Index里变成了

  @State viewModel: IndexViewModel = new IndexViewModel() aboutToAppear() {this.viewModel.refreshData()}

现在Index只依赖一个IndexViewModel了。将来无论扩展到多少数据,统一从IndexViewModel里面读取。refreshData()里面也可以填任意多个其他的请求数据源。

可以预览了吗

我们知道,如果只布局一个固定界面,连数据都不需要,那是最简单的,预览也是没问题的。当涉及到数据的依赖,那问题就开始复杂了。Previewer的数据从哪里获得?我们知道即使现在我们把所有网络请求和数据成员都放到了IndexViewModel里面,但这也只是让Index界面没那么多代码,仅此而已。Index界面和IndexViewModel的依赖还是实实在在存在的。也就是说,Index界面还是依赖着真实的数据源,这将使未来Previewer的工作带来更多不确定性。
聪明的你一定想到了,可以写一个IndexViewModelMock类,和IndexViewModel结构一模一样,只是refreshData()里给articles赋值一个假数据。所以我们此时为了代码有条理,提取一个接口,叫IndexViewModelInterface
这样,Index里面的成员就变成了这样

// 真机运行时
@State viewModel: IndexViewModelInterface = new IndexViewModel()
// 使用Previewer时
@State viewModel: IndexViewModelInterface = new IndexViewModelMock()

现在我们又进了一步,可以用假数据预览了。但是还有手动切换数据源的操作。
哦对了,这个解决方案看似很理想,但似乎Arkts对这种结构并不支持。当@State viewModel: IndexViewModelInterface这样声明的成员,调用接口里的方法,会在运行时报错,说无法调用方法。

Previewer和Run的数据源隔离

现在我们做了很多重构,比最初的意大利面有条理很多。但手动切换终究还是不优雅,主要还是麻烦。我们能不能,只让UI布局做UI布局的事情,彻底把数据请求解耦。
声明一个struct IndexContentIndex的布局变成这样

  build() {Column() {IndexContent({ viewModel: this.viewModel })}}

显然Index的成员这样声明

viewModel: IndexViewModel = new IndexViewModel()

把之前所有的Index下的布局,放到IndexContent中,然后IndexContent的成员这样声明

@Prop viewModel: IndexViewModel

这样,Index里面包了一个IndexContent,数据的请求由Index控制,IndexContent完全被动接受数据,并进行UI布局。
运行一下,确认App可以正常运行。

那么,我们现在能预览Index了吗?不,我们只需要预览IndexPreviewer就行了。布局的本体现在在IndexPreviewer里的IndexContent里面。
新建一个struct IndexPreviewer,同样,布局里面只包含一个IndexContent

@Preview
@Component
struct IndexPreviewer {viewModel: IndexViewModel = new IndexViewModel()async aboutToAppear() {// 刷新数据}build() {IndexContent({ viewModel: this.viewModel })}
}

稍后将重构,给IndexPreviewer里面提供假数据。
这样,由于main_pages.json中定义的页面路径是"pages/Index",所以运行时会显示Index页面中的内容。预览时,不要去预览Index,只需要预览IndexPreviewer,就能快速调整布局。

分离请求和view model

还记得上文提到的ViewModelInterface不管用吗?refreshData()在接口里,运行时调用会报错。于是,我们再把articlesrefreshData()分开,refreshData()放到一个新建的类IndexModel中。
这样,IndexPreviewerIndex里面依赖的成员都是viewModel: IndexViewModel = new IndexViewModel(),而IndexModel可以继承自一个抽象类(之后会解释为什么不是接口)IndexModelBase,再创建一个IndexModelMock继承自IndexModelBase
View model中只保留状态成员的做法,参考了官方文档的 MVVM模式
至此,架构越来越明了了。
Index的完整代码如下

@Entry
@Component
struct Index {model: IndexModelInterface = new IndexModel()viewModel: IndexViewModel = new IndexViewModel()async aboutToAppear() {this.viewModel.articles = await this.model.refreshArticles()}build() {Column() {IndexContent({ viewModel: this.viewModel })}}
}

IndexPreviewer的完整代码如下

@Preview
@Component
struct IndexPreviewer {model: IndexModelInterface = new IndexModelMock()viewModel: IndexViewModel = new IndexViewModel()async aboutToAppear() {this.viewModel.articles = await this.model.refreshArticles()}build() {IndexContent({ viewModel: this.viewModel })}
}

聪明的你一定会想到,这两个struct代码大部分重复,为什么不提取一个基类。别问了,Arkts不支持。@Component struct不支持继承。

Model的实现

最终,真数据和假数据,是在Model里面区分的。
上文中,view model和model都是在界面容器(Index和IndexPreviewer)中持有的。实际上我们``能更进一步,把view model放到model里面。这样,界面容器只和model有耦合,把model里面的view model传到IndexContent里面
IndexModelBase的代码如下

export default abstract class IndexModelBase {abstract refreshArticles(): Promise<Article[]>viewModel: IndexViewModel = new IndexViewModel()async refreshData() {this.viewModel.articles = await this.refreshArticles()}
}

所以IndexModelBase不声明为接口,因为要持有view model,并对里面的articles进行更新。
接下来,让IndexModelBase的子类去实现具体的refreshArticles()方法。IndexModel中,通过网络请求获取数据,更新articlesIndexModelMock中,硬编码假数据给articles
在上文的两个界面容器中,更新数据变得更简单。
以下是Index的最终完整代码

@Entry
@Component
struct Index {model: IndexModelBaseasync aboutToAppear() {this.model = new IndexModel()this.model.refreshData()}build() {Column() {IndexContent({ viewModel: this.model.viewModel })}}
}

以下是IndexPreviewer的最终完整代码

@Entry
@Component
struct IndexPreviewer {model: IndexModelBaseasync aboutToAppear() {this.model = new IndexModelMock()this.model.refreshData()}build() {Column() {IndexContent({ viewModel: this.model.viewModel })}}
}

IndexUI里的内容提出来,创建一个IndexContent,然后用Index去包一层,用于真实数据运行。另外再创建一个IndexPreviewer包一层IndexContent用于预览。
这样的话IndexContent只依赖一个状态变量viewModel,所有的数据和刷新操作都在viewModel里面。然后让Index传入带真实数据的IndexViewModel,让IndexPreviewer传入带假数据的IndexViewModelMock,这两个view model用一个接口或基类来控制它们的实现。
这里补充一下为什么之前这样设计架构会遇到刷新方法调用报错。经过对比调试,发现当IndexIndexPreviewer(以下简称界面容器)传view model的时候,不能在参数里面传new的实例。必须让界面容器先持有view model,声明一个成员,再把这个成员传进去。
那么接下来就开始动刀吧,把繁琐的架构简化一下。

去掉本不愿意加的model层

上一版的设计是让model层去进行数据获取,然后model层有接口,子类去决定使用真数据还是假数据。现在它们已经不需要了。但是删之前先等会,代码还在里面。
先给IndexViewModel创建一个接口IndexViewModelInterface

登录后复制

export default interface IndexViewModelInterface {articles: Array<Article>title: stringrefreshData()
}

IndexViewModel继承该接口

@Observed
export default class IndexViewModel implements IndexViewModelInterface {articles: Array<Article> = []title: string = "1"async refreshData(): Promise<void> {this.articles = await this.refreshArticles()this.title = "2"return new Promise(resolve => { resolve() })}async refreshArticles(): Promise<Article[]> {let articles: Array<Article>// 异步请求,返回文章列表return new Promise(resolve => {resolve(articles)})}
}

声明一个IndexViewModelMock继承该接口

@Observed
export default class IndexViewModelMock implements IndexViewModelInterface {articles: Array<Article> = []title: string = "1"async refreshData(): Promise<void> {this.articles = await this.refreshArticles()this.title = "2"return new Promise(resolve => { resolve() })}refreshArticles(): Promise<Article[]> {return new Promise(resolve => {const articles = [/*硬编码的文章假数据*/]resolve(articles)})}
}

好了,之前的model类现在可以删掉了。

界面容器的更改

Index界面使用真数据,这是更改后的代码

@Entry
@Component
struct Index {viewModel: IndexViewModelInterface = new IndexViewModel()async aboutToAppear() {this.viewModel.refreshData()}build() {Column() {IndexContent({ viewModel: this.viewModel })}}
}

IndexPreviewer使用假数据,这是更改后的代码

@Entry
@Component
struct IndexPreviewer {viewModel: IndexViewModelInterface = new IndexViewModelMock()async aboutToAppear() {this.viewModel.refreshData()}build() {Column() {IndexContent({ viewModel: this.viewModel })}}
}

此处用Index再示范一下,之前调用刷新方法报错的时候是怎么写的

@Entry
@Component
struct Index {build() {Column() {IndexContent({ viewModel: new IndexViewModel() }) // 传入一个未被本实例持有的实例,并在IndexContent里调用refreshData(),将报错并crash。}}
}

765e5079845b5bb77f2aa30a2da70670.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.bcls.cn/Zlue/2209.shtml

如若内容造成侵权/违法违规/事实不符,请联系编程老四网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

开发Chrome插件,background.js中log打印未出现在控制台

不同于内容脚本&#xff08;通常命名content.js&#xff09;&#xff0c;在后台脚本&#xff08;通常命名background.js或service-worker.js&#xff09;中console.log并不会在控制台中直接显示。 要查看后台脚本上下文的正确控制台&#xff0c;执行如下步骤&#xff1a; 访问…

【Wio Terminal】输入/输出

输入/输出 一、概述1、硬件原理图Terminal引脚分布及功能Wio Terminal Grove端口引脚分配 二、使用Wio Terminal上的Grove模拟端口1、RPI 模拟引脚2、Grove引脚配置3、示例 三、使用 Wio Terminal上的Grove数字端口1、RPI 数字引脚2、Grove引脚配置将 Grove I2C 端口用作数字端…

华为笔记本原厂系统镜像恢复安装教程方法

1.安装方法有两种&#xff0c;一种是用PE安装&#xff0c;一种是华为工厂包安装&#xff08;安装完成自带F10智能还原&#xff09; 若没有原装系统文件&#xff0c;请在这里远程恢复安装&#xff1a;https://pan.baidu.com/s/166gtt2okmMmuPUL1Fo3Gpg?pwdm64f 提取码:m64f …

S-35390A计时芯片介绍及开发方案

计时芯片 S-35390A芯片是计时芯片&#xff0c;一般用来计算时间。低功耗&#xff0c;宽电压&#xff0c;受温度影响小&#xff0c;适用于很多电路。它有一个问题&#xff0c;不阻止用户设置不存在的时间&#xff0c;设置进去之后计时或者闹钟定时会出错。 规格书阅读 首先我…

并查集例题(连通块中点的数量)C++(Acwing)

代码&#xff1a; #include <iostream>using namespace std;const int N 100010;int n, m; int p[N], cnt[N];int find(int x) {if (p[x] ! x) p[x] find(p[x]);//优化return p[x]; }int main() {cin >> n >> m;for (int i 1; i < n; i ){p[i] i;cn…

公司新招了个腾讯拿38K的人,让我见识到了什么才是测试天花板

5年测试&#xff0c;应该是能达到资深测试的水准&#xff0c;即不仅能熟练地开发业务&#xff0c;而且还能熟悉项目开发&#xff0c;测试&#xff0c;调试和发布的流程&#xff0c;而且还应该能全面掌握数据库等方面的技能&#xff0c;如果技能再高些的话&#xff0c;甚至熟悉分…

力扣45. 跳跃游戏 II

Problem: 45. 跳跃游戏 II 文章目录 题目描述思路复杂度Code 题目描述 思路 Problem: 55.跳跃游戏 该题在上述的基础上&#xff0c;我们每次先求取当前可跳区间内的最远距离farthest;每当走到当前的区间胃部时&#xff08;end i&#xff09;&#xff1a;跳跃步数加一(jumps)&a…

各种手型都合适,功能高度可定制,雷柏VT9PRO mini和VT9PRO游戏鼠标上手

去年雷柏推出了一系列支持4KHz回报率的鼠标&#xff0c;有着非常敏捷的反应速度&#xff0c;在游戏中操作体验十分出色。尤其是这系列4K鼠标不仅型号丰富&#xff0c;而且对玩家的操作习惯、手型适应也很好&#xff0c;像是VT9系列就主打轻巧&#xff0c;还有专门针对小手用户的…

从故宫修建看「软件物料清单」的重要性 @安全历史01

故宫&#xff0c;这座中国传统文化的重要代表和象征性建筑已屹立近600年&#xff0c;是世界上现存规模最大、保存最为完整的木质结构古建筑之一。 故宫之所以能至今保存完好&#xff0c;除持续保护和修缮外&#xff0c;其使用的木材和砖石等材料也经过了精挑细选&#xff0c;保…

【进程创建】

目录 进程创建的方式查看进程pid 调用系统调用创建子进程fock函数做了的工作子进程刚开始创建的状态 一个变量&#xff0c;两个不同的值 进程创建的方式 1.在操作系统上输入的指令。 2.已经启动的软件。 3.程序员在代码层面上调用系统调用创建进程。 linux中第一个创建的进程是…

JAVA电商平台 免 费 搭 建 B2B2C商城系统 多用户商城系统 直播带货 新零售商城 o2o商城 电子商务 拼团商城 分销商城

涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、Redis …

深度解析Sora的核心技术

Sora要解决的核心问题 Sora面临的挑战是将不同类型的视觉信息&#xff0c;如视频、文本、图像和声音等&#xff0c;整合为一种共同的表征形式。这种转换是实现统一训练过程的关键&#xff0c;旨在将各类数据集中到一个训练框架中&#xff0c;以便于进行大规模的统一学习。简而…

HTTPS网络通信协议基础

目录 前言&#xff1a; 1.HTTPS协议理论 1.1协议概念 1.2加密 2.两类加密 2.1对称加密 2.2非对称加密 3.引入“证书” 3.1证书概念 3.2数据证书内容 3.3数据签名 4.总结 前言&#xff1a; 了解完HTTP协议后&#xff0c;HTTPS协议是HTTP协议的升级加强版&#xff0c…

二进制方式安装MySQL并备份数据库

一、openEuler二进制方式安装MySQL 8.0.28版本 1.1 获取软件包 [rootopenEuler3 ~]# wget -c https://mirrors.aliyun.com/mysql/MySQL-8.0/mysql-8.0.28-linux-glibc2.12-x86_64.tar.xz 1.2 解压软件包 [rootopenEuler3 ~]# dnf install -y tar xz [rootopenEuler3 ~]# t…

SpringBoot+WebSocket实现即时通讯(二)

前言 紧接着上文《SpringBootWebSocket实现即时通讯&#xff08;一&#xff09;》 本博客姊妹篇 SpringBootWebSocket实现即时通讯&#xff08;一&#xff09;SpringBootWebSocket实现即时通讯&#xff08;二&#xff09;SpringBootWebSocket实现即时通讯&#xff08;三&…

基于python-socket构建任务服务器(基于socket发送指令创建、停止任务)

在实现ia业务服务器时需要构建一个python-socket客户端&#xff0c;1、要求能与服务器保持心跳连接&#xff0c;每10秒钟发送一次心跳信号&#xff1b;2、要求能根据socket服务器发送的指令创建或终止一个定时任务。 为此以3个类实现该功能&#xff0c;分别为socket通信类&…

【鸿蒙系统学习笔记】TypeScript开发语言

一、背景 HarmonyOS 应用的主要开发语言是 ArkTS&#xff0c;它由 TypeScript&#xff08;简称TS&#xff09;扩展而来&#xff0c;在继承TypeScript语法的基础上进行了一系列优化&#xff0c;使开发者能够以更简洁、更自然的方式开发应用。值得注意的是&#xff0c;TypeScrip…

CSS基础属性

【三】基础属性 【1】高度和宽度 &#xff08;1&#xff09;参数 width&#xff08;宽度&#xff09;&#xff1a;用于设置元素的宽度。可以使用具体的数值&#xff08;如像素值&#xff09;或百分比来指定宽度。 height&#xff08;高度&#xff09;&#xff1a;用于设置元…

深信服技术认证“SCCA-C”划重点:深信服超融合HCI

为帮助大家更加系统化地学习云计算知识&#xff0c;高效通过云计算工程师认证&#xff0c;深信服特推出“SCCA-C认证备考秘笈”&#xff0c;共十期内容。“考试重点”内容框架&#xff0c;帮助大家快速get重点知识 划重点来啦 *点击图片放大展示 深信服云计算认证&#xff08;S…

Android 沉浸式状态栏

过时的API //设置默认隐藏虚拟按键&#xff0c;虚拟按键显示后为半透明protected open fun hideNavigationBarAndFullScreen() {val flags: Int// This work only for android 4.4flags if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {// This work only for a…
推荐文章