HarmonyOS 5 一杯冰美式的时间 -- MVVM?

HarmonyOS一杯冰咖啡 ------ MVVM?

一、引言

最近看到一个项目的架构设计,刚开始看还有点懵,但仔细想了想,其实它还挺有意思的。它用了一个混合型架构,表面看像 MVVM,实际上中间加了一层 Controller(或者说是 Presenter/DisPatcher),再加上 Biz 和 Imp,把职责细分得非常明确。今天就借这个机会,跟大家一起聊聊这个架构是怎么设计的,我是怎么理解它的。阿弥陀佛。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

二、整体结构特点

来一张简图:

名义上说是 MVVM,但其实中间套了一个 Presenter 层(原来命名为 Controller),实质上形成了 View → ViewModel → Presenter → Biz → Imp 的五层结构。从职责来看,每一层都做了该做的事情,但如果不熟悉这种拆法,初看会觉得「层级很多、结构复杂」,但深入理解后,其实是一种在业务复杂度足够高场景下,合理演化出来的结果。

这里简单拆一拆职责:

  • View(UI 层) 用的是 ComponentV2,拿@Local 接收ViewModel参数,主要负责 UI 渲染和响应交互。不写业务逻辑。状态是从 ViewModel 来的。
  • ViewModel(状态管理层)@ObservedV2@Trace 声明响应式字段,View 绑定它,Presenter 来写它。ViewModel 是个纯状态类,基本没逻辑。状态的生命周期跟 UI 走,页面销毁时销毁。
  • Presenter(协调层) 真正的"控制器",但不叫 Controller,叫 Presenter或者Dispatcher 更合适。它做两件事:调业务、写状态。生命周期跟 UI 绑定,一般会用 base 类封装生命周期钩子和状态注册。大部分业务逻辑的中间流转(例如请求结果如何反馈到哪个字段)都是在这层完成的。
  • Biz(业务逻辑层) 这个层负责具体的业务封装,返回的是可观察对象(Observable、Computed、或直接 Promise)。通常还会做一些字段转换、mock 数据填充、失败兜底,甚至是缓存处理。所有 view 不关心的「但又必须做」的业务逻辑,都放这里。
  • Imp(实现层) 最底层,做网络请求、数据库、本地 IO 的那一层。也就是 data-source 封装层。

注意:这套架构的核心点是 Presenter 负责写 ViewModel,View 只读,Biz 不写 UI 状态,解耦非常清晰。

三、怎么运作的?

说白了,这套架构的本质是「解耦 + 单向流动 + 层级清晰」,我们可以从四个角度来看:

状态流(State Flow)

来一张简图:

这部分负责的是 UI 的展示更新。基本原则是:

  • UI 不持状态,所有状态都在 ViewModel;
  • ViewModel 是响应式的,写进去,UI 自动刷新;
  • 只有 Presenter(Controller)能写 ViewModel
  • ViewModel 是纯粹的数据结构,不做业务逻辑
less 复制代码
// ViewModel
@ObservedV2 class HomeViewModel {
  @State isLoading: boolean = false;
  @State list: Article[] = []
}
​
// View
@Entry
@ComponentV2
struct HomePage {
  @Local viewModel = new HomeViewModel()
​
  build() {
    Column() {
      if(this.viewModel.isLoading) {
        Text('加载中...')
      }
      List(this.viewModel.list)
    }
  }
}
​
// Presenter
class HomeController {
  constructor(private vm: HomeViewModel, private biz: HomeBiz) {}
​
  async loadData() {
    this.vm.isLoading = true
    const data = await this.biz.fetchArticles()
    this.vm.list = data
    this.vm.isLoading = false
  }
}
​

事件流(Event Flow)

1. 正向流

用户操作 → View → ViewModel → Presenter → Biz → Imp → 网络请求

2. 逆向流(响应)

网络响应 → Imp → Biz → Presenter → ViewModel → View(UI自动更新)

来一张简图:

事件流就是用户交互如何传递给业务逻辑。

  • UI 只触发事件(比如 onClick
  • Presenter 响应事件,负责"决定做什么"
  • 再由 Presenter 去调用 Biz 层
  • Biz 层可以异步处理、整合数据、兜底等
  • 最后回到 Presenter,再写入 ViewModel

生命周期联动(Lifecycle Coordination)

Presenter 和 ViewModel 都是跟随 UI 生命周期存在的。当页面销毁时,会自动释放这些状态和逻辑。(当然了你需要自己绑定生命周期)

这种结构天然防止了:

  • 定时器/任务泄漏
  • 请求未取消
  • 状态悬挂(UI 销毁后回调才回来)

此外:

  • 如果需要页面恢复自动刷新数据,可在 onPageShow 中让 Presenter 监听;
  • 如果需要在页面退出时中断请求,可在 onPageHide/onDestroy 中清理 Presenter 内部状态。

跨层交互与回调解耦

业务层(Biz)永远不会直接改 UI,也不会直接通知 ViewModel,它只暴露标准 Promise 或回调。

这种方式有几个好处:

  • Presenter 可以自由组合不同业务逻辑(灵活拼装)
  • 可以针对不同 UI 写不同 Presenter,但复用相同 Biz
  • UI 需求变了,不需要改业务逻辑,只改 Presenter 就行

错误处理机制(统一兜底)

Presenter 是所有请求的入口点,所以也是统一的错误处理中心

kotlin 复制代码
async loadData() {
  try {
    this.vm.isLoading = true
    const data = await this.biz.fetchArticles()
    this.vm.list = data
  } catch (e) {
    this.vm.errorMessage = '网络请求失败'
  } finally {
    this.vm.isLoading = false
}

异步编排能力(支持复杂请求流程)

多个接口依赖,或需要缓存/合并处理的逻辑,都在 Biz 层做掉,Presenter 不用关心细节。

kotlin 复制代码
// Biz 层
async fetchArticles(): Promise<Article[]> {
  const cache = this.cacheRepo.load()
  if (cache) return cache
​
  const netData = await this.apiRepo.getArticles()
  this.cacheRepo.save(netData)
  return netData
}

Presenter 只要管调用:

kotlin 复制代码
const data = await this.biz.fetchArticles()

过程代码

第一层:View → ViewModel
less 复制代码
@ComponentV2
export struct HomePage {
  @Local viewModel: HomeViewModel = new HomeViewModel()
  controller: HomeController = new HomeController(this.getUIContext(), this.viewModel)
}
第二层:ViewModel → Presenter/Controller
scala 复制代码
export class HomeController extends BaseController<HomeViewModel> {
  biz: HomeBiz = new HomeBiz()
  viewModel: T  // 持有ViewModel引用
}
第三层:Presenter /Controller→ Biz
kotlin 复制代码
getData() {
  const area = areaHistoryUtil.getCurArea()
  this.biz.getData(area.id, this.homeDataCancelRequest).then((result) => {
    if (result) {
      //Do something·
      this.viewModel.recommends = result.recommends
      this.viewModel.lazyMapPres = result.lazyMapPres
      this.viewModel.discoveryPres = result.discoveryPres
    }
  })
}
第四层:Biz → Imp
typescript 复制代码
public async getData(id: string, cancelRequest: CancelRequest): Promise<HomeInfo | undefined> {
  try {
    const homeInfo = await HomeImp.getHomeData(id, cancelRequest)
    return Promise.resolve(UIUtils.makeObserved(homeInfo))
  } catch (err) {
    return Promise.reject(err)
  }
}
第五层:Imp → 网络请求
typescript 复制代码
public static getHomeData(id: string, cancelRequest: CancelRequest): Promise<HomeInfo | undefined> {
  return new Promise((resolve, reject) => {
    Api.get<HomeInfo>(Net.HomeUrl, { "id": id }, cancelRequest).then((result) => {
      resolve(result)
    }).catch((err: BusinessError) => {
      reject(err)
    })
  })
}

四、好在哪?

就像三提到过的,

1. 分工明确,职责单一

  • UI 页面只负责展示和用户操作响应,不涉及任何业务逻辑;
  • ViewModel 是纯状态容器,结构轻,易 mock;
  • Presenter 只做数据流调度、生命周期管理,不关心业务细节;
  • Biz 专注业务封装和组合,不关心 UI 和状态;
  • Imp 是对 SDK / HTTP 的底层封装,适合换实现或写 UT。

职责划分清晰,就意味着每个角色都能独立演进,适合多人协作,尤其适合和 QA、测试团队协作。

2. 易测试、易 mock

  • 想测试 Presenter,可以 mock 掉 Biz 和 VM;
  • 想测试 Biz,可以 mock 掉 Imp;
  • UI 层如果够纯粹,也可以引入自动化 UI 测试。

每层的依赖都是可控制的,利于单测,适合做稳定性要求高的项目。

3. 便于团队协作、可插拔

  • 状态不共享,每一层都是独立封装;
  • VM 之间不直接通信,Presenter 是唯一协调者;
  • 页面状态逻辑清晰,适合模块复用。

换句话说,只要 API 契约稳定,底层怎么实现都能无感替换。

五、它的问题在哪?(缺点分析)

但是吧但是~各位别急着喷,问题当然也是有的!

说了那么多优点,这套架构也并不是万能的,甚至在某些场景下还会"反客为主",变成一种负担。

1. Presenter(Controller)变成"巨无霸"

虽然试图通过 Presenter 来集中管理数据流和逻辑调度,但实际上,一旦页面稍微复杂一点,Presenter 就极容易变臃肿

常见现象:

  • 写着写着 Presenter 变成了"迷你 ViewModel + 迷你 Biz"的合体;
  • 所有事件、调度逻辑、兜底、节流、节流回调,全都堆在一起;
  • 很难单测,更难复用;
  • 还动不动要关心生命周期、页面状态、异步流程等。

一旦你不小心,就会发现自己维护的是一个 500 行以上的 Controller 文件。

2. ViewModel 只是"数据搬运工"

在这个架构中,ViewModel 被约定成 "纯状态容器" ,甚至不能写逻辑(只能读写变量)。这在保持清晰上确实有用,但代价是:

  • ViewModel 的定义量变得很多;
  • 各种字段都要一一声明和维护(比如 loading、empty、error 三个状态);
  • 其实很多状态是临时性的,只为一次交互服务,却也得写进 ViewModel。

如果没有代码生成工具,这会是一件极其重复又机械的活。 (好在这个生成工具是好写的)

3. 层次多、理解门槛高,新人上手慢

第一次看这个结构,特别是没有文档的情况下,很容易出现这样的想法:

  • 为什么有 ViewModel 又有 Controller?
  • 为什么状态不直接放在页面里,而要抽出去?
  • 为什么请求不直接写在页面,而要放到 Biz?
  • 为什么请求还得通过 Presenter 走一遍?

也就是说,它比标准 MVVM 增加了一个额外维度的思考成本。一旦团队没有统一规范,新人写出来的 Presenter 和 Biz 很容易搞混甚至交叉。

六、总结一下

"每一层都很清晰,但也意味着每一层都得你写一遍。"

这套架构本质是把职责拆得非常纯粹、单一,但正因为"单一职责"做得太彻底,在某些场景下反而显得不够灵活,甚至拖慢了开发节奏。

尤其是在需求快速变动的前期,你可能会更希望结构简单直接,方便试错和反复修改。

但在一些长线项目、核心页面、多人协作密集的大模块中,这种架构会展现出它的生命力。

结构感越强、可替换性越好、职责边界越清晰,越适合交给团队中的每一个人维护。

愿我们都能找到适合自己项目节奏的架构节奏,阿弥陀佛。

七、结尾

如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧

没了。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

相关推荐
AORO20252 小时前
三防平板电脑是什么?这款三防平板支持红外测温!
5g·安全·智能手机·电脑·harmonyos
zhanshuo15 小时前
HarmonyOS 多屏适配最佳实践:基于 ArkUI 的响应式 UI 方案
harmonyos
zhanshuo15 小时前
玩转 ArkUI 拖拽功能:5 分钟搞定拖放交互与场景实战
harmonyos
shenshizhong19 小时前
鸿蒙南向开发 编写一个简单子系统
前端·harmonyos
HarmonyOS_SDK1 天前
一碰即传,重构跨设备文件分享体验
harmonyos
zhanshuo2 天前
让鸿蒙应用飞起来!ArkUI 图形渲染性能优化全攻略
harmonyos
zhanshuo2 天前
在 ArkUI 中实现丝滑嵌套滚动:让你的页面像抖音一样顺滑
harmonyos
simple_lau2 天前
鸿蒙开发中如何快速定位丢帧
harmonyos·arkts·arkui
云_杰2 天前
利用AI开发我又又上架了一个鸿蒙产品——青蓝程序员工具箱
harmonyos·trae