我是如何把一个传统 Android 协程示例,重构成 Clean Architecture 项目的

📌 本文面向有 Android 基础、了解协程基本用法,但尚未接触 Clean Architecture 的读者。


前言

Learn-Kotlin-Coroutines 原本是 Amit Shekhar 的一个 Android 协程教学项目(仓库地址)。Amit Shekhar 本身是一位优秀的 Android 实战性讲师,这个项目在大约四年前的语境下,是一个质量不错、很适合入门的示例工程。

它的示例场景覆盖相当完整:

  • ui/basic:基础协程示例
  • ui/retrofit/*:单次、串行、并行网络请求
  • ui/room:本地数据库
  • ui/errorhandling/*:异常处理相关示例
  • ui/task/*ui/timeout:长任务、超时等协程场景

这种组织方式非常适合入门------页面直接,学习路径清楚,每个示例基本可以单独理解。

但一旦目标从「演示协程用法」变成「作为可继续演进的 Android 项目基础」,问题就开始放大了。

这篇文章不是在否定原项目的教学价值,而是在保留它示例价值的前提下,对它做一次现代化、工程化的重构,并把这个过程完整记录下来。


一眼看清改动全貌

在进入细节之前,先用一张表把改造前后的差异说清楚:

维度 重构前 重构后
依赖管理 手动 Factory / 页面传递 Koin 统一注入
ViewModel 职责 既调数据源,又管异常和状态 只协调 UseCase 与 StateFlow<UiState>
业务逻辑位置 散落在 ViewModel 下沉到 UseCase
数据驱动方式 主动拉取、一次性请求 Flow 持续发射,UI 订阅状态
异常处理 每个页面单独 try-catch Repository 统一封装 Resource
工程结构 示例堆叠 UI → Domain → Data 分层

一、先把方法论说清楚:这次重构遵守了哪些结构原则

如果以后再遇到类似的 Android 示例项目,应该按什么原则去重构。

原则 1:依赖方向必须固定

复制代码
UI  →  Domain  →  Data
  • UI 依赖 Domain
  • Domain 依赖抽象,不依赖具体数据实现
  • Data 负责实现这些抽象

一旦这个方向固定,层与层之间的职责就不会继续相互渗透。

原则 2:UI 层只做三件事

✅ 订阅状态

✅ 触发行为

✅ 渲染界面

❌ 不创建依赖

❌ 不编排业务

❌ 不处理底层异常策略

原则 3:Domain 层是稳定边界

Domain 层承担「结构稳定器」的角色:

  • UseCase:表达业务动作
  • Repository 接口:隔离数据实现
  • Resource:统一业务结果模型

UI 怎么变、数据源怎么变,都不应该直接冲击到 Domain 的边界表达。

原则 4:Data 层不是搬运层,而是策略层

Data 层不只是「调接口 + 查数据库」,它还负责:

  • 缓存优先级
  • 刷新策略
  • 错误传播策略
  • 连续数据流建模

RepositoryImpl 是数据策略协调器,而不是简单的 DAO/Api 包装器。

原则 5:状态应该被订阅,而不是被手动推送

UI 不再主动驱动数据,而是订阅已经存在的状态流。

这也是为什么这次重构最终会落到 Flow → StateFlow → repeatOnLifecycle + collect 这一整条链路上。


二、原项目的问题在哪里

原始版本是一种传统的、偏扁平化的 MVVM 示例结构:

当页面少时,链路短、容易讲清楚。但结构问题很快会暴露:

症状一:手动拼装依赖

原始的 ViewModelFactory

  • 每新增一个 ViewModel,都要改 Factory
  • Factory 变成越来越大的分发中心
  • 测试时很难优雅地替换真实依赖

症状二:ViewModel 知道太多底层细节

症状三:异常处理散落各处

每个页面都在重复写几乎一样的错误处理逻辑,错误建模不统一。

💡 代码的问题不在于它不能运行,而在于它无法以低成本继续演进。说白了,项目大了就难以维护了。


三、第一刀:把依赖图从 UI 层收回来(引入 Koin)

没有 DI 的本质问题不是「代码多」,而是依赖关系被泄露到了 UI 层。

一旦 UI 知道依赖如何构造,它就同时承担了两种职责:展示 + 组装系统。这才是结构逐渐失控的根源。

重构后:集中声明依赖

统一在 Application 中初始化:

页面层终于可以回到它该有的样子:

这一步的工程收益

  • ✅ 去掉了手动拼装依赖的重复劳动
  • ✅ 给后续分层改造提供了稳定注入入口
  • ✅ 让「依赖关系」第一次变成了一个可以集中维护的系统

DI 解决的不只是「怎么创建对象」,而是「谁应该知道依赖关系」。


四、真正的重构:按 Clean Architecture 重划边界

引入 Koin 之后,依赖创建问题解决了。但核心耦合关系还没有消失。

这次改造把职责重新拆成了三层:

ViewModel 解耦:只依赖 UseCase

重构前:

重构后:

对应的业务逻辑下沉到 UseCase:

五、把错误处理下沉到 Repository

目标: 从「防御式写法」转向「结果驱动写法」。

重构后:Repository 统一发射 Flow<Resource<T>>

这里的 Repository 已经不只是「封装数据来源」,而是在承担数据策略协调器的角色,负责:

  • 📦 数据优先级(本地还是远程)
  • 🔄 刷新策略(何时更新缓存)
  • ❌ 错误传播方式(什么时候报错,什么时候吞掉)
  • 🌊 数据流是否连续(一次性请求 vs 持续状态流)

ViewModel 此时只负责把结果流映射成 UI 状态流,不再写一行 try-catch


六、协程不只是会写就行:关键是写对层

串行请求 UseCase

使用 flatMapConcat,保证第一个请求完成后才发起第二个,不会并发执行中间流,符合串行语义:

并行请求 UseCase

使用 combine,两个 Flow 同时订阅,任意一个发射新值时重新合并:

💡 好的示例项目,不应该只教「怎么 launch / async」,还应该教「这些协程代码应该写在哪一层」。


七、重构后的完整结构与数据流

目录结构

bash 复制代码
app/src/main/java/me/amitshekhar/learn/kotlin/coroutines
├── data
│   ├── api
│   ├── local
│   └── repository
├── di
│   └── module
├── domain
│   ├── base
│   ├── repository
│   └── usecase
├── ui
│   ├── base
│   ├── basic
│   ├── errorhandling
│   ├── retrofit
│   ├── room
│   ├── task
│   └── timeout
└── CoroutinesApp.kt

以前是「功能按页面堆起来」,现在是「职责按层次分开」。

依赖关系与数据流

Flow 在三层中的不同角色

层级 Flow 的角色 具体形态
Repository 连续数据流建模 Flow<Resource<T>>,先发本地,再发远程
ViewModel 定义 UI 应该消费什么状态 StateFlow<UiState<T>>,映射 Resource
UI 在正确的生命周期里消费状态 repeatOnLifecycle + collect

这是这次重构里最底层的变化:

UI 不再驱动数据,而是消费状态。


八、如果今天重新学习这个项目,推荐按这个顺序看

  1. ui/basic --- 理解最基础的协程用法
  2. single / series / parallel --- 理解不同请求模型
  3. domain/usecase --- 理解这些协程逻辑为什么被放在这里
  4. data/repository/UserRepositoryImpl.kt --- 理解本地缓存、远程刷新和结果封装
  5. 各个 Activity 中的 repeatOnLifecycle + collect --- 理解 UI 如何订阅状态流
  6. di/module/AppModule.ktCoroutinesApp.kt --- 把依赖注入链路串起来

九、现在这个项目里有哪些示例入口

首页 MainActivity 目前提供了这些入口:

示例 路径
基础协程示例 ui/basic
单次网络请求 ui/retrofit/single
串行网络请求 ui/retrofit/series
并行网络请求 ui/retrofit/parallel
Room 数据库读取 ui/room
超时控制 ui/timeout
try-catch 异常处理 ui/errorhandling/trycatch
SupervisorJob / 错误隔离 ui/errorhandling/supervisor
单个长任务 ui/task/onetask
两个长任务 ui/task/twotasks

最后:这次重构到底改变了什么

这次重构的本质,不是引入了多少新技术,而是完成了三件更关键的事情:

  1. 让依赖关系从「隐式分散」变成「显式集中」
  2. 让业务逻辑从「UI 层泄露」回归到「稳定边界(UseCase)」
  3. 让数据流从「一次性请求」升级为「可持续状态流(Flow)」

最终结果不是代码更复杂,而是:

架构实现了高度解耦,新需求的介入不再引发全身性的震荡。

如果你也有一个「能跑,但结构开始变重」的 Android 示例项目,这条优化路径值得参考。

Github Clean-Refactor

参考: 从送外卖看Android Clean架构:为什么老板不需要知道外卖员开什么车

用一个小 Demo,带你入门安卓 Clean Architecture

Android 现代架构不需要事件总线

为什么我不在 Android ViewModel 中直接处理异常?

相关推荐
齊家治國平天下5 小时前
Android 14 Vehicle HAL (VHAL) 设计与实现深度解析
android·aaos·carservice·车载开发·android 14·vhal·vehicle hal
AFinalStone11 小时前
Android 7系统网络(八)应用API层—ConnectivityManager使用与实战调试
android·网络
plainGeekDev11 小时前
kapt 替换为 KSP
android·java·kotlin
蜡台12 小时前
通过Gradle脚本声明更改Java变量
android·java·开发语言·python·kotlin·gradle·groovy
阿pin15 小时前
Android随笔-SharedPreferences
android·sp
阿pin15 小时前
Android随笔-DataStore
android·datastore
茶栀(*´I`*)16 小时前
Android 测试入门指南:ADB 基础配置与常用设备管理命令解析
android·adb
ihuyigui18 小时前
海外酒店预订短信接口
android·java·运维·服务器·前端·后端·架构
gxgldyh18 小时前
Android Framework源码解析(七):BootAnimation 启动流程解析——开机动画是如何显示出来的?
android
达达尼昂19 小时前
在 Claude Cowork 中用好 Claude Fable 5
android·人工智能·后端