
文章目录
前言
在 HarmonyOS7 项目里谈 ViewModel,很容易走两个极端:要么页面里什么都写,最后 build() 周围全是业务方法;要么一上来就做复杂分层,简单页面也要绕好几层。我更倾向于中间路线:页面只保存能驱动 UI 的状态,ViewModel 负责把原始数据整理成页面可直接展示的结构。
ViewModel 的价值不是“看起来像架构”,而是让页面少做判断。特别是用户中心、账户页、工作台这种信息密度高的页面,如果 UI 里到处写 if 和字符串拼接,后面改字段会很累。
简单判断:如果一段逻辑是在“把业务数据翻译成界面文案”,它通常更适合放进 ViewModel。
为什么 ViewModel 最容易被两边都骂
因为很多人第一次接触它时,要么觉得它太虚,要么一上来就把它做得太重。前者会说“页面里直接写不也能跑”,后者会把一个简单页面拆出一堆层,最后连改个文案都要绕来绕去。
真正难的地方不是会不会写一个 ViewModel 类,而是知道它到底该替页面解决什么问题。说白了,就是哪些逻辑应该留在页面里,哪些逻辑应该提前整理好再交给页面渲染。

所以这篇文章的重点,不是讲一个大而全的架构,而是讲一个很实用的判断标准:什么时候值得抽 ViewModel,抽出来以后边界放在哪里。
场景:用户中心信息面板
我们模拟一个用户中心页面,原始数据里有会员等级、积分、待处理任务数。页面需要展示欢迎语、会员标签、任务提示,并支持刷新。
这个页面如果不做 ViewModel,build() 里很容易出现会员等级判断、积分拼接、任务数量判断。字段不多时还能忍,一旦用户中心加上权益、签到、优惠券和认证状态,页面就会迅速变成展示规则的集合。
ViewModel 分工
| 层次 | 负责内容 | 不负责内容 |
|---|---|---|
| ArkUI 页面 | 渲染、交互、状态绑定 | 拼接复杂展示文案 |
| ViewModel | 数据整形、默认值、展示模型 | 直接操作 UI 组件 |
| 数据源 | 原始字段 | 页面布局细节 |

先把页面目标想清楚
在真正写代码之前,先别急着盯着 API。更有用的做法是先想清楚:这个页面到底是在“展示原始数据”,还是在“展示一层加工后的界面信息”。
像用户中心这类页面,用户看到的从来不只是接口字段本身,而是欢迎语、等级名称、任务提示、认证文案这些已经被整理过的展示结果。既然页面最终要消费的是“展示结果”,那就很适合在进入 UI 之前先做一层翻译。
对小白来说,这一步特别重要。因为它能帮你判断,抽 ViewModel 是在解决真实问题,还是只是为了让结构看起来更像架构图。
完整代码示例
interface UserProfileRaw {
id: number
name: string
level: number
points: number
pendingTasks: number
verified: boolean
}
interface UserCenterUiState {
greeting: string
levelText: string
pointsText: string
taskText: string
hasPendingTask: boolean
verifyText: string
}
class UserCenterViewModel {
buildState(raw: UserProfileRaw): UserCenterUiState {
return {
greeting: `你好,${raw.name}`,
levelText: this.formatLevel(raw.level),
pointsText: `${raw.points} 积分`,
taskText: raw.pendingTasks > 0 ? `还有 ${raw.pendingTasks} 个任务待处理` : '今天没有待处理任务',
hasPendingTask: raw.pendingTasks > 0,
verifyText: raw.verified ? '已完成实名验证' : '请完善实名验证'
}
}
private formatLevel(level: number): string {
if (level >= 5) {
return '高级会员'
}
if (level >= 3) {
return '成长会员'
}
return '普通会员'
}
}
@Entry
@Component
struct UserCenterVmPage {
private viewModel: UserCenterViewModel = new UserCenterViewModel()
@State uiState: UserCenterUiState = {
greeting: '你好',
levelText: '普通会员',
pointsText: '0 积分',
taskText: '正在加载账户信息',
hasPendingTask: false,
verifyText: '验证状态加载中'
}
@State refreshCount: number = 0
aboutToAppear(): void {
this.refreshProfile()
}
private refreshProfile(): void {
const raw: UserProfileRaw = {
id: 7,
name: this.refreshCount % 2 === 0 ? '林一' : '林一同学',
level: this.refreshCount % 2 === 0 ? 5 : 3,
points: 2680 + this.refreshCount * 20,
pendingTasks: this.refreshCount % 2 === 0 ? 2 : 0,
verified: this.refreshCount % 2 === 0
}
this.uiState = this.viewModel.buildState(raw)
this.refreshCount += 1
}
build() {
Column({ space: 16 }) {
Row() {
Column({ space: 8 }) {
Text(this.uiState.greeting)
.fontSize(26)
.fontWeight(FontWeight.Bold)
Text(this.uiState.levelText)
.fontSize(14)
.fontColor('#0A59F7')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#EAF1FF')
.borderRadius(6)
}
.alignItems(HorizontalAlign.Start)
Blank()
Button('刷新')
.onClick(() => {
this.refreshProfile()
})
}
.width('100%')
Column({ space: 12 }) {
Text(this.uiState.pointsText)
.fontSize(22)
.fontWeight(FontWeight.Medium)
.width('100%')
Text(this.uiState.taskText)
.fontSize(15)
.fontColor(this.uiState.hasPendingTask ? '#B26A00' : '#2E7D32')
.width('100%')
Text(this.uiState.verifyText)
.fontSize(14)
.fontColor('#666666')
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.padding(18)
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#F6F7FB')
}
}
把关键代码一段段拆开
UserProfileRaw 和 UserCenterUiState 分开。 原始数据不直接等于页面数据。比如 level 是数字,但页面需要“高级会员”;pendingTasks 是数量,但页面要的是一句可读提示。
UserCenterViewModel.buildState() 只返回结果,不碰 UI。 这让 ViewModel 更容易测试,也避免它知道 ArkUI 的布局细节。
页面只保存 uiState。 页面不需要同时保存 name、level、points、pendingTasks 四个状态,因为这些字段最终都是为了展示同一个用户中心面板。
ViewModel 里适合放什么
| 逻辑 | 是否适合 | 说明 |
|---|---|---|
| 等级数字转文案 | 适合 | 展示规则,不属于布局 |
| 待办数量转提示 | 适合 | 多处可能复用同一表达 |
| 按钮点击跳转 | 不适合 | 页面交互更清楚 |
| 组件颜色选择 | 视情况 | 如果来自业务状态可放入 UI 模型 |
| 直接调用 ArkUI API | 不适合 | ViewModel 不应该依赖 UI 组件 |
ViewModel 不一定要很“纯”,但边界要清楚:它可以知道业务状态如何展示,不能知道页面用 Row 还是 Column。
什么时候不需要 ViewModel
- 页面只有一两个字段,且没有展示转换
- 逻辑只服务一个按钮点击,写成私有方法就够
- 数据不需要跨组件传递
- 抽出来以后命名更绕、调用更长
ViewModel 落地时别做重
这个例子里,ViewModel 只做了一件事:把 UserProfileRaw 翻译成 UserCenterUiState。它没有持有页面引用,也没有调用 ArkUI API,更没有负责路由跳转。边界越小,越容易长期维护。
页面侧也要克制。UserCenterVmPage 只保存一个 uiState,刷新时重新构建一份状态并整体赋值。这样比同时维护 name、levelText、pointsText、taskText 更清楚,字段之间也不容易不同步。
真正项目里,如果接口返回字段很多,可以先从最痛的展示转换开始抽:等级文案、积分格式、任务提示、认证状态。等这些逻辑从 build() 里移出去,页面自然会干净很多。
我的建议
如果你现在写的页面,build() 里已经开始出现文案拼接、状态判断、字段兜底这些展示规则,就可以试着先抽一层轻量 ViewModel。别一上来追求完整架构,先把最影响可读性的那部分搬出去,收益通常就已经很明显。
反过来,如果页面本来就很简单,硬拆只会让代码更绕。ViewModel 的目标从来不是“显得专业”,而是让页面更好读、更好改。
写在最后
HarmonyOS7 项目里 ViewModel 不必一开始就很重。先把“页面展示需要的数据”整理清楚,把 build() 里的判断和拼接拿出去,收益就已经很明显。等页面复杂度上来,再考虑仓库层、服务层和跨页面状态管理。

721

被折叠的 条评论
为什么被折叠?



