HarmonyOS7 ViewModel 在 ArkUI 里怎么落地:ArkUI/ArkTS 实战拆解

手绘笔记风的技术框架信息图,主题是 HarmonyOS7 ArkUI 里 ViewModel 的职责

前言

在 HarmonyOS7 项目里谈 ViewModel,很容易走两个极端:要么页面里什么都写,最后 build() 周围全是业务方法;要么一上来就做复杂分层,简单页面也要绕好几层。我更倾向于中间路线:页面只保存能驱动 UI 的状态,ViewModel 负责把原始数据整理成页面可直接展示的结构。

ViewModel 的价值不是“看起来像架构”,而是让页面少做判断。特别是用户中心、账户页、工作台这种信息密度高的页面,如果 UI 里到处写 if 和字符串拼接,后面改字段会很累。

简单判断:如果一段逻辑是在“把业务数据翻译成界面文案”,它通常更适合放进 ViewModel。

为什么 ViewModel 最容易被两边都骂

因为很多人第一次接触它时,要么觉得它太虚,要么一上来就把它做得太重。前者会说“页面里直接写不也能跑”,后者会把一个简单页面拆出一堆层,最后连改个文案都要绕来绕去。

真正难的地方不是会不会写一个 ViewModel 类,而是知道它到底该替页面解决什么问题。说白了,就是哪些逻辑应该留在页面里,哪些逻辑应该提前整理好再交给页面渲染。

手绘笔记风场景插图,展示一个 HarmonyOS7 用户中心信息面板的开发与展示场景。左侧是原始数据

所以这篇文章的重点,不是讲一个大而全的架构,而是讲一个很实用的判断标准:什么时候值得抽 ViewModel,抽出来以后边界放在哪里。

场景:用户中心信息面板

我们模拟一个用户中心页面,原始数据里有会员等级、积分、待处理任务数。页面需要展示欢迎语、会员标签、任务提示,并支持刷新。

这个页面如果不做 ViewModel,build() 里很容易出现会员等级判断、积分拼接、任务数量判断。字段不多时还能忍,一旦用户中心加上权益、签到、优惠券和认证状态,页面就会迅速变成展示规则的集合。

ViewModel 分工

层次负责内容不负责内容
ArkUI 页面渲染、交互、状态绑定拼接复杂展示文案
ViewModel数据整形、默认值、展示模型直接操作 UI 组件
数据源原始字段页面布局细节

手绘笔记风流程图,主题是 HarmonyOS7 ArkUI 页面刷新用户中心信息的完整数据流。流程从

先把页面目标想清楚

在真正写代码之前,先别急着盯着 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')
  }
}

把关键代码一段段拆开

UserProfileRawUserCenterUiState 分开。 原始数据不直接等于页面数据。比如 level 是数字,但页面需要“高级会员”;pendingTasks 是数量,但页面要的是一句可读提示。

UserCenterViewModel.buildState() 只返回结果,不碰 UI。 这让 ViewModel 更容易测试,也避免它知道 ArkUI 的布局细节。

页面只保存 uiState 页面不需要同时保存 namelevelpointspendingTasks 四个状态,因为这些字段最终都是为了展示同一个用户中心面板。

ViewModel 里适合放什么

逻辑是否适合说明
等级数字转文案适合展示规则,不属于布局
待办数量转提示适合多处可能复用同一表达
按钮点击跳转不适合页面交互更清楚
组件颜色选择视情况如果来自业务状态可放入 UI 模型
直接调用 ArkUI API不适合ViewModel 不应该依赖 UI 组件

ViewModel 不一定要很“纯”,但边界要清楚:它可以知道业务状态如何展示,不能知道页面用 Row 还是 Column

什么时候不需要 ViewModel

  • 页面只有一两个字段,且没有展示转换
  • 逻辑只服务一个按钮点击,写成私有方法就够
  • 数据不需要跨组件传递
  • 抽出来以后命名更绕、调用更长

ViewModel 落地时别做重

这个例子里,ViewModel 只做了一件事:把 UserProfileRaw 翻译成 UserCenterUiState。它没有持有页面引用,也没有调用 ArkUI API,更没有负责路由跳转。边界越小,越容易长期维护。

页面侧也要克制。UserCenterVmPage 只保存一个 uiState,刷新时重新构建一份状态并整体赋值。这样比同时维护 namelevelTextpointsTexttaskText 更清楚,字段之间也不容易不同步。

真正项目里,如果接口返回字段很多,可以先从最痛的展示转换开始抽:等级文案、积分格式、任务提示、认证状态。等这些逻辑从 build() 里移出去,页面自然会干净很多。

我的建议

如果你现在写的页面,build() 里已经开始出现文案拼接、状态判断、字段兜底这些展示规则,就可以试着先抽一层轻量 ViewModel。别一上来追求完整架构,先把最影响可读性的那部分搬出去,收益通常就已经很明显。

反过来,如果页面本来就很简单,硬拆只会让代码更绕。ViewModel 的目标从来不是“显得专业”,而是让页面更好读、更好改。

写在最后

HarmonyOS7 项目里 ViewModel 不必一开始就很重。先把“页面展示需要的数据”整理清楚,把 build() 里的判断和拼接拿出去,收益就已经很明显。等页面复杂度上来,再考虑仓库层、服务层和跨页面状态管理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值