reactive-mvvm-android 架构全景图:响应式 MVVM 的 Clean Architecture 分层设计一次看懂
【免费下载链接】reactive-mvvm-androidMy way to MVVM using KotlinX Coroutines and Android>项目地址: https://gitcode.com/gh_mirrors/re/reactive-mvvm-android
reactive-mvvm-android 是一个用MVVM + Clean Architecture实现的 Android 漫画搜索应用,核心亮点在于用 KotlinX Coroutines 的 Flow 构建完整的响应式数据流,并借助 Android DataBinding 把 ViewModel 状态"零胶水"地渲染到界面。本文将从一张全景图出发,带你逐层拆解这套响应式 MVVM 的分层设计,适合刚接触 Android 架构的新手对照学习。
一、项目定位:一个小 App 撑起完整架构 🎯
别看它只是个搜索漫画封面的小应用,五脏俱全:
- 业务:搜索 ComicVine 开放 API,展示漫画卷(Volume)列表,点击查看详情
- 缓存:搜索过的关键词和结果存进 Room 数据库,二次搜索直接走本地
- 架构:MVVM 负责 UI 与业务解耦,Clean Architecture 负责业务与框架解耦
| 技术组件 | 承担职责 |
|---|---|
| KotlinX Coroutines + Flow | 响应式异步通信的"血液" |
| Android DataBinding + LiveData | 状态驱动 UI 自动刷新 |
| Room | 本地缓存(DAO 直接返回 Flow) |
| Retrofit + OkHttp | 网络层,接口方法直接suspend化 |
| Koin | 轻量依赖注入 |
| Arrow (Either) | 函数式错误处理 |
| Kotest + Turbine | 单元测试(见app/src/test/) |
二、架构全景图:一次看懂四层结构 🗺️
整个app/src/main/java/es/ffgiraldez/comicsearch/目录按 Clean Architecture 划分为四个同心层,依赖方向永远指向内层:
┌─────────────────────────────────────────────────────┐ │ UI 层 (query/ui/、detail/、navigation/) │ │ Activity · 自定义 BindingAdapter · 布局 XML │ │ 只做"展示",不写任何业务逻辑 │ └──────────────────┬──────────────────────────────────┘ │ DataBinding 自动观察 ┌──────────────────▼──────────────────────────────────┐ │ Presentation 层 (query/*/presentation/) │ │ SuspendQueryStateViewModel(基类) │ │ SuspendSearchViewModel / SuspendSuggestionViewModel │ │ 产出 QueryViewState 状态机(Idle/Loading/Result/Error)│ └──────────────────┬──────────────────────────────────┘ │ 只依赖"仓库接口",不关心实现 ┌──────────────────▼──────────────────────────────────┐ │ Domain 层 (comics/domain/) │ │ 纯 Kotlin 实体:Volume / Query / ComicError │ │ 零 Android 依赖,是架构的"心脏" │ └──────────────────┬──────────────────────────────────┘ │ 仓库实现位于 Data 层,反向满足 Domain 契约 ┌──────────────────▼──────────────────────────────────┐ │ Data 层 (comics/data/) │ │ SuspendComicRepository(本地优先策略) │ │ ├── network/ Retrofit 接口(ComicVineApi.kt) │ │ └── storage/ Room DAO + 数据库(ComicDao.kt 等) │ └─────────────────────────────────────────────────────┘各层对应的核心目录:
| 层 | 目录 | 关键文件 |
|---|---|---|
| UI | query/ui/、query/search/ui/ | QueryActivity.kt、SearchBindingAdapters.kt |
| Presentation | query/base/presentation/ | SuspendQueryStateViewModel.kt、QueryViewState.kt |
| Domain | comics/domain/ | Entities.kt |
| Data | comics/data/ | SuspendComicRepository.kt、ComicDataSources.kt |
| 基础设施 | comics/di/、platform/、navigation/ | comicModule.kt、Navigator.kt |
三、响应式数据流:从输入框到界面的单向流动 🔄
这是整个项目的"灵魂"。用户在搜索框输入时,数据沿一条纯响应式链路流动,全程没有回调、没有 Handler:
- 输入捕获:基类
SuspendQueryStateViewModel持有一个MutableLiveData<String>作为查询输入,通过query.asFlow()把它变成 Flow 流(见SuspendQueryStateViewModel.kt) - 状态转换:子类(如
SuspendSearchViewModel.kt)用flatMapLatest把"词流行"切换为"结果流"——输入一变,旧请求自动取消,天然防抖防竞态 - 错误建模:仓库返回
Either<ComicError, List<T>>,用 Arrow 把"网络失败/结果为空"建模为类型而不是异常(QueryViewState.kt中的toViewState()完成转换) - 状态外发:
onStart先发出Idle/Loading,再流式发出Result/Error,最终asLiveData()交还给 LiveData - UI 自动刷新:DataBinding 观察到
state变化,通过自定义 BindingAdapter 自动更新列表、进度条、错误文案——Activity 里几乎零逻辑代码
一句话总结:Flow 负责"怎么算",ViewState 负责"算什么状态",DataBinding 负责"怎么显示"。三者各司其职,这就是"响应式 MVVM"的完整含义。
四、Domain 层:只有 3 个类的纯 Kotlin 核心 ❤️
打开 Entities.kt,整个领域模型只有三个定义:
Volume:漫画卷(标题、作者、封面)Query:一次搜索词ComicError(sealed class):NetworkError/EmptyResultsError
为什么这么重要?Domain 层不 import 任何 Android、Retrofit、Room 包。这意味着:
- 它可以在纯 JVM 上单测,不需要 Robolectric
- UI 和数据层都围着这几个纯数据类转,边界清晰
- 换掉整个网络或存储实现,Domain 一行不用改
这就是 Clean Architecture 的"依赖倒置"精髓:内层定义契约,外层实现契约。
五、Data 层:Repository 的"本地优先"缓存策略 📦
SuspendComicRepository.kt 是 Data 层的中枢,它把 ComicDataSources.kt 中定义的本地/远程两个数据源接口组合起来,策略非常经典:
findByTerm(term) ├─ 先问本地:Room DAO 返回 Flow,查这个关键词搜过没有 ├─ 搜过 → 直接回放本地结果(命中空结果也算错误态,提示"无结果") └─ 没搜过 → suspend 请求远端 → 成功后写回本地 → 再流式发出几个值得抄的设计细节:
- 整个仓库就是一条 Flow 管道:
flatMapConcat把"查本地 → 拉远端 → 存本地"串成一条链,消费方只管订阅 - DAO 直接返回 Flow:ComicDao.kt 里的
@Query方法签名是Flow<...>,数据一变,UI 自动跟着变,无需手动"刷新" - 错误在源头就被类型化:
Either.catch包裹网络调用,异常在进入上层之前就已变成ComicError.network(),UI 层永远不会收到未捕获异常 - 一个抽象基类,两个具体仓库:
SuspendComicRepository<T>被搜索(Volume)和搜索建议(Suggestion)两个场景复用,避免重复逻辑
六、Presentation 层:ViewModel 基类 + 状态机 🎭
这一层有两个"模板方法"设计,是项目最可复用的部分:
① 基类模板:SuspendQueryStateViewModel接收一个"转换器函数"(Flow<String>) -> Flow<QueryViewState<T>>,子类只需注入转换器即可获得完整的query输入与state输出。搜索和建议两个功能因此共享 80% 代码。
② 状态机模板:QueryViewState是 sealed class,恰好四个状态:
| 状态 | 触发时机 | UI 表现 |
|---|---|---|
Idle | 初始 | 什么都不显示 |
Loading | 请求发出 | 进度条可见 |
Result | 拿到数据 | 列表更新 |
Error | 网络失败/空结果 | 错误文案 |
sealed class 逼着 UI 层穷尽所有分支,不存在"未处理状态"——这是比回调地狱可靠得多的状态管理方式。
七、UI 层:BindingAdapter 如何做到"无胶水渲染" 🖼️
以 SearchBindingAdapters.kt 为例,作者用@BindingAdapter把"状态 → 控件"的映射封装成四个小函数:
RecyclerView.bindStateData(...):列表随Result数据自动提交FrameLayout.bindStateVisibility(...):错误容器随错误态显隐TextView.bindErrorText(...):错误文案人性化转换ProgressBar.bindProgress(...):加载动画随Loading态开关
配合 ViewBinding.kt 里的gone(Boolean)扩展,布局文件app/src/main/res/layout/query_activity.xml里只剩声明式绑定,QueryActivity.kt几乎没有任何状态处理代码。界面变成了状态的"投影"——这正是响应式 UI 的终极形态。
八、依赖注入与可测性:Koin + Kotest 双保险 ✅
- 注入:comicModule.kt 用 Koin 装配 Retrofit 客户端与 Room 数据库,搜索/建议各自还有独立 DI 模块(
query/search/di/SearchModule.kt),替换实现只需改模块,不动业务代码 - 导航:
navigation/目录把 Activity 跳转抽象成Screen密封类 +Navigator,UI 层不直接startActivity - 测试:
app/src/test/下用 Kotest + Turbine 写了完整的规格测试——
| 测试文件 | 验证对象 |
|---|---|
SuspendComicRepositoryShould.kt | 仓库的本地优先策略(配 fake 数据源) |
SearchViewModelSpec.kt/SuggestionViewModelSpec.kt | ViewModel 状态流(用 Turbine 断言 Flow 事件) |
SearchBindingAdapterSpec.kt/SuggestionBindingAdapterSpec.kt | 绑定适配器的 UI 映射 |
测试能写得这么顺,正是因为前面每一层都遵守了"依赖内层契约"的约定。
九、新手学习路线:按这个顺序读代码最省力 📚
- 先读 Entities.kt——3 个数据类看懂领域边界
- 再读 QueryViewState.kt——理解四态状态机
- 接着读 SuspendSearchViewModel.kt——18 行代码看完整条响应式链路
- 然后读 SuspendComicRepository.kt——Flow 组合 + 缓存策略
- 最后读 SearchBindingAdapters.kt——状态如何变成界面
总结:这套架构教会我们的 5 件事
| # | 设计点 | 可迁移的实践 |
|---|---|---|
| 1 | Domain 层纯 Kotlin | 业务核心永远不依赖框架,可测性来自边界 |
| 2 | 数据源接口定义在数据层、注入仓库 | 依赖倒置让"先本地后远端"可插拔 |
| 3 | 全链路 Flow + flatMapLatest | 用流编排异步,输入变化自动取消旧任务 |
| 4 | sealed class 状态机 | 穷举状态比 try-catch 更可控 |
| 5 | BindingAdapter 封装渲染 | Activity 越薄,架构越纯粹 |
🎯一句话带走:reactive-mvvm-android 证明了——响应式 MVVM 的本质,就是用 Flow 把"输入 → 状态 → 界面"焊成一条自动传送带,再用 Clean Architecture 的四层结构保证传送带上每一环都可独立测试、独立替换。
【免费下载链接】reactive-mvvm-androidMy way to MVVM using KotlinX Coroutines and Android>项目地址: https://gitcode.com/gh_mirrors/re/reactive-mvvm-android
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考