首页 > 其他分享 >HarmonyOS UI架构探索

HarmonyOS UI架构探索

时间:2024-02-07 10:32:15浏览次数:34  
标签:IndexViewModel Index 架构 articles viewModel HarmonyOS UI model IndexContent

作者:大李子 团队:坚果派 十年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 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: 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 })
    }
  }
}

完整代码请参考这里

本文作者:大黑布林李子

想了解更多关于鸿蒙的内容,请访问:​

​51CTO鸿蒙开发者社区

​https://ost.51cto.com/#bkwz​

标签:IndexViewModel,Index,架构,articles,viewModel,HarmonyOS,UI,model,IndexContent
From: https://blog.51cto.com/harmonyos/9634713

相关文章

  • .NET Avalonia开源、免费的桌面UI库 - SukiUI
    前言今天分享一款.NET Avalonia基于MITLicense协议开源、免费的桌面UI库:SukiUI。Avalonia介绍Avalonia是一个强大的框架,使开发人员能够使用.NET创建跨平台应用程序。它使用自己的渲染引擎绘制UI控件,确保在Windows、macOS、Linux、Android、iOS和WebAssembly等不同平台上具有......
  • 大厂聚合支付系统架构演进(上)
    0前言聚合支付主要是就是一个将所有的第三方支付,通过借助形式融合在一起,相当于对接一个支付接口,就可以使用各种支付的场景。如便利店购物,贴个码,上有微信支付,支付宝等各种支付。它主要是针对一个微小商户进行一个收款工具,让商家他那边会有一个收钱吧商户通,第一个可以实时的收听语音......
  • 一文快速了解微服务架构
    服务提供者按照一定格式的服务描述,向注册中心注册服务,声明自己能够提供哪些服务及服务的地址是什么,完成服务发布。服务消费者请求注册中心,查询所需要调用服务的地址,然后以约定的通信协议向服务提供者发起请求,得到请求结果后再按照约定的协议解析结果。在服务的调用过程中,服务的请求......
  • 单元化架构基本设计
    单元化架构基本设计关于架构分层的一些显著定义AccessLayer面向App客户端公网访问。ComputingLayer内网所有无状态的计算模块集合StorageLayer所有存储的集合关于单元化的一些显著定义单元(Set)单元的划分可以是任何维度,比如电商对的买家用户维度。单元对的......
  • harmonyOS基础(二)-简单认识UIAbility
    大家好!我是黑臂麒麟,一位6年的前端工程师;随着鸿蒙4.0的发布。鸿蒙的社区壮大,而且市场越来越对harmonyOS认可度越来越高。现很多大公司开始需要招聘鸿蒙应用开发工程师,待遇都非常好。以后中心厂跟进,也可以赶上红利;之前一直想入坑鸿蒙,但犹豫徘徊,2024不在等待,只争朝夕学,勇往直前。系统......
  • delphi GUID相关操作
    GUID相关操作代码procedureTForm1.Button1Click(Sender:TObject);varGUID1,GUID2:TGUID;begin//创建GUIDifCreateGUID(GUID1)<>0thenMemo1.Lines.Add('创建失败');//转换为字符串Memo1.Lines.Add(GUIDToString(GUID1));//分段输出Memo1.Li......
  • 通过squid将本地作为代理让不可联网的远端服务器联网
    一种方法 https://unix.stackexchange.com/questions/116191/give-server-access-to-internet-via-client-connecting-by-ssh以上方法在我这里不太行。尝试了另一种方式,连上了:1、远端服务器需要能ping到我们本地ip,windows通过ipconfig查看。2、本地的squid软......
  • 读《凤凰架构》感悟
    本书提供了一个完整的网站代码,网站名字是:凤凰书店。该网站是一个网上卖书的站点。类似于互联网最早期的亚马逊书城、当当网。一个传统的互联网网站。值得关注的是该书提供的不仅仅是勉强把这个网站实现出来的代码,而是用非常好的标准和风格实现了,并且实现了5套,让读者观察5套网站代......
  • Figma 是最受欢迎的 UI 设计工具,你认同吗?
    在UXtools最新发布的2023年设计工具调查报告中,全球几千名设计师做出了一个选择:Figma是最受欢迎的UI设计工具。它的独特之处在于其强大的协作功能、直观的界面以及不断创新的特性。*报告见:https://uxtools.co/survey/2023/Figma官网总的来说,Figma受欢迎的原因表现在以下三......
  • StringBuilder
    StringBuilder可以看成是一个容器,创建之后里面的内容是可变的。当我们在拼接字符串和反转字符串的时候会使用到1基本使用publicclassStringBuilderDemo3{publicstaticvoidmain(String[]args){//1.创建对象StringBuildersb=newStringBuilder("......