Bootstrap

【HarmonyOS——MVVM模式 | 理解MVVM模式,看这一篇就够了】

大家好,我是学徒小z,近期项目开发中遇到一些数据源放置混乱的问题,所以带来一篇MVVM模式的文章

MVVM模式

为什么要用MVVM模式

举一个例子,小明是一个开发者,学习了许多基础的知识之后,迫不及待去开发一款APP,这是他第一次开发自己的APP,没有什么经验,所以没有注重APP的项目结构,随着完成了简单的登录注册功能,小明觉得开发一个APP应该很快就能完成,但随着功能模块的增加,一些棘手的问题出现了,数据源的地方到处都有,不知道从哪个地方调用,还有一些重复的地方;想要获取某些数据,发现正常的办法根本行不通,于是使用耗费性能的办法去解决,等等。久而久之,APP最后是写完了,但想要添加新的需求,这个地方要改,那个地方也要改,一不小心,改了一大片,运行不了了。
所以MVVM模式就是要去解决这些问题的,指导开发者更容易的开发和维护产品。

对于鸿蒙中MVVM模式的疑惑

其实很多小伙伴在初次接触到鸿蒙中的MVVM都会感到一些困惑,就比如,诶,为什么找不到viewmodel层在哪里,应该在viewmodel里面放什么东西,在model里面方什么东西,为什么又说@State、@Link就是viewmodel呢等等问题,这些问题,我接下来以我的理解讲一下。

首先就是鸿蒙中的MVVM模式,分为ArkUI的MVVM模式和项目结构中的MVVM模式。这里分开讲是为了讲得清楚一些,其实它们中的viewmodel是一体的,在项目结构中写一个viewmodel层,结构能更加清晰。

ArkUI的MVVM

  • ArkUI采用了MVVM模式,其中ViewModel将数据与视图绑定在一起,更新数据的时候直接更新视图。
    也就是说,一系列装饰器本事就实现了viewmodel的能力,ArkUI中的viewmodel就是装饰器装饰的状态变量。
    关于状态管理最佳实践,本文不再细讲,要不然跑题了,感兴趣的可点击前面的链接

  • 至于model和view,和项目结构中的MVVM一起来讲

    image-20241110204554197

    image-20241110210818468

    image-20241110211809048

项目结构中的MVVM

1. 概述

  • 应用开发中,UI的更新状态需要随着数据的更新而同步更新,这种同步往往决定了应用的性能和用户体验。为了解决UI和数据同步的复杂性,ArkUI采用了Model-View-ViewModel的架构模式。通过这种模式,UI可以随着状态的变化自动更新,无需手动处理。

    • Model:处理与应用数据相关的业务逻辑和数据访问层。它通常包括数据结构、服务层调用和与数据库操作。
    • View:没有业务逻辑,尽量保持“傻瓜化”,通过数据绑定和事件监听与ViewModel交互。
    • ViewModel:负责管理UI状态和交互逻辑。作为连接Model和View的桥梁,ViewModel监控Model数据的变化,通知View更新UI,同时处理用户交互事件并转换为数据操作
  • 状态管理在MVVM模式中扮演者ViewModel的角色,向上刷新UI,向下更新数据,整体框架如下图
    image-20241110212530874

2 .分层说明

  • view

    • 页面组件,比如登录页、列表页等。页面的数据源有可能是相同的,也可能是不同的。

    • 业务组件:本身具备本APP部分业务能力的功能组件,典型的就是这个业务组件可能关联了本项目的ViewModel中的数据,不可以被共享给其他项目使用。

    • 通用组件:和内置组件一样,不会关联到ViewModel中的数据

  • ViewModel层

    • 在用户进入页面的时候,页面的有些数据不一定被访问到,所以最好讲页面数据设计成懒加载的模式。
    • 和model的区别:Model层数据是整个项目的数据,而ViewModel负责对应页面的数据,是整个APP业务数据的一部分,同时进行逻辑处理,也就是对Model的数据继续一定的处理,进行对View的渲染。
    • ViewModel层不只是存放数据,他同时需要提供数据的服务及处理,因此很多框架会以“service”来进行表达此层。
  • Model层

    • 主要负责整个应用的原始数据和数据库操作

    • 一些网络请求获取的数据放在这里,网络请求可以另写为工具类,在该层是调用工具获取原始数据

3. 架构核心原则

不可跨层访问
  • View层不可以直接调用Model层的数据,只能通过ViewModel提供的方法进行调用。
  • Model层数据,不可以直接操作UI,Model层只能通知ViewModel层数据有更新,由ViewModel层更新对应的数据
下层不可访问上层数据

下层的数据通过通知模式更新上层数据。在业务逻辑中,下层不可直接写代码去获取上层数据。如ViewModel层的逻辑处理,不能去依赖View层界面上的某个值。

举例说明一下

//错误示例,不符合MVVM原则
@Entry
@Component
export default class MyView extends View {
    private viewModel = new MyViewModel();

    @State userInput: string = '';

    build() {
        Column() {
            TextInput()
                .onChange((value) => this.userInput = value);

            Button('Process')
                .onClick(() => {
                    // ViewModel直接访问View的状态
                    this.viewModel.processInput(this.userInput);
                });
        }
    }
}

export class MyViewModel {
    processInput(input: string) {
        // 处理逻辑(假设将字符串变为大写)
        console.log('Processed:', input.toUpperCase());
    }
}

//正确示例,在MVVM中,ViewModel不应直接依赖View,而是通过数据绑定来进行交互:
@Entry
@Component
export default class MyView extends View {
    private viewModel = new MyViewModel();

    @State userInput: string = '';
    @Link processedOutput: string = this.viewModel.processedOutput;

    build() {
        Column() {
            TextInput()
                .onChange((value) => this.userInput = value);

            Button('Process')
                .onClick(() => {
                    // 调用ViewModel方法,不直接传递View的值
                    this.viewModel.processInput(this.userInput);
                });

            // 显示处理后的输出
            Text(this.processedOutput);
        }
    }
}

export class MyViewModel {
    @State processedOutput: string = '';

    processInput(input: string) {
        // 处理逻辑
        this.processedOutput = input.toUpperCase();
    }
}

非父子组件间不可直接访问

这是针对View层设计的核心原则,一个组件应该具备这样的逻辑:

  • 禁止直接访问父组件

    使用eventhub,或者通过在父组件中定义个一个回调函数,将他传给子组件A,子组件通过该函数进行数据处理,返回给父组件,父组件传给子组件B

  • 禁止直接访问兄弟组件能力。这是因为组件应该仅能访问自己看的见的子节点(通过传参)和父节点(通过事件或通知),以此完成组件之间的解耦。

    // ParentComponent.ts
    @Entry
    @Component
    export default class ParentComponent {
        @State sharedData: string = '';
    
        handleDataChange(newData: string) {
            this.sharedData = newData; // 更新状态,触发子组件的重新渲染
        }
    
        build() {
            Column() {
                ComponentA({ onDataChange: (data) => this.handleDataChange(data) })
                ComponentB({ sharedData: this.sharedData })
            }
        }
    }
    
    // ComponentA.ts
    export default class ComponentA {
        @Prop onDataChange: (data: string) => void;
    
        someMethod() {
            this.onDataChange('new value'); // 通过回调通知父组件
        }
    }
    
    // ComponentB.ts
    export default class ComponentB {
        @Prop sharedData: string; // 接收父组件的状态
    
        build() {
            Text(this.sharedData); // 渲染来自父组件的数据
        }
    }
    
    
  • 我们举一个场景例子说明

    想象一家大公司里有不同的员工和部门,每个部门可以看作一个组件,公司里的每个员工则代表组件中的逻辑或功能。为了确保公司运作高效,各部门之间需要遵循一定的沟通流程:

    总经理代表父组件,负责协调和管理整个公司。
    销售部和财务部代表两个子组件(ComponentA 和 ComponentB)。
    员工之间的交流需要遵循一定的规则,不能随意跨部门沟通。
    错误的沟通方式(违反原则的做法)

    假设销售部的员工小张需要财务部的员工小李帮忙完成一份财务报表。如果小张直接跑到小李的办公室说:“小李,帮我做这份报表”,这就是组件直接相互访问。虽然可以完成工作,但会导致以下问题:

    • 干扰:小张打断了小李的工作流程,造成工作混乱。
    • 混乱:不同部门的人随意要求会造成信息不对称,影响其他工作的协调。

    正确的沟通方式(符合解耦原则)

    • 通过总经理中转
      在规范的公司里,小张不会直接找小李,而是向总经理(父组件)报告需求:“我需要一份财务报表。”总经理会根据工作安排通知财务部,由财务部的员工小李完成这份工作。

      解释

      **销售部(ComponentA)和财务部(ComponentB)**之间没有直接联系。
      总经理(父组件)作为中间人,协调工作和信息流。
      这样做避免了部门之间的直接依赖,确保了各部门的独立性和工作有序性。

    • 通过公司公告(事件总线)
      另一种沟通方式是在公司公告栏发布信息。例如,小张将请求写在公告栏上:“需要财务报表”。任何有能力处理这件事的员工,比如小李,就会看到公告并主动响应。这类似于使用事件总线进行通信。

      解释

      小张不需要知道具体是谁来处理,只需发布请求。

      小李看到公告并进行处理,小张和小李之间没有直接沟通。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;