仓颉架构实战:解构MVVM模式——从“视图”到“视图模型”的演进
目录
-
引言:为什么在仓颉中讨论MVVM?
-
MVVM 核心三要素解析
-
仓颉的“天然”MVVM亲和性
-
实战一:轻量级实现 (View即ViewModel)
-
实战二:经典MVVM分离 (View + ViewModel)
-
深度探讨:ViewModel的职责与生命周期
-
Model层的设计:服务与数据
-
MVVM的真正优势:可测试性
-
总结:MVVM是手段,不是目的
一、引言:为什么在仓颉中讨论MVVM?
随着鸿蒙应用的复杂度不断提升,我们不可避免地会遇到一个问题:逻辑往哪里放?
在仓颉的声明式UI范式中,我们很容易将所有状态(@State)和业务逻辑(方法)都堆砌在 @Component 结构体中。对于简单的页面,这无可厚非。但当页面逻辑变得复杂时(例如:网络请求、数据处理、多状态校验),@Component 就会迅速膨胀,变得难以维护和测试。这就是所谓的“重量级视图”(Massive View)问题。
MVVM(Model-View-ViewModel)架构模式,正是解决这一问题的利器。它倡导职责分离(Separation of Concerns),将UI展示、状态与逻辑、数据模型彻底分开。
仓颉的响应式状态管理系统(@State, @Observable, @Computed 等)与MVVM的设计哲学天然契合。本文将深入探讨如何在仓颉项目中,从简单的“视图即模型”演进到经典的MVVM架构,构建可维护、可测试、可扩展的鸿蒙应用。
二、MVVM 核心三要素解析
MVVM模式将应用分为三个核心部分:
-
View (视图):
-
职责:UI的声明式描述。在仓颉中,这就是你的
@Component结构体,主要负责build()函数的实现。 -
原则:它应该是“哑巴”(Dumb)。它只负责展示数据(来自ViewModel)和转发事件(给ViewModel)。它不包含任何业务逻辑。
-
-
ViewModel (视图模型):
-
职责:View的“大脑”。它持有View所需的所有状态(State),以及响应View事件的业务逻辑(Logic/Commands)。
-
原则:它不知道 View 的存在。ViewModel 不会引用任何UI组件。它只通过其可观察的状态(
@Observable)来“通知”View更新。
-
-
Model (模型):
-
职责:应用的数据层。包括数据结构(如
struct User)和数据服务(如class ApiService,负责网络请求、数据库读写等)。 -
原则:它完全独立于UI,是可复用的业务核心。
-
[Image of MVVM Diagram: View <-> ViewModel <-> Model]
(示意图:View 观察 ViewModel 的状态;View 调用 ViewModel 的方法;ViewModel 与 Model 交互)
三、仓颉的“天然”MVVM亲和性
仓颉之所以适合MVVM,关键在于其强大的数据绑定和响应式系统:
-
View 订阅 ViewModel:
-
仓颉的
@Observable和@State装饰器使得ViewModel的class能够被View观察。 -
当ViewModel中的状态(如
@State var username)发生变化时,仓颉的运行时会自动通知订阅了该状态的View进行重渲染。这就是MVVM中 VM -> V 的单向数据流。
-
-
View 调用 ViewModel:
-
View通过事件处理器(如
onClick)调用ViewModel暴露的公共方法(如viewModel.login())。这就是 V -> VM 的事件流。
-
-
派生状态:
-
@Computed装饰器允许我们在ViewModel中根据基础状态计算出派生状态(如isLoginButtonEnabled),View可以直接绑定这个派生状态,而无需自己计算。
-
四、实战一:轻量级实现 (View即ViewModel)
在进入经典MVVM之前,我们先看看最常见的实现:@Component 自身充当了View和ViewModel的角色。
```cang见的实现:@Component 自身充当了View和ViewModel的角色。
@Component
struct SimpleCounter {
// 状态 (State)
@State var count: Int32 = 0
@State var lastUpdated: Date? = None
// 业务逻辑 (Logic)
func increment() {
this.count += 1
this.lastUpdated = Date.now()
Logger.info("Count increased")
}
func decrement() {
if (this.count > 0) {
this.count -= 1
this.lastUpdated = Date.now()
}
}
// 视图 (View)
func build() -> View {
Column(spacing: 16.0) {
Text("Count: ${this.count}")
.fontSize(32.0)
if (let time = this.lastUpdated) {
Text("Last updated: ${time.toLocalString()}")
}
Row(spacing: 12.0) {
Button("Increment") {
this.increment() // 逻辑和View耦合
}
Button("Decrement") {
this.decrement() // 逻辑和View耦合
}
}
}
}
}
分析:
* * 优点:代码集中,对于简单场景非常直观、高效。
-
缺点:职责不清。
increment和decrement是业务逻辑,但它们被硬编码在SimpleCounter这个UI组件中。如果increment需要调用API,这个View就会变得很“重”。
五、实战二:经典MVVM分离 (View + ViewModel)
现在,让我们用一个更复杂的“登录”场景来重构为经典的MVVM模式。
5.1 Model (模型)
首先定义数据结构和服务。
// Model: 数据结构
struct User {
let id: String
let username: String
let token: String
}
// Model: 数据服务 (模拟)
class AuthService {
// 模拟API调用
func login(username: String, password: String) -> Promise<User> {
return Promise<User> { (resolve, reject) =>
Timer.schedule(1500) { // 模拟1.5秒网络延迟
if (username == "Cangjie" && password == "123456") {
resolve(User(
id: "user-001",
username: "Cangjie",
token: "fake-token-string"
))
} else {
reject(Error("用户名或密码错误"))
}
}
}
}
}
5.2 ViewModel (视图模型)
这是MVVM的核心。我们创建一个 @Observable 的 class 来承载所有状态和逻辑。
// ViewModel: 视图模型
@Observable
class LoginViewModel {
// 1. 状态 (State)
@State var username: String = ""
@State var password: String = ""
@State var isLoading: Bool = false
@State var errorMessage: String? = None
@State var loginSuccess: Bool = false
// 2. 依赖 (Model)
private var authService: AuthService = AuthService()
// 3. 派生状态 (Computed State)
@Computed var isLoginButtonEnabled: Bool {
get {
// 登录按钮的可用性
return !this.username.isEmpty &&
!this.password.isEmpty &&
!this.isLoading
}
}
// 4. 业务逻辑 (Logic / Command)
func login() {
if (!this.isLoginButtonEnabled) { return }
this.isLoading = true
this.errorMessage = None
// 异步任务
Task {
try {
let user = await this.authService.login(
this.username,
this.password
)
// 登录成功
Logger.info("Login success for user: ${user.username}")
this.loginSuccess = true
// 可以在这里存储Token, 更新全局用户状态等
} catch (e: Error) {
// 登录失败
this.errorMessage = e.message
} finally {
// 结束加载
this.isLoading = false
}
}
}
func reset() {
this.username = ""
this.password = ""
this.errorMessage = None
this.isLoading = false
this.loginSuccess = false
}
}
5.3 View (视图)
View变得非常“薄”和“哑”。它只负责绑定和委托。
// View: 视图
@Component
struct LoginPage {
// 1. 持有 ViewModel 实例
// 使用 @State 确保ViewModel的生命周期与View绑定
@State var viewModel: LoginViewModel = LoginViewModel()
func build() -> View {
Column(spacing: 16.0) {
Text("欢迎登录")
.fontSize(28.0)
.fontWeight(FontWeight.Bold)
// 2. 双向绑定 (V <-> VM)
TextField(
text: $this.viewModel.username, // 使用 $ 实现双向绑定
placeholder: "用户名"
)
.disabled(this.viewModel.isLoading)
TextField(
text: $this.viewModel.password,
placeholder: "密码",
type: InputType.Password
)
.disabled(this.viewModel.isLoading)
// 3. 错误信息展示 (VM -> V)
if (let error = this.viewModel.errorMessage) {
Text(error)
.color(Color.Red)
.fontSize(14.0)
}
// 4. 事件委托 (V -> VM)
Button("登录") {
this.viewModel.login()
}
// 5. 派生状态绑定 (VM -> V)
.disabled(!this.viewModel.isLoginButtonEnabled)
.loading(this.viewModel.isLoading)
.width("100%")
// 6. 响应成功状态
if (this.viewModel.loginSuccess) {
Text("登录成功!正在跳转...")
.color(Color.Green)
// 可以在这里触发导航
// this.navigateToHome()
}
}
.padding(24.0)
}
}
分析:
-
**职责**:
LoginPage(View) 完全不关心“如何”登录,它只知道“何时”调用viewModel.login()。所有的加载状态、错误处理、API调用都在LoginViewModel(VM) 中。 -
响应式:当VM中的
isLoading变为true时,View自动重渲染,显示加载中并禁用按钮,我们无需手动操作UI。
六、深度探讨:ViewModel的职责与生命周期
6.1 ViewModel的职责边界
-
应该做:持有和管理UI状态、执行业务逻辑(表单验证、API调用)、格式化数据(如将
Date格式化为String)。 -
不应该做:
-
引用View:绝对禁止。
-
执行导航:严格来说,VM不应控制导航。它应该只设置一个状态(如
loginSuccess = true),由View(或更高层的协调器)来响应这个状态并执行导航。
-----操作UI组件*:VM不应该知道有Button或TextField的存在。
-
6.2 ViewModel的生命周期与实例化
在上述例子中,我们使用 @State var viewModel = LoginViewModel()。这意味着:
-
生命周期:ViewModel的生命周期与
LoginPage绑定。当LoginPage创建时,LoginViewModel被创建;当LoginPage销毁时,VM也被销毁。这适用于大多数页面级ViewModel。 -
共享ViewModel:如果多个View需要共享同一个ViewModel(例如:购物车、用户信息),则不应在View内部用
@State创建。而应在它们的共同祖先处创建,并通过@Provide/@Consume或其他依赖注入方式将其注入到子View中。
七、Model层的设计:服务与数据
Model层是应用的地基。在仓颉中,我们通常这样设计:
-
数据实体(Structs):使用
struct定义不可变或值类型的数据模型。它们是纯粹的数据容器。 -
服务(Classes):使用
class定义服务,如ApiService,DatabaseService,LocationService。这些服务封装了数据获取和持久化的具体实现。 -
仓库(Repository)(可选):引入Repository模式,作为ViewModel和数据服务之间的中介。ViewModel只与Repository通信,Repository负责决定是从网络还是本地缓存获取数据。
// Repository 模式示例
class UserRepository {
private var api: AuthService = AuthService()
private var cache: LocalCache = LocalCache()
func login(u: String, p: String) -> Promise<User> {
// ViewModel 只管调用 login
// Repository 负责复杂的逻辑
return this.api.login(u, p).then { user =>
this.cache.saveUser(user) // 登录成功后缓存
return user
}
}
}
// ViewModel 中
class LoginViewModel {
private var userRepository: UserRepository = UserRepository()
// ...
func login() {
// ...
await this.userRepository.login(u, p)
// ...
}
}
八、MVVM的真正优势:可测试性
分离职责的最大好处之一是可测试性。
我们可以独立地对 LoginViewModel 进行单元测试,而无需启动任何UI界面。
// 单元测试(示例)
@Test
func testLoginViewModel_LoginFails() {
// 1. 准备
let viewModel = LoginViewModel()
viewModel.username = "wrong"
viewModel.password = "user"
// 2. 执行
viewModel.login()
// 3. 断言
// (等待异步任务完成)
// ...
Assert.isNotNull(viewModel.errorMessage)
Assert.isFalse(viewModel.loginSuccess)
Assert.isFalse(viewModel.isLoading)
}
@Test
func testLoginViewModel_ButtonDisabled() {
// 1. 准备
let viewModel = LoginViewModel()
// 2. 断言 (初始状态)
Assert.isFalse(viewModel.isLoginButtonEnabled)
// 3. 执行
viewModel.username = "Cangjie"
// 4. 断言 (状态变化)
Assert.isFalse(viewModel.isLoginButtonEnabled)
// 5. 执行
viewModel.password = "123456"
// 6. 断言 (派生状态更新)
Assert.isTrue(viewModel.isLoginButtonEnabled)
}
这种测试的运行速度极快,且非常稳定,是保证应用质量的核心手段。
九、总结:MVVM是手段,不是目的
MVVM不是银弹,它是一种用于管理复杂度的工具。
-
对于简单组件:
@Component内部管理状态(实战一)完全足够,强行使用MVVM是过度设计。 -
**对于复杂页面:当状态、异步逻辑和业务规则开始混杂时(实战二),MVVM是保持代码清晰、可维护、可测试的最佳实践。
仓颉的响应式系统为MVVM提供了强大的底层支持。作为开发者,我们的“成长”体现在能准确判断何时从轻量级实现演进到经典MVVM架构,以应对不断增长的应用复杂度。
更多推荐


所有评论(0)