HarmonyOS UI可预览架构探索 原创
作者:大李子
团队:坚果派
十年iOS,All in转鸿蒙
2024年2月4日更新:
架构更新了,请参考最新的帖子《HarmonyOS UI可预览架构探索(续)》
2024年2月2日更新:
感谢wx65b0afa1cee7b的留言。他提出的做法,我曾经实践过,但之前失败了。然而今天我又尝试了一下,好像有新的发现,应该可以成功。这也是我最初的设计,当时遇到了一些奇怪的问题,只能作罢,想了另一套方案,就是本文介绍的架构。我需要再整理一下代码,仔细读一读文档,抽时间再优化一下这个UI的架构。
前言
现在的鸿蒙入门教程,可以说是满天飞,一个简单的demo实践分分钟就能做出来,有手就行。但是项目如果要做大,就不得不开始考虑架构的问题。比如,如何合理地管理代码结构,合理地解耦。
本文将探索,如何把现有常用的架构理论和Arkts,ArkUI结合起来,使代码更有条理。当然,最重要的是发挥Previewer的优势,用Previewer快速调整布局,并同时能在不改变代码的情况下,直接运行可以显示真实数据。
开发环境:
Windows 11
DevEco Studio 4.0 Release
Build Version: 4.0.0.600, built on October 17, 2023
运行环境:
华为畅享50Pro
HarmonyOS 4.0 API9
初步布局Index
当我们新建一个工程之后,首先会进入Index页。我们先简单的做一个文章列表的显示
class Article {
title?: string
desc?: string
link?: 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 IndexContent
,Index
的布局变成这样
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()
在接口里,运行时调用会报错。于是,我们再把articles
和refreshData()
分开,refreshData()
放到一个新建的类IndexModel
中。
这样,IndexPreviewer
和Index
里面依赖的成员都是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
中,通过网络请求获取数据,更新articles
,IndexModelMock
中,硬编码假数据给articles
。
在上文的两个界面容器中,更新数据变得更简单。
以下是Index
的最终完整代码
@Entry
@Component
struct Index {
model: IndexModelBase
async aboutToAppear() {
this.model = new IndexModel()
this.model.refreshData()
}
build() {
Column() {
IndexContent({ viewModel: this.model.viewModel })
}
}
}
以下是IndexPreviewer
的最终完整代码
@Entry
@Component
struct IndexPreviewer {
model: IndexModelBase
async aboutToAppear() {
this.model = new IndexModelMock()
this.model.refreshData()
}
build() {
Column() {
IndexContent({ viewModel: this.model.viewModel })
}
}
}
完整代码请参考这里
不错不错,祝大佬在鸿蒙事业顺利
谢谢支持!
为什么不把请求数据放在viewModel呢,按照mvvm的模式,数据获取都是放在viewmodel层的,而model的数据格式是固定的。
可以将modelbase这一套放在viewmodel,定义一个抽象类,然后搞两个viewmodel继承抽象类拉不同的数据,model不用改
在previewer:
viewModel: IndexViewModelBase = new IndexViewMockModel()
在index:
viewModel: IndexViewModelBase = new IndexViewModel()
拉数据的方法由子类重写即可
你说得对,这是通常的做法。不过我声明了IndexViewModelBase ,无论是接口还是抽象类,当在子类调用刷新数据的方法的时候,会报错,无法执行方法。
起初的设想是我把数据和刷新放在view model里,所以给IndexViewModelBase的子类加了@Observed。这样view model里的成员都能被UI监听到更新。不过这样行不通。
子类调用抽象类定义的方法会报错,算是arkts的语法规定还是这算是个bug?
起初的设想是我把数据和刷新放在view model里,所以给IndexViewModelBase的子类加了@Observed。这样view model里的成员都能被UI监听到更新。不过这样行不通。
这段话不太理解,为什么要给viewmodel加observed呢,observed个人理解用来修饰model所属的class,然后在Index跟IndexPreviewer:@State listData: ListData[] = viewmodel.getData(),在viewmodel声明一个属性: private listData: ListData[] = ListData,同时声明获取数据的方法getData(): ListData[] { return this.listData }与更新数据的方法async refreshData() { await this.listData = ... },在IndexContent:@Link listData: ListData[], 再从Index或IndexPreviewer传进去, 试试这样还会报错不
这个我之前试下来,是@Observed导致的
谢谢大佬,继承确实不好使
这样的话,从架构角度来说,viewModel里面每个属性都和UI关联状态更新的话,耦合太多了。
我考虑的是之后不止一个listData的数据,还会有其他的数据,这样的话未来每个数据都要和UI关联,耦合太多了。所以打算把整个view model和UI关联起来,让UI统一监听view model里面成员的变化。