• 鸿蒙开发-HarmonyOS 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%')
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

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

    数据从哪来

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

    登录后复制

    aboutToAppear() {
      // 请求网络数据
      axios.get(url).then(response => {  
        // 更新this.articles
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

    数据请求扔给另一个类IndexViewModel

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

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

    那么Index里变成了

      @State viewModel: IndexViewModel = new IndexViewModel() 
      aboutToAppear() {
        this.viewModel.refreshData()
      }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    现在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()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    Previewer和Run的数据源隔离

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

      build() {
        Column() {
          IndexContent({ viewModel: this.viewModel })
        }
      }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    显然Index的成员这样声明

    viewModel: IndexViewModel = new IndexViewModel()
    
    
    • 1
    • 2

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

    @Prop viewModel: IndexViewModel
    
    
    • 1
    • 2

    这样,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 })
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    稍后将重构,给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 })
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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 })
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    聪明的你一定会想到,这两个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()
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    所以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 })
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    以下是IndexPreviewer的最终完整代码

    @Entry
    @Component
    struct IndexPreviewer {
      model: IndexModelBase
    
      async aboutToAppear() {
        this.model = new IndexModelMock()
        this.model.refreshData()
      }
    
      build() {
        Column() {
          IndexContent({ viewModel: this.model.viewModel })
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    7ebde29c85a3faa0ae369e2ea9dd3130.jpeg

  • 相关阅读:
    Python如何自动操作电脑桌面应用程序
    opencv之并行计算多线程parallel_for_
    Vue前端添加水印功能
    防爆等级与防爆原理概述
    精灵图练习---pink老师课程
    [附源码]Python计算机毕业设计SSM教务管理系统(程序+LW)
    常见的动态内存错误总结
    go语言实现LeetCode59 螺旋矩阵Ⅱ
    day068:字符流读、写数据,及其注意事项、flush和close方法、字符缓冲流
    python项目2to3方案预研
  • 原文地址:https://blog.csdn.net/m0_62167422/article/details/136130080