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 很容易搞混甚至交叉。
六、总结一下
"每一层都很清晰,但也意味着每一层都得你写一遍。"
这套架构本质是把职责拆得非常纯粹、单一,但正因为"单一职责"做得太彻底,在某些场景下反而显得不够灵活,甚至拖慢了开发节奏。
尤其是在需求快速变动的前期,你可能会更希望结构简单直接,方便试错和反复修改。
但在一些长线项目、核心页面、多人协作密集的大模块中,这种架构会展现出它的生命力。
结构感越强、可替换性越好、职责边界越清晰,越适合交给团队中的每一个人维护。
愿我们都能找到适合自己项目节奏的架构节奏,阿弥陀佛。
七、结尾
没了。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏