我是如何把一个传统 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 中直接处理异常?

相关推荐
我重来不说话2 小时前
Android 自动化工作流平台——群控手机
android·智能手机·自动化·工作流·群控
therese_100862 小时前
安卓-触摸事件、事件分发机制及滑动冲突解决方法、CeilingNestedScrollView、常见拖拽容器设计及实现方案
android
张风捷特烈3 小时前
状态管理大乱斗#03 | Provider 源码全面评析
android·前端·flutter
鹏晨互联11 小时前
《Android 自定义 WebView 组件:从封装到路由,打造灵活可复用的混合开发利器》
android
程序员陆业聪11 小时前
AI Code Review:让每一行代码都有AI审查员
android
程序员陆业聪11 小时前
AI Bug修复与测试生成:从崩溃日志到修复PR的自动化 | AI提效Android开发(5)
android
诸神黄昏EX11 小时前
Android Google Widevine
android
HealthScience14 小时前
【Bib 2026】基因最新综述(有什么任务、benchmark、代表性模型)
android·开发语言·kotlin
夏沫琅琊15 小时前
Android拨打电话技术文档
android·kotlin