鸿蒙开发:以登录案例了解代码架构MVVM

前言

代码架构的出现,不仅仅是为了让代码之间结构清晰,便于维护,更是为了软件质量做底层加固,虽然说,所有的网络请求,所有的业务逻辑,我们都可以放到UI页面里来处理,在用户视觉的效果里没有什么问题,但是在开发层,长此以往,暴露的问题则是越积越深,比如页面代码臃肿,毫无代码复用可言,维护成本剧增等等问题,所以,没有架构的项目,就像一堆随意堆放的砖头和钢筋,虽然材料齐全,但风一吹就倒。

代码架构,在一个成熟且完整的项目中,是必不可少的,具体来说,架构解决了三个根本痛点:

第一、应对需求的频繁变化:产品经理今天要改UI样式,明天要换第三方SDK。如果没有分层,改一个按钮颜色可能会牵连到数据逻辑,改一个接口地址可能导致整个页面崩溃。架构提供了"隔离带",让变化只影响局部。

第二、降低认知负荷:人脑同时能处理的信息有限。如果没有分层,一个文件里既写网络请求、又写数据处理、还写UI布局,几百行代码混在一起,开发者大脑会"过载"。架构通过"关注点分离",让你看View时只关心UI,看Model时只关心数据。

第三、支持团队并行开发:在团队协作中,各业务模块相互对立,各司其职。如果没有架构,大家会在同一个文件里"打架"(合并冲突)。架构定义了清晰的契约(接口),让分工成为可能。

目前主流的代码架构就是MVVM了,可以说是很多语言或者三方架构的首选,像Android,IOS,Vue、小程序,等等,几乎都是主推MVVM,当然了鸿蒙也不例外,官方的ArkUI采用了Model-View-ViewModel(MVVM)架构模式。

什么是MVVM?

MVVM将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。通过这种模式,UI可以随着状态的变化自动更新,无需手动处理,从而高效管理数据和视图的绑定与更新。

Model:存储和管理应用数据及业务逻辑,不直接与用户界面交互。通常从后端接口获取数据,是应用程序的数据基础,确保数据的一致性和完整性。

View:负责用户界面展示数据并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据来动态更新UI。

ViewModel:负责管理UI状态和交互逻辑。作为连接Model和View的桥梁,ViewModel监控Model数据的变化,通知View更新UI,同时处理用户交互事件并转换为数据操作。

登录案例

我们就用一个登录案例,来完整的展示鸿蒙当中的mvvm架构模式。

model层

model层包含两部分,一部分是实体类对象,也就是登录之后,把接口返回的数据转换为实际开发中需要的用户数据,可能包含了,用户的ID,头像,昵称等等数据。

以下为测试数据,实际开发中,请以具体业务为主。

TypeScript 复制代码
/**
 *AUTHOR:AbnerMing
 *DATE:2026/7/23
 *INTRODUCE:登录model
 */
export class LoginModel {
  /**
   *AUTHOR:AbnerMing
   *INTRODUCE:发起登录
   */
  login(userName: string, userPass: string): UserBean {
    //在实际开发中,进行执行网络请求,把服务器返回的json数据,进行存储或者转换为用户对象
    //这里为简单模拟数据返回
    let bean = new UserBean()
    bean.userName = userName
    bean.userPass = userPass
    return bean
  }
}

/**
 *AUTHOR:AbnerMing
 *INTRODUCE:模拟的数据对象
 */
export class UserBean {
  userName?: string
  userPass?: string
}

viewmodel层

TypeScript 复制代码
import { UserBean, LoginModel } from "../model/LoginModel";
/**
 *AUTHOR:AbnerMing
 *DATE:2026/7/23
 *INTRODUCE:处理用户登录相关逻辑,存储用户信息数据,判断用户是否登录等
 */
@ObservedV2
export class LoginViewModel {
  @Trace  userBean?: UserBean

  /**
   *AUTHOR:AbnerMing
   *INTRODUCE:执行登录
   */
  login(userName: string, userPass: string) {
    let model = new LoginModel()
    this.userBean = model.login(userName, userPass)
  }

}

view层

TypeScript 复制代码
import { LoginViewModel } from '../viewmodel/LoginViewModel'

/**
 *AUTHOR:AbnerMing
 *DATE:2026/7/23
 *INTRODUCE:登录
 */
@Entry
@ComponentV2
struct LoginPage {
  @Local userName?: string
  @Local userPass?: string
  loginViewModel: LoginViewModel = new LoginViewModel()

  build() {
    Column() {
      TextInput({ placeholder: "请输入账号", text: $$this.userName })
      TextInput({ placeholder: "请输入密码", text: $$this.userPass })
        .margin({ top: 10 })

      Button("登录")
        .margin({ top: 10 })
        .onClick(() => {
          //发起登录
          if (this.userName == undefined || this.userPass == undefined) {
            this.getUIContext().getPromptAction().showToast({ message: "请输入账号或密码" })
            return
          }
          //执行登录逻辑
          this.loginViewModel.login(this.userName, this.userPass)
        })


      Text("测试渲染用户信息:账号:" + this.loginViewModel.userBean?.userName + " 密码" +
        this.loginViewModel.userBean?.userPass)
        .margin({ top: 10 })

    }.width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding({ left: 20, right: 20 })
  }
}

以上就是一个很简单的登录逻辑,由view层触发viewmodel层的登录操作,再由viewmodel层执行model层的登录请求,最后把数据返回给viewmodel层,由viewmodel层通知View更新UI。

程序包创建

mvvm并不是一成不变的,在实际的开发中,可能会有一个业务一组mvvm包,比如登录是一个业务,注册是一个业务,视频是一个业务,那么每个业务下,都会存在model、view、viewmodel三个包,这是为了便于业务结构清晰,当然也有着同模块下只有model、view、viewmodel三个包,所有的业务按照类型存在着不同的包下,这种方式减少了程序包的创建。

两种方式哪种方式好呢?对于多业务模块的项目,推荐按业务模块划分,即每个业务模块内部各自包含独立的view、model、viewmodel子包,而非全局只建一套view/model/viewmodel。这种"高内聚、低耦合"的组织方式在模块扩展、团队协作和后续拆分为HAR/HSP动态包时都更具优势。

两种方案对比

工程目录结构

TypeScript 复制代码
entry/                          # 主入口模块
├── src/main/ets/
│   ├── entryability/           # Ability入口
│   ├── pages/                  # 页面路由入口(仅引用各模块的View)
│   │
│   ├── features/               # 业务模块集合
│   │   ├── login/              # 登录模块
│   │   │   ├── view/           # 登录页面组件
│   │   │   │   ├── LoginPage.ets
│   │   │   │   └── LoginComponents.ets
│   │   │   ├── viewmodel/      # 登录状态与逻辑
│   │   │   │   └── LoginViewModel.ets
│   │   │   └── model/          # 登录数据接口与模型
│   │   │       ├── LoginRepository.ets
│   │   │       └── LoginTypes.ets
│   │   │
│   │   ├── register/           # 注册模块
│   │   │   ├── view/
│   │   │   │   └── RegisterPage.ets
│   │   │   ├── viewmodel/
│   │   │   │   └── RegisterViewModel.ets
│   │   │   └── model/
│   │   │       ├── RegisterRepository.ets
│   │   │       └── RegisterTypes.ets
│   │   │
│   │   ├── video/              # 视频模块
│   │   │   ├── view/
│   │   │   │   ├── VideoListPage.ets
│   │   │   │   ├── VideoDetailPage.ets
│   │   │   │   └── components/ # 视频模块专用UI组件
│   │   │   │       └── VideoCard.ets
│   │   │   ├── viewmodel/
│   │   │   │   ├── VideoListViewModel.ets
│   │   │   │   └── VideoDetailViewModel.ets
│   │   │   └── model/
│   │   │       ├── VideoRepository.ets
│   │   │       └── VideoTypes.ets
│   │   │
│   │   └── community/          # 社区模块
│   │       ├── view/
│   │       │   ├── CommunityPage.ets
│   │       │   └── PostDetailPage.ets
│   │       ├── viewmodel/
│   │       │   └── CommunityViewModel.ets
│   │       └── model/
│   │           ├── CommunityRepository.ets
│   │           └── CommunityTypes.ets
│   │
│   ├── common/                 # 跨模块公共层
│   │   ├── components/         # 公共UI组件(如按钮、弹窗、 loading)
│   │   ├── utils/              # 工具类(网络请求封装、格式化等)
│   │   ├── constants/          # 全局常量
│   │   └── types/              # 公共类型定义
│   │
│   └── network/                # 网络层(各模块共享的请求基础设施)
│       ├── HttpClient.ets
│       └── ApiResponse.ets

model层拆分

在实际的开发中,我们可能会经常遇到,业务逻辑代码复用的问题,比如很多个页面都需要进行某一个网络请求,或者数据操作,那么这种情况下,我们就可以对model层进行拆分,分为数据实体类,数据源,数据仓库。

Repository层示例:

TypeScript 复制代码
// models/datasource/UserApi.ets ------ 网络请求的具体实现
export class UserApi {
  async fetchUser(userId: string): Promise<User> {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    return response.json();
  }
}

// models/repositories/UserRepository.ets ------ 数据仓库,统一管理数据来源
export class UserRepository {
  private api: UserApi = new UserApi();

  async getUser(userId: string): Promise<User> {
    // 可在此加入缓存策略、错误处理等逻辑
    return await this.api.fetchUser(userId);
  }
}

ViewModel层示例:

TypeScript 复制代码
// viewmodels/HomeViewModel.ets
@ObservedV2
export class HomeViewModel {
  private repository: UserRepository = new UserRepository();
  @Trace userInfo: User | null = null;

  async loadUser(userId: string) {
    try {
      this.userInfo = await this.repository.getUser(userId);
    } catch (error) {
      // 处理错误,更新UI状态
    }
  }
}

相关总结

当然了,在实际的开发中,大家结合自己的业务,具体问题具体分析。以上的案例使用的是V2装饰器,如果你还在使用V1,可能在装饰器上有所差异,其他的大致都一样。在开发中通过正确应用Model、View和ViewModel分层的代码结构,可以使得项目在开发中提升开发效率、保证代码质量,并优化数据与UI的同步机制,简化整体开发流程。

相关推荐
用户69371750013842 小时前
从代码生产者到 AI 协作者:软件工程师的角色重构
android·前端·后端
GitLqr2 小时前
别在 Flutter 的 main() 里乱锁屏幕方向,小心 iPad 分屏功能被你搞没了
android·flutter·ios
sg_knight2 小时前
MySQL 存储过程详解:从入门到实战
android·数据库·mysql·database·dba·关系型数据库·db
爱笑鱼2 小时前
Binder(四):ioctl(BINDER_WRITE_READ) 之后,事务怎样到达目标进程?
android
AFinalStone2 小时前
Android 7系统休眠唤醒(二)开机全链路—BootROM到Launcher
android·电源管理·休眠唤醒
Mr YiRan3 小时前
Android NDK开发之统计到未被回收的图片
android
风华圆舞3 小时前
rawfile 资源与强类型词库加载器:schema / data / source 三层版本
harmonyos·arkui·resourcemanager·rawfile·arkts 编译·arkts 强类型
程序员黑豆4 小时前
鸿蒙应用开发:Grid组件实现九宫格布局教程
前端·华为·harmonyos
浮江雾4 小时前
Flutter第十七节-----路由管理(3)
android·开发语言·前端·javascript·flutter·入门