鸿蒙OS之UI架构解析

这篇具有很好参考价值的文章主要介绍了鸿蒙OS之UI架构解析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

初步布局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 })
    }
  }
}

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: string
  refreshData()
}

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。
    }
  }
}

鸿蒙OS之UI架构解析,鸿蒙,harmonyos,ui,架构,鸿蒙开发,鸿蒙系统,ArkUI,移动开发文章来源地址https://www.toymoban.com/news/detail-834239.html

到了这里,关于鸿蒙OS之UI架构解析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • HarmonyOS鸿蒙基于Java开发: Java UI 多模输入开发

    目录 约束与限制 场景介绍 接口说明 开发步骤 HarmonyOS旨在为开发者提供NUI(Natural User Interface)的交互方式。有别于传统操作系统的输入划分方式,在HarmonyOS上,我们将多种维度的输入整合在一起,开发者可以借助应用程序框架、系统自带的UI组件或API接口轻松地实现具有多

    2024年01月20日
    浏览(34)
  • 鸿蒙ArkUI即将取代Java UI?

    关注前端开发的同学,一定听过近些年,DSL 描述式的 UI 构建写法,大有取代传统命令式布局的趋势。 传统上,写一套 UI 代码,需要根据数据的逻辑,手动的在业务代码里,去改变界面 UI 元素的状态,造成业务代码和 UI 代码搅在一起,黑话=“耦合性极高”。结果就是,把人

    2024年02月11日
    浏览(34)
  • HarmonyOS鸿蒙基于Java开发: Java UI 自定义布局

    当Java UI框架提供的布局无法满足需求时,可以创建自定义布局,根据需求自定义布局规则。 Component类相关接口  表1  Component类相关接口 接口名称 作用 setEstimateSizeListener 设置测量组件的侦听器 setEstimatedSize 设置测量的宽度和高度 onEstimateSize 测量组件的大小以确定宽度和高度

    2024年02月19日
    浏览(42)
  • HarmonyOS鸿蒙基于Java开发:Java UI 常用组件 ToastDialog

    目录 接口说明 构造方法 常用方法 创建和使用 创建一个ToastDialog 设置位置 自定义ToastDialog的Component

    2024年01月19日
    浏览(33)
  • HarmonyOS鸿蒙基于Java开发:Java UI 资源文件的分类

    目录 resources目录 限定词目录 限定词目录的命名要求 限定词目录与设备状态的匹配规则 资源组目录 创建资源文件 应用的资源文件(字符串、图片、音频等)统一存放于resources目录下,便于开发者使用和维护。resources目录包括三类目录,一类为base目录与限定词目录,二类为

    2024年01月16日
    浏览(28)
  • HarmonyOS鸿蒙基于Java开发:Java UI 常用组件Switch

    目录 支持的XML属性 创建Switch 设置Switch Switch是切换单个设置开/关两种状态的组件。 Switch的共有XML属性继承自:Text Switch的自有XML属性见下表: 表1  Switch的自有XML属性 属性名称 中文描述 取值 取值说明 使用案例 text_state_on

    2024年01月18日
    浏览(38)
  • HarmonyOS鸿蒙基于Java开发:Java UI 常用组件Text

    目录 支持的XML属性 创建Text 设置Text 自动调节字体大小 跑马灯效果 场景示例 Text是用来显示字符串的组件,在界面上显示为一块文本区域。Text作为一个基本组件,有很多扩展,常见的有按钮组件Button,文本编辑组件TextField。 Text的共有XML属性继承自

    2024年01月19日
    浏览(29)
  • HarmonyOS鸿蒙基于Java开发:Java UI 常用组件 ListContainer

    目录 支持的XML属性 ListContainer的使用方法 ListContainer的常用接口 ListContainer的样式设置 ListContainer性能优化 ListContainer是用来呈现连续、多行数据的组件,包含一系列相同类型的列表项。 ListContainer的共有XML属性继承自:Component ListContainer的自有XML属性见下表: 表1  ListContainer的

    2024年01月20日
    浏览(37)
  • HarmonyOS鸿蒙基于Java开发:Java UI 常用组件 RadioButton

    目录 支持的XML属性 创建RadioButton 设置RadioButton 设置单选按钮的字体颜色 设置状态标志样式 RadioButton用于多选一的操作,需要搭配RadioContainer使用,实现单选效果。 RadioButton的共有XML属性继承自:Text RadioButton的自有XML属性见下表: 表1  RadioButton的自有XML属性

    2024年01月24日
    浏览(36)
  • HarmonyOS鸿蒙基于Java开发:Java UI 常用组件 PageSliderIndicator

    目录 PageSliderIndicator的创建和使用 PageSliderIndicator的常用方法 关联PageSlider 响应页面切换事件 设置所选导航点的页面位置 设置导航点的背景 设置导航点之间的偏移量 PageSliderIndicator,

    2024年01月16日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包