【鸿蒙原生开发会议随记 Pro】拒绝面条代码 基于 MVVM 的代码架构与状态管理选型

文章目录

    • 一、 为什么要折腾 MVVM?从面条代码的痛点说起
    • 二、 鸿蒙状态管理的三剑客 State、Prop 与 Link
    • 三、 封装 BaseViewModel
    • 四、 构建录音页面的 MVVM 脚手架
    • 五、 总结

在前两篇文章中,我们像产品经理一样规划了“会议随记 Pro”的商业蓝图,又像架构师一样搭建了分层的工程目录。现在,我们要正式进入代码的世界。但在我们写下第一个 UI 组件之前,我必须和你聊聊一个可能会决定项目生死的关键话题:代码架构。

你一定有过这样的经历,接手一个老项目,打开一个名为MeetingDetail.ets的文件,发现里面洋洋洒洒写了两千行代码。UI 布局、网络请求、数据库操作、甚至还有复杂的业务算法,全部纠缠在一起。你想修改一个按钮的颜色,结果不小心删掉了一行逻辑代码,导致整个页面无法加载数据。

这种代码,我们形象地称之为面条代码。它们纠缠不清,不仅难以维护,更是 Bug 的温床。

今天这篇文章,我们要彻底解决这个问题。我们将引入MVVM(Model-View-ViewModel)架构模式,并结合 HarmonyOS NEXT 特有的状态管理机制,为我们的应用打造一套坚固的代码脚手架。我们要明确一个核心原则:让 UI 只负责渲染,让逻辑只负责思考

一、 为什么要折腾 MVVM?从面条代码的痛点说起

在传统的开发模式或者早期的 FA 模型中,我们往往习惯于在 UI 组件的生命周期里直接写业务逻辑。比如在aboutToAppear方法里,你可能会直接调用http.createHttp().request(...),然后在回调里拿到数据,直接赋值给@State变量。这看起来很直观,写起来也快。

但请 思考一下我们的“会议随记 Pro”的录音页面。这个页面非常复杂,它不仅要显示波形图(UI),还要实时计算时长(逻辑),要监听麦克风的状态(逻辑),要计算当前的会议成本(逻辑),还得在录音结束时把数据写入数据库(数据层)。

如果你把这一切都写在RecordPage.ets这个 UI 文件里,这个文件很快就会膨胀到不可阅读的程度。

当我们采用 MVVM 模式时,情况就完全不同了。我们将代码拆分为三个部分。Model(模型层)是我们在上一篇中定义好的MeetingProject等数据结构,它们代表了数据的真实形态。View(视图层)是我们用 ArkUI 写的界面,它只负责长得好看,比如按钮是圆的还是方的,文字是红的还是黑的。而连接这两者的桥梁,就是ViewModel(视图模型层)

ViewModel 是 UI 的管家。它持有 UI 所需的数据状态,并暴露操作这些数据的方法。比如,View 层有一个“开始录音”的按钮,View 不会自己去调用麦克风,它只会告诉 ViewModel:“嘿,用户想录音了”。ViewModel 收到指令,去调用底层的录音服务,更新“正在录音”的状态。View 监听到状态变化,自动把按钮图标从“播放”变成“暂停”。

这种单向数据流状态驱动的开发模式,是 HarmonyOS 开发的精髓。不仅让代码逻辑清晰,更重要的是方便测试。你甚至可以在不启动 UI 的情况下,单独对 ViewModel 进行单元测试,这在商业级项目的开发中是至关重要的。

二、 鸿蒙状态管理的三剑客 State、Prop 与 Link

在构建 MVVM 之前,我们必须先精通手中的武器。HarmonyOS NEXT 提供了一套强大的状态管理机制,其中最常用的就是@State@Prop@Link。很多开发者只是机械地使用它们,却不理解它们在架构中的真正含义。

让我们结合“会议随记 Pro”的场景来深度剖析一下。

@State,是组件内部的灵魂。当你把一个变量标记为@State,你就告诉了编译器:这个变量是我的私有财产,但我允许 UI 监听它的变化。一旦它变了,用到它的 UI 必须自动刷新。在本应用中,BaseViewModel里定义的isLoading状态,或者录音页面里当前的duration(时长),都属于这种类型。它们不需要外界干涉,完全由组件或 ViewModel 内部控制。

@Prop,是一种单向的父子传递。想象一下,我们在首页有一个“会议列表”,列表里有一个个“会议卡片”组件。父组件(列表)把会议标题传给子组件(卡片)。这时候,子组件里的标题变量就应该用@Prop修饰。这意味着:父组件改了标题,子组件会跟着变;但子组件如果自己改了标题,是不会影响父组件的,而且下一次父组件更新时,子组件的修改会被覆盖。这就像是上级给下级下达命令,下级只能执行,不能反向修改命令本身。在我们的项目中,通用的展示型组件,比如“项目颜色标签”,就大量使用@Prop

@Link,是双向同步的强力胶水。比如我们在“设置页”里封装了一个“时薪设置组件”。父页面持有“当前时薪”这个数据,子组件(设置组件)不仅要显示它,还要允许用户修改它。当用户在子组件里输入了新的金额,父页面里的数据必须同步更新,以便进行存储。这时候,我们就必须使用@Link。它让父子组件共享了对同一份数据的读写权限。但请注意,@Link不能滥用,因为它破坏了单向数据流的封闭性,如果传递层级过深,会让数据流向变得难以追踪。我们只在那些确实需要双向控制的场景下使用它。

除了这三剑客,还有一个针对复杂对象的@Observed@ObjectLink。我们的Meeting对象是一个嵌套了多层属性的类。如果我们在数组里修改了某个 Meeting 的title属性,普通的@State数组可能监听不到这个深层的变化。这时候,我们需要把 Meeting 类标记为@Observed,并在子组件中用@ObjectLink接收它。这是很多新手容易踩坑的地方,也是导致“明明改了数据,界面却不刷新”的罪魁祸首。

三、 封装 BaseViewModel

理解了理论,我们开始动手。在写具体的业务 ViewModel 之前,我们需要一个基类来处理通用的逻辑。

在一个商业 App 中,几乎每个页面都面临三个共性问题:加载中(Loading)的状态管理、错误信息(Error)的提示、以及异步任务(Async Task)的异常捕获。如果我们把isLoading = truetry-catch复制粘贴到每一个方法里,那简直是重复劳动的地狱。

我们需要封装一个BaseViewModel

请在entry/src/main/ets/commons/base目录下新建BaseViewModel.ts。我们不需要它继承任何系统类,它就是一个纯粹的 TypeScript 类,但我们要利用好泛型和异步封装。

你看,我们这样做。首先定义两个基础状态:loadingerror

// entry/src/main/ets/commons/base/BaseViewModel.ts /** * 所有 ViewModel 的基类 * 负责统一处理 Loading 状态、错误捕获和资源释放 */ export class BaseViewModel { /** * 页面是否正在加载数据 * UI 层可以监听这个变量来显示/隐藏 Loading 组件 */ isLoading: boolean = false; /** * 错误信息提示 * 当发生异常时,这里会被赋值,UI 层可以监听并弹出 Toast */ errorMessage: string = ''; /** * 统一的异步任务执行器 * 自动管理 isLoading 的状态切换,并自动捕获异常 * * @param task 需要执行的异步函数 * @param onError 可选的错误回调,如果需要特殊处理错误可传入 */ async launch<T>(task: () => Promise<T>, onError?: (e: Error) => void): Promise<T | null> { try { // 任务开始前,自动显示 Loading this.isLoading = true; this.errorMessage = ''; // 执行真正的业务逻辑 const result = await task(); return result; } catch (error) { // 统一的错误日志打印 console.error(`[ViewModel] Task Failed: ${JSON.stringify(error)}`); // 更新错误状态 this.errorMessage = error.message || '未知错误,请重试'; // 如果调用者提供了自定义错误处理,则执行 if (onError) { onError(error as Error); } return null; } finally { // 无论成功还是失败,任务结束时自动关闭 Loading this.isLoading = false; } } }

但是,如果没有这个封装,我们在“获取会议列表”时,代码可能是这样的:

// 不好的写法 async getMeetings() { this.isLoading = true; try { const list = await db.query(); this.list = list; } catch (e) { this.errorMessage = e.message; } finally { this.isLoading = false; } }

有了BaseViewModel,我们的代码就变成了这样:

// 优雅的写法 async getMeetings() { await this.launch(async () => { this.list = await db.query(); }); }

代码量减少了一半,而且逻辑更加聚焦于业务本身,而不是状态的维护。这就是架构设计的魅力。

四、 构建录音页面的 MVVM 脚手架

有了基类,我们现在来实战构建 录音页面 RecordPage 的 MVVM 结构。这将是我们 App 中最复杂的页面之一,用它来练手最合适不过。

我们需要创建两个文件:RecordViewModel.tsRecordPage.ets

首先是RecordViewModel。它继承自BaseViewModel。它需要持有录音时长、当前分贝值(用于波形图)、以及录音状态。注意,这里我们不使用@State装饰器,因为 ViewModel 本身是一个普通的类。我们将依靠 View 层来实例化它,并利用 View 层的状态机制来驱动更新。为了保持兼容性和易理解性,我们采用 View 持有 State,ViewModel 操作数据 的模式。

// entry/src/main/ets/features/record/viewmodel/RecordViewModel.ts import { BaseViewModel } from '../../../commons/base/BaseViewModel'; export class RecordViewModel extends BaseViewModel { // 录音时长(秒) duration: number = 0; // 当前录音状态:idle, recording, paused recordStatus: 'idle' | 'recording' | 'paused' = 'idle'; // 模拟一个定时器引用 private timer: number = -1; /** * 开始录音 * 这里的逻辑纯粹是状态的流转和底层服务的调用 */ async startRecording() { await this.launch(async () => { // 1. 调用底层 AudioService 开始录音 (后续章节实现) // await AudioService.start(); // 2. 更新状态 this.recordStatus = 'recording'; // 3. 开启计时器 this.startTimer(); }); } /** * 停止录音 */ async stopRecording() { await this.launch(async () => { // await AudioService.stop(); this.recordStatus = 'idle'; this.stopTimer(); // 这里未来会加入写入数据库的逻辑 }); } private startTimer() { this.stopTimer(); this.timer = setInterval(() => { this.duration++; // 在这里,我们其实修改了类内部的属性 // UI 层需要一种机制来感知这个 duration 的变化 }, 1000); } private stopTimer() { if (this.timer !== -1) { clearInterval(this.timer); this.timer = -1; } } }

RecordPage,是 View 层。这里的关键在于如何让 UI 感知到 ViewModel 中duration的变化。

在 HarmonyOS 开发中,如果不使用 V2 版本的@Observable,普通的类属性变化是不会自动触发 UI 刷新的。所以,我们通常会在 View 层定义@State变量,并通过 ViewModel 的方法来更新这些@State变量,或者让 ViewModel 返回一个可观察的对象。

但在更现代的开发实践中,为了让代码更整洁,我们推荐一种状态代理的模式。即 ViewModel 负责计算,View 负责存储状态。或者,我们让 ViewModel 本身成为一个被@State管理的对象(但这需要 ViewModel 是 Struct 或者是被深度监听的 Class)。

为了降低理解门槛,我们这里采用最稳妥的ViewModel 驱动 View 状态的方式。但为了让大家体验更高级的写法,我这里演示一种基于Callback或者UIState对象的模式。

不过,最直白的方式其实是在 View 中把 ViewModel 标记为 @State。这要求 ViewModel 必须是一个可以被观察的对象。在 ArkTS 中,我们可以简单地把 ViewModel 的属性通过 View 的方法暴露出来,或者在 View 中定义对应的@State变量,在 ViewModel 的回调中更新它们。

让我们看一个结合了实际可行性的代码架构。我们在 View 中定义状态,ViewModel 只是纯逻辑类。

// entry/src/main/ets/features/record/pages/RecordPage.ets import { RecordViewModel } from '../viewmodel/RecordViewModel'; @Entry @Component struct RecordPage { // 1. 实例化 ViewModel // 注意:这里没有用 @State 修饰 vm,因为 vm 本身的方法不直接触发 UI 刷新 // 我们用单独的 @State 变量来驱动 UI private vm: RecordViewModel = new RecordViewModel(); // 2. 定义 UI 状态 @State duration: number = 0; @State status: string = 'idle'; @State isLoading: boolean = false; aboutToAppear() { // 可以在这里做一些初始化 } build() { Column() { // 顶部导航栏 Text('正在录音') .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ top: 20, bottom: 20 }) // 核心内容区:显示时长 // 这里使用了 formatTime 工具函数(假设在 commons 里) Text(this.formatDuration(this.duration)) .fontSize(60) .fontColor($r('app.color.brand_color')) .fontWeight(FontWeight.Bold) // 状态显示 Text(`当前状态: ${this.status}`) .fontSize(16) .fontColor($r('app.color.text_secondary')) .margin({ top: 10 }) // 底部操作区 Row() { if (this.status === 'recording') { Button('停止') .onClick(() => { this.handleStop(); }) .backgroundColor(Color.Red) } else { Button('开始') .onClick(() => { this.handleStart(); }) .backgroundColor($r('app.color.brand_color')) } } .width('100%') .justifyContent(FlexAlign.Center) .margin({ top: 50 }) // Loading 遮罩 if (this.isLoading) { LoadingProgress() .width(50) .height(50) } } .height('100%') .width('100%') .backgroundColor($r('app.color.bg_page')) } // --- 胶水代码区:连接 View 和 ViewModel --- async handleStart() { // 更新 Loading 状态 this.isLoading = true; // 调用 VM 的逻辑 await this.vm.launch(async () => { // 模拟异步启动录音 await new Promise<void>(resolve => setTimeout(resolve, 500)); this.status = 'recording'; // 启动一个界面定时器来同步显示 // 注意:这里为了 UI 刷新,我们在 View 层维护了一个定时器来读取 VM 的数据 // 或者直接在 View 层计时。为了演示 MVVM,我们假设 VM 内部有复杂逻辑 // 真实场景中,VM 可以通过 callback 通知 View 更新 this.startUITimer(); }); this.isLoading = false; } async handleStop() { this.isLoading = true; await this.vm.stopRecording(); this.status = 'idle'; this.stopUITimer(); this.isLoading = false; } // UI 层的定时器,只负责从 VM 同步数据或者自我更新 private uiTimer: number = -1; startUITimer() { this.uiTimer = setInterval(() => { // 假设 VM 中有真实的音频时长(可能来自底层 SDK 回调) // 这里我们简单自增 this.duration++; }, 1000); } stopUITimer() { clearInterval(this.uiTimer); } formatDuration(seconds: number): string { const min = Math.floor(seconds / 60); const sec = seconds % 60; return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; } }

在这个架构中,View 层非常傻。它不知道录音文件存在哪里,也不知道数据库怎么写。它只知道:用户点了按钮 -> 我叫 ViewModel 去干活 -> ViewModel 干完了 -> 我更新statusduration-> 界面重绘。

当然,随着鸿蒙生态的发展,V2 版本的状态管理(@ObservedV2 和 @Trace)正在变得越来越流行,它允许 ViewModel 中的属性变化直接驱动 UI 更新,不再需要像上面代码那样在 View 层写胶水代码和手动同步状态。

但在现阶段,为了保证你的代码能稳定运行在绝大多数设备上,掌握这种基础的逻辑与视图分离的写法,是理解更高级框架的必经之路。

五、 总结

今天我们没有写太多花哨的界面,但我们完成了一次代码灵魂的洗礼。

我们拒绝了将所有逻辑堆砌在 UI 组件里的面条代码,而是坚定地选择了MVVM架构。我们深度辨析了@State@Prop@Link的使用场景,确保数据流向清晰可控。

更重要的是,我们手写了一个功能强大的BaseViewModel,它将成为我们未来几十个页面开发的基石,帮我们统一处理 Loading 和 Error,让业务代码变得清爽无比。

现在,你的工程里已经有了合理的目录结构,有了通用的 ViewModel 封装,有了清晰的数据模型。你已经做好了迎接复杂业务挑战的准备。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1201719.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

aiSim领衔!国内外自动驾驶仿真软件大全:热门推荐与选择指南

在自动驾驶技术飞速发展的今天&#xff0c;仿真测试已成为自动驾驶算法研发、验证的核心环节&#xff0c;能够大幅降低路测成本、突破场景复现限制&#xff0c;据行业数据显示&#xff0c;约90%的自动驾驶算法测试通过仿真平台完成。目前市面上涌现出多款功能各异的自动驾驶仿真…

芒格的“反向激励“分析在量子计算云服务定价中的应用

芒格的"反向激励"分析在量子计算云服务定价中的应用 关键词&#xff1a;芒格、反向激励分析、量子计算云服务、定价策略、市场竞争 摘要&#xff1a;本文深入探讨了芒格的“反向激励”分析方法在量子计算云服务定价中的应用。首先介绍了研究的背景、目的、预期读者和…

基于springboot的植物花卉销售管理系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

20252803-Linux安全类实验-ShellShock 攻击实验 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

铟材料:稀散金属隐形明星,半导体+光伏核心刚需

在稀土、锂钴镍占据资源赛道 C 位的当下,有一种地壳丰度仅百万分之 0.1 的稀散金属,低调却不可或缺——它就是铟材料。这种被称为“金属界的维生素”“高科技隐形骨架”的战略资源,熔点低、延展性极佳,尤其氧化铟锡…

自动驾驶仿真软件推荐:康谋aiSim——ISO 26262 ASIL-D 认证的高保真选择

自动驾驶技术的快速发展离不开高效可靠的仿真测试工具。面对市面上众多仿真软件&#xff0c;用户常问 “自动驾驶仿真软件有哪些”“哪些自动驾驶仿真软件好用”“如何选择自动驾驶仿真软件” 等问题&#xff0c;选择一款功能全面、性价比高且符合自身研发需求的平台&#xff0…

关于Uvicorn:一个遵循ASGI规范的异步Web服务器

一、核心定位:Uvicorn 是一个 ASGI 服务器 首先要明确两个关键概念,才能理解 Uvicorn 的核心价值:ASGI:全称 Asynchronous Server Gateway Interface(异步服务器网关接口),是 Python 生态中用于连接「异步 Web …

9个最佳性能测试工具(2026)

1、前言 性能测试检查软件程序在预期工作负载下的速度、响应时间、可靠性、资源使用情况和可扩展性。性能测试的目的不是发现功能缺陷&#xff0c;而是消除软件或设备中的性能瓶颈。 性能测试为利益相关者提供有关其应用程序的速度、稳定性和可扩展性的信息。更重要的是&…

058.质数判断 +质数筛 + 质因子分解

质数判断 朴素判断\(O(\sqrt{n})\)bool isp(int n){for(int i=2;i*i<=n;++i){if(n%i==0){return 0;}}return 1; }Miller_Rabin素性测试判断高精度数\(O(k(\log n)^3)\) ,k为测试次数测试链接#include<bits/stdc…

超融合 “进化论”:当 HCI 遇上云原生技术栈,下一代基础设施雏形初现

从物理服务器堆砌的 “石器时代”&#xff0c;到虚拟化普及的 “青铜时代”&#xff0c;再到超融合&#xff08;HCI&#xff09;重构数据中心的 “铁器时代”&#xff0c;企业基础设施的每一次迭代&#xff0c;都在回应业务增长与技术变革的双重诉求。如今&#xff0c;当云原生…

从零构建云原生“试验田”:超融合的自我修养

对于多数企业而言&#xff0c;云原生转型从不是“一步到位”的豪赌&#xff0c;而是通过搭建轻量化“试验田”逐步验证、迭代的过程。这个试验田既要低成本、易部署&#xff0c;又要能模拟真实生产环境的复杂负载&#xff0c;还要为后续规模化扩展预留空间。超融合凭借“计算、…

智慧园区智能照明控制系统解决方案

1、概述园区照明比较复杂&#xff0c;办公建筑、生产车间和园区道路、景观照明等类型比较多&#xff0c;而且对照明控制方式要求不一样。所以合理使用照明控制系统&#xff0c;针对不同建筑不同场景使用不同的控制策略&#xff0c;大程度使用自然光照明达到节省照明用电&#x…

3-VueAjax

什么是Vue Vue是一款用于构建用户界面的渐进式的JavaScript框架。官方网站&#xff1a;https://cn.vuejs.org/ 前端负责将数据以美观的样式呈现出来&#xff0c;而数据最终又要在数据库服务器中存储并管理。前端想要拿到数据&#xff0c;就需要请求服务器。然后服务器将数据返…

基于springBoot的动漫分享系统的设计与实现

背景与意义随着互联网技术的快速发展&#xff0c;动漫文化在全球范围内的影响力不断扩大。动漫爱好者群体日益壮大&#xff0c;对动漫资源的分享、讨论和收藏需求显著增加。传统的动漫分享方式如论坛、贴吧等存在信息分散、互动性不足、资源管理混乱等问题。基于SpringBoot的动…

天然蛋白与重组蛋白的技术区别与实验应用全解析:科研试剂视角下的最佳指南

天然蛋白通常指直接从原代生物组织、细胞裂解液或生物体分泌体系中分离得到的蛋白质。这类蛋白在自然状态下完成了基因调控、翻译后修饰(如磷酸化、糖基化等),具备本源的构象和修饰状态。 重组蛋白是通过基因克隆技…

2026年还在靠“开机等单”跑网约车?学会这几条,超越同城80%的司机!

亲爱的司机师傅&#xff0c;如果你还在靠“开机等单”跑网约车&#xff0c;那今天的文章&#xff0c;请你一定看完。跑车早已不是拼体力、拼时间的年代。真正能站稳脚跟、赚得盆满钵满的&#xff0c;都是懂得借平台之力、摸透接单逻辑的“智慧型司机”。从现在起&#xff0c;改…

导师严选2026 AI论文平台TOP8:MBA开题报告全测评

导师严选2026 AI论文平台TOP8&#xff1a;MBA开题报告全测评 2026年MBA论文写作工具测评&#xff1a;为何需要一份权威榜单&#xff1f; MBA学生在撰写开题报告与论文过程中&#xff0c;常面临选题思路不清晰、文献资料查找困难、格式规范不熟悉等挑战。随着AI技术的不断进步&a…

基于springBoot的高校学生绩点管理系统的设计与实现

高校学生绩点管理系统的背景高校学生绩点管理系统是教育信息化的重要组成部分。随着高校招生规模的扩大&#xff0c;传统手工记录和计算学生成绩的方式已无法满足现代教育管理的需求。学生成绩数据量大、计算复杂&#xff0c;人工操作易出错且效率低下。高校亟需一套自动化、智…

5年测试被裁,去面试差点被问哭了······

我的个人背景非常简单&#xff0c;也可以说丝毫没有亮点。 学历普通&#xff0c;计算机专业二本毕业&#xff0c;毕业后出来就一直在一家小公司&#xff0c;岁月如梭细&#xff0c;算了下至今从事软件测试已经5年了&#xff0c;也点点点了五年&#xff0c;每天都是重复的工作&…

基于springBoot的高校毕业生公职资讯系统的设计与实现

高校毕业生公职资讯系统的背景 高校毕业生数量逐年增加&#xff0c;就业压力不断增大。公职岗位因其稳定性、福利保障等特点&#xff0c;成为许多毕业生的首选就业方向。然而&#xff0c;公职考试信息分散、更新不及时、缺乏针对性指导等问题&#xff0c;导致毕业生在获取有效…