Android MVVM 示例 Demo:从分层到状态收敛的完整实践
简介这份资源是面向Android开发者、尤其是希望从MVC或MVP转向MVVM架构的中级学习者的示例Demo通过完整可运行的项目演示Model、View、ViewModel三层如何协作解决架构分层不清、UI逻辑臃肿、数据与视图耦合过紧等常见问题。压缩包共737个文件约20.65MB以xml布局、json配置、class与dex字节码、java与kt源码、gradle构建脚本及properties属性文件为主另含少量jar、webp资源与apk成品覆盖从源码到打包的完整工程结构。目前已有200人学习下载。Demo重点展示ViewModel持有数据、LiveData感知生命周期、Data Binding在XML中声明绑定关系以及Binding Adapter处理自定义属性等关键环节读者可据此理解仓库类如何向ViewModel供数、视图如何自动响应数据变化并参考其目录组织方式将MVVM快速落地到自己的项目中提升代码可测试性与团队协作效率。1. 从一次“改不动”的界面说起这个 Android MVVM 示例 Demo 到底能解决什么接手过一个二手页面Activity 里塞了八百多行网络请求、JSON 解析、按钮状态、列表刷新全搅在一起。产品要加一个“加载失败重试”我改了三处崩了两处。那次之后我开始认真找一套能照着抄的 MVVM 骨架而不是只看概念图。这个 Android MVVM 示例 Demo 就是干这个的它把 View、ViewModel、Model 三层拆开用可运行的最小工程演示数据怎么从数据源流到界面、状态怎么回传、配置变更后数据怎么不丢。适合两类人一是刚接触 Android 设计模式、被 MVP 和 MVC 绕晕的新手二是想把手头 Activity 拆干净、但缺一个可对照模板的熟手。它不教你写炫酷 UI只解决一件事——让界面逻辑和数据逻辑各回各家。2. 拆开这个 Demo三层结构、数据流向与选型理由2.1 View 层只做“看”和“转发”不碰业务在这个 Demo 里Activity 或 Fragment 的职责被压到极窄初始化视图、监听用户操作、把结果丢给 ViewModel、订阅状态变化刷新 UI。它不直接 new 数据仓库也不写解析逻辑。常见做法是让 View 持有 ViewModel 引用通过观察者模式接收数据。这样做的直接好处是你把 Activity 删掉换成 Fragment业务代码一行不用动。判断一个 View 层写得合不合格有个土办法看它有没有 import 网络库或数据库类。如果 import 了说明分层没做干净。Demo 里刻意把这类依赖全部挡在 View 之外就是为了让这个边界看得见。2.2 ViewModel 层扛住状态配置变更不丢数据ViewModel 是这个模式的核心。它持有界面状态暴露可观察的数据流处理来自 View 的事件。关键点在于它的生命周期比 Activity 长——屏幕旋转时 Activity 重建ViewModel 还活着数据不用重新拉。Demo 里通常用一个可变的状态容器加一个只读暴露外部只能读不能直接改改必须走方法。// ViewModel 持有状态对外只读暴露 class UserViewModel(private val repo: UserRepository) : ViewModel() { // 内部可变外部不可见 private val _uiState MutableLiveDataUiState() // 对外只读View 只能观察 val uiState: LiveDataUiState _uiState fun loadUser(id: String) { // 切到后台线程做数据请求 viewModelScope.launch { _uiState.value UiState.Loading try { val user repo.fetchUser(id) // 挂起函数不阻塞主线程 _uiState.value UiState.Success(user) } catch (e: Exception) { _uiState.value UiState.Error(e.message) } } } }这段代码里_uiState和uiState的命名约定是刻意为之下划线前缀表示“仅内部可写”。viewModelScope绑定 ViewModel 生命周期页面销毁时协程自动取消避免回调打到已销毁的 View 上。UiState一般是个密封类把 Loading、Success、Error 三种状态收拢View 层用 when 分支处理不会出现“又加载又报错”的中间态。参数上要注意loadUser接收的 id 建议做非空校验别指望调用方一定传对。Demo 里如果用了LiveDatasetValue只能在主线程调子线程要用postValue这是新手最容易翻车的地方之一。2.3 Model 层与仓库数据从哪来、怎么换源Model 层在 Demo 里通常拆成两块数据实体Entity/Model和数据仓库Repository。实体就是纯数据类仓库负责决定数据从网络拿还是从本地缓存拿。这样拆的意义在于将来换接口、加缓存、做离线只动仓库ViewModel 和 View 无感。// 仓库层对上层屏蔽数据来源 class UserRepository( private val api: UserApi, private val dao: UserDao ) { suspend fun fetchUser(id: String): User { // 先查本地缓存命中直接返回 dao.findById(id)?.let { return it } // 未命中再走网络并写入缓存 val remote api.getUser(id) dao.insert(remote) return remote } }fetchUser是挂起函数调用方必须在协程里执行。先本地后网络的策略叫 Cache-Aside适合读多写少的场景。如果业务要求数据必须最新就把顺序反过来或加过期时间。Demo 里用接口注入 api 和 dao是为了方便替换成假数据做测试——这也是选 MVVM 而不是 MVP 的一个现实理由ViewModel 不依赖 Android 框架类单元测试跑得飞快。2.4 为什么是 MVVM而不是继续用 MVPMVP 里 Presenter 持有 View 接口引用页面销毁时得手动解绑漏了就内存泄漏。MVVM 用可观察数据流替代了这层手动绑定View 观察 ViewModelViewModel 不知道 View 存在解耦更彻底。再加上 ViewModel 天然扛配置变更旋转屏幕不用重新请求这是 MVP 要额外写代码才能做到的。代价是学习曲线LiveData、协程、数据绑定这些概念得先过一遍。Demo 的价值就在于把这些概念压进一个能跑起来的最小工程而不是散落在十几篇文章里。3. 把这个 Demo 跑起来环境、依赖与最小可运行步骤3.1 环境准备与依赖配置拿到工程先别急着点运行版本对不上是最高频的翻车点。Demo 一般基于 Kotlin 编写构建工具用 Gradle。你需要确认本地 JDK 版本、Android Gradle Plugin 版本、Kotlin 版本三者兼容。常见做法是打开工程根目录的build.gradle看插件版本再对照官方兼容表调整。// app/build.gradle 中 MVVM 相关核心依赖 dependencies { // ViewModel 与 LiveData implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 implementation androidx.lifecycle:lifecycle-livedata-ktx:2.6.2 // 协程 implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 // 网络与数据库按 Demo 实际选型保留 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation androidx.room:room-runtime:2.5.2 }lifecycle-viewmodel-ktx提供viewModelScope没有它协程作用域得自己建。lifecycle-livedata-ktx提供liveData {}构建块。协程版本和 Kotlin 版本强相关Kotlin 1.8 配 1.7.x 协程是稳的跨大版本容易出NoSuchMethodError。Retrofit 和 Room 是 Demo 里常见的网络与本地存储组合如果你的工程只演示内存数据这两行可以去掉但仓库层的接口要相应改成假实现。提示依赖拉不下来时先看是不是仓库地址没配全再确认网络能访问 Maven 中央仓库别一上来就怀疑代码。3.2 从入口到数据流一次完整调用链跑起来之后建议按一次按钮顺着日志把整条链路走一遍。以“加载用户”为例Activity 的点击监听调用viewModel.loadUser(id)ViewModel 切协程发请求仓库先查本地再走网络结果通过 LiveData 回传Activity 的观察者收到后刷新 TextView 或列表。// Activity 中订阅状态按状态分支刷新 UI viewModel.uiState.observe(this) { state - when (state) { is UiState.Loading - progressBar.visibility View.VISIBLE is UiState.Success - { progressBar.visibility View.GONE nameText.text state.user.name // 只在成功态取数据 } is UiState.Error - { progressBar.visibility View.GONE toast(state.message) } } }observe的第一个参数传thisActivity 或 Fragment生命周期感知页面不可见时不回调避免空指针。when必须穷举密封类的所有子类编译器会帮你查漏这是密封类比枚举加字符串好用的地方。注意state.user只在 Success 分支能访问其他分支拿不到从类型上杜绝了“数据没来就刷新”的错误。3.3 用假数据验证分层是否真的解耦想确认分层有没有做到位有个低成本验证法把仓库实现换成一个返回固定假数据的版本不改 ViewModel 和 View看界面能不能正常显示。如果能说明依赖方向是对的。// 测试用假仓库不碰网络和数据库 class FakeUserRepository : UserRepository() { override suspend fun fetchUser(id: String): User { delay(500) // 模拟网络延迟 return User(id, 测试用户, 18) // 固定返回 } }delay模拟耗时用来观察 Loading 态是否正常显示。如果换成假仓库后界面卡死或状态不刷新问题多半出在 ViewModel 的线程切换或观察者注册时机上而不是数据源。这个替换动作本身也是单元测试的基础ViewModel 的测试不需要 Android 环境传个假仓库进去就能跑。4. 避坑与排查MVVM 落地时最容易翻车的五件事4.1 现象旋转屏幕后数据重新加载Loading 又转一圈原因ViewModel 没有真正独立于 Activity或者数据请求写在了 Activity 的 onCreate 里而不是 ViewModel 的初始化块里。解决把加载逻辑放进 ViewModel 的init或首次调用时判断状态Activity 重建时只重新订阅不重新触发请求。检查viewModelScope是否被误建成了普通CoroutineScope。4.2 现象LiveData 的 observe 回调不执行原因observe注册在了错误的生命周期所有者上或者用了observeForever却忘了移除。解决Activity/Fragment 里统一传this或viewLifecycleOwnerFragment 里尤其要用后者否则会收到已销毁视图的回调。observeForever只在确实需要常驻监听时用且必须配对removeObserver。4.3 现象子线程更新 UI 报 “Only the original thread that created a view hierarchy can touch its views”原因在协程里直接改了 View或者用setValue在子线程更新 LiveData。解决UI 操作放回主线程LiveData 子线程更新用postValue。更稳的做法是 ViewModel 只暴露状态View 层在观察回调里刷新回调默认在主线程。4.4 现象内存泄漏LeakCanary 报 Activity 被 ViewModel 持有原因ViewModel 里存了 Activity、Context 或 View 的引用。解决ViewModel 只持有数据仓库和状态需要 Context 时用 AndroidViewModel 并只拿 Application 上下文。任何 View 类型的字段都不该出现在 ViewModel 里。4.5 现象依赖注入没配好运行时报 “Cannot create an instance of class ViewModel”原因ViewModel 构造函数带了参数但用的是默认工厂工厂不知道怎么造。解决要么给 ViewModel 提供无参构造要么自定义 Factory 或引入依赖注入框架把仓库传进去。Demo 里如果用了 Hilt检查HiltViewModel和AndroidEntryPoint是否都标了漏一个就报这个错。5. 进阶把状态收敛成一个 UiState再谈测试与复用Demo 跑通之后真正拉开差距的是状态管理。早期我习惯在 ViewModel 里散着放isLoading、errorMsg、data三个字段结果界面偶尔同时显示“加载中”和“加载失败”排查半天发现是两个字段更新不同步。后来改成单一UiState密封类任何时刻只有一个状态成立这类玄学问题直接消失。// 单一状态源杜绝中间态打架 sealed class UiState { object Loading : UiState() data class Success(val user: User) : UiState() data class Error(val message: String) : UiState() }object Loading不需要携带数据用 object 比 class 省内存。Success 和 Error 带各自需要的数据类型安全。View 层一个 when 处理完新增状态时编译器强制你补分支不会漏。状态收敛之后测试就好写了。ViewModel 的测试不需要真机用runTest加假仓库即可Test fun 加载成功时状态为 Success() runTest { val vm UserViewModel(FakeUserRepository()) vm.loadUser(1) // 推进协程直到空闲 advanceUntilIdle() assertTrue(vm.uiState.value is UiState.Success) }runTest提供测试调度器advanceUntilIdle把挂起的协程推到执行完。断言只关心状态类型不关心具体数据测试稳定不易碎。这套写法在 CI 上跑几秒出结果比 instrumentation 测试快一个数量级。复用层面一个页面一个 ViewModel 是默认但列表项这种重复结构可以抽共享的 ViewModel 或状态容器。判断标准是如果两个页面的状态字段和事件高度重合就抽只是长得像但业务不同别硬抽否则改一个崩两个。我一般会先让功能跑通等第三个相似页面出现再动手抽象避免过早设计。从那以后我每次拆 Activity都强制先画一遍数据流向谁触发、谁处理、谁持有状态、谁刷新界面四步对不上就不动手写代码。这套 Demo 给我的最大价值不是代码本身而是让我养成了先定边界再填逻辑的习惯。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

拆解硬汗健身的男性向设计:从睾酮差异到AI计划引擎的性别适配逻辑

拆解硬汗健身的男性向设计:从睾酮差异到AI计划引擎的性别适配逻辑

文章目录一、性别差异:健身App设计里被低估的变量二、生理学地基:为什么男性健身该走"力量增肌"路线三、技术设计原理一:AI定制计划的性别适配四、技术设计原理二:动作库与器械双场景的男性导向五、产品定位对比&#x…

2026/10/13 3:12:38 阅读更多 →
E900V22D刷机必看:芯片方案识别与完整实操指南

E900V22D刷机必看:芯片方案识别与完整实操指南

1. 先搞清楚一件事:E900V22D的芯片方案决定了刷机的天花板说起电信定制盒子E900V22D,很多人的第一反应是“这不就是个运营商送的网络机顶盒嘛”,然后就开始满网找刷机包。但我经手这类盒子这么久,最想提醒新人的一句话是&#xff…

2026/10/13 3:12:00 阅读更多 →
交通流量检测系统落地三难:小目标漏检、光照泛化、工程链路断裂

交通流量检测系统落地三难:小目标漏检、光照泛化、工程链路断裂

简介:本资源是一个基于深度学习的交通流量检测系统完整实现项目,面向人工智能初学者、计算机视觉方向学生及智能交通应用开发者,聚焦于视频流中车辆识别与实时计数任务。项目采用Python开发,集成TensorFlow/PyTorch常用生态&#…

2026/10/13 3:12:02 阅读更多 →

最新新闻

Streamyfin 后台下载模块(Background Downloader)深入解析:从 TypeScript API 到 iOS / Android 原生实现

Streamyfin 后台下载模块(Background Downloader)深入解析:从 TypeScript API 到 iOS / Android 原生实现

音视频移动开发 【免费下载链接】streamyfin A modern Jellyfin client built with Expo 项目地址: https://gitcode.com/gh_mirrors/st/streamyfin 点击查看 免费下载 本篇技术指南以 modules/background-downloader/README.md 为骨架,结合 Streamyfin…

2026/10/13 6:00:40 阅读更多 →
10 分钟跑通私有本地 AI 聊天:Open WebUI 从零部署到第一句对话

10 分钟跑通私有本地 AI 聊天:Open WebUI 从零部署到第一句对话

10 分钟跑通私有本地 AI 聊天:Open WebUI 从零部署到第一句对话 【免费下载链接】open-webui User-friendly AI Interface (Supports Ollama, OpenAI API, ...) 项目地址: https://gitcode.com/GitHub_Trending/op/open-webui Open WebUI 是一个开源的自托管…

2026/10/13 6:00:40 阅读更多 →
Kener 状态徽章实战指南:status、uptime 与 Dot 徽章的 URL 语法、查询参数与源码实现解析

Kener 状态徽章实战指南:status、uptime 与 Dot 徽章的 URL 语法、查询参数与源码实现解析

可观测性后端前端运维 【免费下载链接】kener Stunning status pages, batteries included! 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ke/kener 点击查看 免费下载 Kener 内置了三种可直接以 <img> 引入的状态徽章&#xff08;Badge&#xff09;&#xff1…

2026/10/13 6:00:40 阅读更多 →
AUV协同定位与故障检测一体化MATLAB实现

AUV协同定位与故障检测一体化MATLAB实现

简介&#xff1a;本资源是一份面向海洋机器人与水下自主系统研究者的AUV协同定位算法实现方案&#xff0c;聚焦多AUV编队中高精度定位与实时故障检测两大核心挑战&#xff0c;适用于自动化、控制工程及海洋探测方向的研究生、科研人员与算法工程师。压缩包为RAR格式&#xff0c…

2026/10/13 6:00:40 阅读更多 →
Pomotroid 计时器时长键盘输入:从整分钟滑块到 MM:SS 精确设置的完整改造指南

Pomotroid 计时器时长键盘输入:从整分钟滑块到 MM:SS 精确设置的完整改造指南

【免费下载链接】pomotroid :tomato: Simple and visually-pleasing Pomodoro timer 项目地址&#xff1a; https://gitcode.com/gh_mirrors/po/pomotroid 点击查看 免费下载 Pomotroid 是一款基于 Tauri&#xff08;Rust Svelte&#xff09;的番茄钟应用。本篇技术指南围绕其…

2026/10/13 6:00:40 阅读更多 →
OpenClaw 跨平台部署指南:Windows/macOS/Linux 环境搭建避坑全记录

OpenClaw 跨平台部署指南:Windows/macOS/Linux 环境搭建避坑全记录

如果你准备把 OpenClaw 真正用起来&#xff0c;第一道门槛往往不是模型接入&#xff0c;也不是技能配置&#xff0c;而是环境搭建。我见过太多人卡在“装好了但跑不动”“这台能跑换台就崩”的阶段&#xff0c;尤其是在 Windows、macOS、Linux 三套系统之间反复横跳的时候&…

2026/10/13 5:59:40 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目&#xff0c;我最大的感受是&#xff0c;大部分坑不是出在技术前沿&#xff0c;而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节&#xff0c;最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了&#xff0c;很难不碰到舵机控制这道坎。做智能小车要转方向&#xff0c;做机械臂要抬关节&#xff0c;做云台要偏航俯仰&#xff0c;背后都是同一个东西在干活&#xff1a;舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”&#xff1f;——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台&#xff1f;或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜&#xff1f;又或者&#xff0c;在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →