Android 开发中,协程几乎已经成为异步编程的默认选择。但“使用协程”并不等于项目天然具备一致、可靠的异步模型。
很多项目在长期演进过程中都会出现类似情况:新的代码使用 suspend 和 Flow,旧模块仍然依赖 Callback、LiveData、RxJava 或 Java Future;有些人使用 Mutex 管理共享状态,有些人继续使用 synchronized 或 ReentrantLock;方法虽然标记为 suspend,内部却调用 Thread.sleep()。 之前写过Android Data 层设计的四条红线:为什么必须坚持、如何落地没有看过的,可以先看看这个。
RedLine 就是围绕这一问题构建的一个 Android 示例项目。
我不试图提供完整业务功能,而是通过自定义 Detekt 规则,将团队关于数据层异步与并发的约定转化为自动执行的构建规则。
它的目标是:
让不符合团队约定的代码无法通过静态检查。
一、项目定位:从口头规范到自动化约束
团队规范经常以文档、Code Review 意见或者经验传承的形式存在。
例如:
- Repository 应该使用协程接口;
- 不要在协程中阻塞线程;
- 不要在新代码中使用 Java 锁;
- 不要在数据层返回
LiveData; - 不要开启 Room 主线程查询。
这些约定最大的问题是:依赖人工记忆和人工审查。
随着团队扩大、需求变快、模块增多,代码 Review 很难保证每一次都能识别全部风险。
RedLine 选择了另一种方式:
把规范直接编码成 Detekt Rule。
Detekt 是 Kotlin 生态中的静态分析工具。通过扩展 Detekt,可以编写自定义 Rule,对 Kotlin 源码进行扫描。
只要发现不符合团队规范的代码,就产生问题;再配合:
1build: 2 maxIssues: 0 3
就可以让违规代码直接导致构建失败。
于是:
1团队规范 2 ↓ 3Detekt Rule 4 ↓ 5静态分析 6 ↓ 7发现违规 8 ↓ 9构建失败 10
规范就从“建议”变成了可执行的工程约束。
二、整体架构(Demo)
项目主要包含两个模块:
1RedLine 2├── app 3│ ├── MainActivity 4│ ├── CorrectRepository 5│ └── SampleRepository 6│ 7├── detekt-rules 8│ ├── RepositoryPublicApiSuspendOrFlowRule 9│ ├── ForbiddenBlockingQueueApiRule 10│ ├── ForbiddenJavaLockApiRule 11│ ├── ForbiddenPseudoAsyncRule 12│ └── RepositorySuspendOrFlowRuleSetProvider 13│ 14└── config 15 └── detekt 16 └── detekt.yml 17
app 模块是规则的使用方。
其中包含:
- 合规的
CorrectRepository; - 故意违规的
SampleRepository。
detekt-rules 则是规则实现模块。
它使用 Detekt API 定义自定义 Rule,并通过 RuleSetProvider 将多个规则注册成:
1repository-rules 2
应用模块通过:
1dependencies { 2 detektPlugins(project(":detekt-rules")) 3} 4
加载自定义规则。
因此执行 Detekt 时,不仅会执行官方规则,还会执行项目自己的架构规则。
三、第一条红线:Repository 公开接口必须是 suspend 或 Flow
Repository 是 Data 层对上层暴露能力的边界。
这个边界一旦不统一,调用层就必须不断适配不同的异步模型。
例如:
1fun getUser(): User 2 3fun getUserAsync(callback: (User) -> Unit) 4 5fun getUserFuture(): CompletableFuture<User> 6 7fun getUserLiveData(): LiveData<User> 8 9suspend fun getUserById(id: String): User 10
一个 Repository 同时存在这么多异步模型,会导致:
- 调用方需要记住不同 API 的调用方式;
- 异常传播机制不一致;
- 取消机制不一致;
- ViewModel 被迫承担适配职责;
- 后续架构迁移成本增加。
因此,RedLine 的第一条规则是:
Repository 的公开方法必须是
suspend,或者返回Flow。
例如:
1class UserRepository { 2 fun getUser(): User = TODO() 3} 4
这个方法会被判定为违规。
正确写法:
1class UserRepository { 2 suspend fun getUser(): User = TODO() 3} 4
或者:
1class UserRepository { 2 fun observeUser(): Flow<User> = TODO() 3} 4
规则同时识别这些不建议直接从 Data 层暴露的类型:
1LiveData 2CompletableFuture 3Single 4Observable 5Callback 6
例如:
1fun getUser(): LiveData<User> 2
或者:
1fun getUser(): CompletableFuture<User> 2
都会被拦截。
真正的设计意图是:
统一 Data 层的异步模型。
一个比较清晰的职责划分是:
1Data 2 ↓ 3suspend / Flow 4 ↓ 5Domain 6 ↓ 7suspend / Flow 8 ↓ 9ViewModel 10 ↓ 11StateFlow / UI State 12 ↓ 13UI 14 ↓ 15Compose State 16
LiveData 本身带有 Android 生命周期语义,更适合 ViewModel 与 UI 的边界,而不是进入纯数据层。
四、第二条红线:禁止 BlockingQueue
Java 的 BlockingQueue 体系包括:
1BlockingQueue 2LinkedBlockingQueue 3ArrayBlockingQueue 4SynchronousQueue 5
它们的核心特点是:
当操作无法立即完成时,调用线程可能被阻塞。
例如:
1private val queue = LinkedBlockingQueue<String>() 2 3fun produce() { 4 queue.put("message") 5} 6
如果队列已满,put() 就可能阻塞当前线程。
在以协程为主要异步模型的 Android 项目中,这通常不是理想的设计。
因此 RedLine 会扫描这些阻塞队列类型,并提示使用协程模型下的替代方案。
例如:
1private val channel = Channel<String>() 2 3suspend fun produce(message: String) { 4 channel.send(message) 5} 6 7suspend fun consume(): String { 8 return channel.receive() 9} 10
当 send() 或 receive() 暂时无法继续时,协程可以挂起,而不是让线程一直等待。
如果需求本质上是状态或数据流,也可以使用:
1private val events = MutableSharedFlow<String>() 2 3suspend fun emitEvent(event: String) { 4 events.emit(event) 5} 6 7fun observeEvents(): Flow<String> = events 8
当然:
1Channel 2StateFlow 3SharedFlow 4Flow 5
并不是完全等价的。
可以简单理解为:
1Channel 2 → 协程消息传递 3 4StateFlow 5 → 当前状态 6 7SharedFlow 8 → 多订阅者事件/数据广播 9 10Flow 11 → 数据流抽象 12
所以 RedLine 的目标不是机械地要求:
BlockingQueue 一律替换成 Channel。
而是:
禁止阻塞式队列进入以协程为核心的异步架构。
具体使用 Channel、StateFlow、SharedFlow 还是 Flow,应由业务语义决定。
五、第三条红线:禁止 Java 锁和阻塞同步原语
RedLine 禁止以下典型 Java 并发 API:
1ReentrantLock 2Lock 3Condition 4CountDownLatch 5Semaphore 6CyclicBarrier 7synchronized 8@Synchronized 9wait 10notify 11notifyAll 12
例如:
1private val lock = ReentrantLock() 2 3fun updateCache() { 4 lock.withLock { 5 // 修改共享状态 6 } 7} 8
或者:
1@Synchronized 2fun updateCache() { 3 // 修改共享状态 4} 5
这些 API 在传统 Java 多线程模型中非常常见。
但在协程模型下,需要特别关注一个问题:
等待同步资源时,是否阻塞了底层线程?
协程环境中可以使用:
1private val mutex = Mutex() 2 3suspend fun updateCache() { 4 mutex.withLock { 5 // 修改共享状态 6 } 7} 8
当协程暂时无法获取 Mutex 时,它可以挂起,而不是持续占用线程等待。
六、第四条红线:禁止伪异步
这是 RedLine 中非常重要的一条规则。
很多代码看起来已经使用协程:
1suspend fun refresh() 2
但内部却仍然是阻塞操作:
1suspend fun refresh() { 2 Thread.sleep(2_000) 3} 4
问题在于:
1suspend 2
并不意味着:
1不会阻塞线程 2
suspend 只是允许函数挂起。
如果函数内部执行:
1Thread.sleep() 2
当前线程仍然会被阻塞。
如果运行在主线程,会导致 UI 卡顿甚至 ANR。
如果运行在线程池,也会降低线程池吞吐能力。
因此:
1suspend fun refresh() { 2 delay(2_000) 3} 4
才符合协程模型。
delay() 会挂起协程,并把线程让出来。
所以 RedLine 将:
1Thread.sleep() 2
定义为伪异步红线。
八、Detekt 规则注册
自定义 Detekt Rule 需要经过规则注册才能真正参与分析。
RedLine 使用 RuleSetProvider 将多个 Rule 组织成一个规则集:
1class RepositorySuspendOrFlowRuleSetProvider : RuleSetProvider { 2 3 override val ruleSetId: String = "repository-rules" 4 5 override fun instance(config: Config): RuleSet = RuleSet( 6 ruleSetId, 7 listOf( 8 RepositoryPublicApiSuspendOrFlowRule(config), 9 ForbiddenBlockingQueueApiRule(config), 10 ForbiddenJavaLockApiRule(config), 11 ForbiddenPseudoAsyncRule(config) 12 ) 13 ) 14} 15
然后在 detekt.yml 中启用:
1repository-rules: 2 active: true 3 4 RepositoryPublicApiSuspendOrFlow: 5 active: true 6 7 ForbiddenBlockingQueueApi: 8 active: true 9 10 ForbiddenJavaLockApi: 11 active: true 12 13 ForbiddenPseudoAsync: 14 active: true 15
同时设置:
1build: 2 maxIssues: 0 3
这意味着:
只要发现一条红线违规,Detekt 就失败。
对于普通代码质量规则,可以接受一定数量的问题。
但对于团队已经明确认定的架构红线:
10 2
反而是最合理的配置。
九、一次真实的 Detekt 执行结果
1./gradlew detekt 2
Gradle 会分析 app 模块中的 Kotlin 文件,同时加载 detekt-rules 中定义的 repository-rules。
本次执行共分析:
1RepositoryPublicApiSuspendOrFlow - [红线一违规:Repository 公开方法 `getRawData` 必须是 [`suspend`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.suspend.md) 或返回 `Flow`。] at RedLine/app/src/main/java/com/sample/redline/data/violations/SampleRepository.kt:22:5 2 RepositoryPublicApiSuspendOrFlow - [红线一违规:`getLiveData` 禁止在 Data 层返回 LiveData/Rx/Future 类型。] at RedLine/app/src/main/java/com/sample/redline/data/violations/SampleRepository.kt:25:5 3 RepositoryPublicApiSuspendOrFlow - [红线一违规:`getFutureData` 禁止在 Data 层返回 LiveData/Rx/Future 类型。] 4
例如,对于同步 Repository 方法,规则会给出类似提示:
1红线一违规:Repository 公开方法 getRawData 2必须是 suspend 或返回 Flow。 3
如果数据层直接返回 LiveData:
1红线一违规:getLiveData 2禁止在 Data 层返回 LiveData/Rx/Future 类型。 3
如果使用:
1LinkedBlockingQueue 2
则会提示:
1红线二违规:检测到阻塞队列 LinkedBlockingQueue。 2请替换为 Channel 或 Flow。 3
如果使用:
1ReentrantLock 2
则会提示:
1红线三违规:检测到 Java 锁 API ReentrantLock。 2请替换为 kotlinx.coroutines.sync.Mutex。 3
而对于:
1suspend fun pseudoAsync() { 2 Thread.sleep(2000) 3} 4
则会提示:
1红线四违规:禁止在 suspend 函数中使用 Thread.sleep, 2请使用 delay。 3
读者请自行运行测试
十、合规示例与违规示例
项目中包含两个非常重要的 Repository。
CorrectRepository
用于展示符合规范的代码:
1class CorrectRepository { 2 3 private val mutex = Mutex() 4 private val channel = Channel<String>() 5 6 suspend fun fetchData(): String { 7 delay(100) 8 return "Good Data" 9 } 10 11 fun observeData(): Flow<String> = flow { 12 emit("Good Flow Data") 13 } 14 15 suspend fun produceItem(item: String) { 16 channel.send(item) 17 } 18 19 suspend fun safeOperation() { 20 mutex.withLock { 21 // 协程安全的临界区 22 } 23 } 24} 25
它体现了四条红线对应的替代方案:
1同步 API 2 ↓ 3suspend 4 5阻塞队列 6 ↓ 7Channel / Flow 8 9Java Lock 10 ↓ 11Mutex 12 13Thread.sleep 14 ↓ 15delay 16
SampleRepository
而 SampleRepository 则是一个故意设计出来的错误示例。
例如:
1fun getRawData(): String = "Bad" 2 3fun getLiveData(): LiveData<String> = 4 MutableLiveData() 5 6fun getFutureData(): CompletableFuture<String> = 7 CompletableFuture.completedFuture("Bad") 8
以及:
1suspend fun pseudoAsync() { 2 Thread.sleep(2000) 3} 4
它的存在并不是为了成为真正的业务代码,而是为了验证:
规则能不能真正抓到这些问题。
所以直接执行:
1./gradlew :app:detekt 2
失败是预期结果。
十一、为什么这种方式比 Code Review 更可靠?
Code Review 仍然非常重要。
但 Code Review 有一个天然限制:
人会遗漏问题。
例如一个团队已经规定:
1Repository 不允许 LiveData 2
那么每一次 Review 都可能出现:
1Reviewer A:这个 LiveData 不应该放 Repository。 2 3Developer:好的,我改掉。 4
几个月后,又出现:
1Reviewer B:这里为什么又用了 LiveData? 2
这种重复讨论本质上是在浪费工程资源。
如果把它写成 Detekt Rule:
1Repository 2 + 3LiveData 4 ↓ 5直接失败 6
开发者第一次提交代码就会收到反馈。
于是 Code Review 的关注点可以从:
1“你这里为什么用了 LiveData?” 2
提升到:
1“这个 Repository 的职责边界应该怎么设计?” 2
也就是说:
静态检查负责守住机械规则,Code Review 负责讨论真正的设计问题。
十二、如何在真实项目中落地
如果要把 RedLine 的思想引入真实 Android 项目,可以按照几个阶段推进。
第一阶段:确定少量红线
不要一开始就增加几十条规则。
优先选择:
1线程阻塞 2并发错误 3架构边界 4高风险 API 5
例如:
1Repository 不允许同步 API 2Repository 不允许 LiveData 3禁止 Thread.sleep 4禁止 BlockingQueue 5禁止 Java Lock 6禁止 allowMainThreadQueries 7
这些规则的收益非常明确。
第二阶段:独立规则模块
将:
1detekt-rules 2
独立出来。
这样可以让多个 Android 项目共享:
1company-detekt-rules 2
例如:
1Project A 2 ↓ 3company-detekt-rules 4 5Project B 6 ↓ 7company-detekt-rules 8 9Project C 10 ↓ 11company-detekt-rules 12
最终形成团队统一的工程规范。
第三阶段:为 Rule 编写测试
Detekt Rule 本身也是代码。
因此必须测试:
1应该报错 2 ↓ 3必须报错 4 5不应该报错 6 ↓ 7不能误报 8
否则静态检查工具本身就可能制造大量噪声。
第五阶段:治理历史代码
大型项目通常存在大量遗留代码。
不应该直接要求:
1今天开始 2所有历史代码必须全部符合规则 3
更现实的方案是:
1旧代码 2 ↓ 3逐模块治理 4 5新代码 6 ↓ 7立即执行红线 8
最终逐渐把历史技术债清理掉。
失败是必然的。
对于真实项目,更合理的方式是将违规示例放入:
1detekt-rules-test 2
或者:
1lint-fixtures 2
或者独立 Demo 模块。
这样既可以验证规则,又不会让正常应用的 CI 永远处于失败状态。
十三、RedLine 真正解决的是什么问题?
表面上看,RedLine 解决的是:
1禁止 LiveData 2禁止 BlockingQueue 3禁止 Java Lock 4禁止 Thread.sleep 5
但更深层的问题其实是:
统一团队的异步模型和并发模型。
一个没有约束的 Android 项目,很容易逐渐变成:
1Callback 2 + 3LiveData 4 + 5RxJava 6 + 7Future 8 + 9suspend 10 + 11Flow 12 + 13BlockingQueue 14 + 15ReentrantLock 16 + 17synchronized 18 + 19Mutex 20
每一种方案单独看都可能有合理的使用场景。
但放在同一个 Data Layer 中,最终会形成:
1异步模型不统一 2 ↓ 3线程语义不统一 4 ↓ 5异常模型不统一 6 ↓ 7取消模型不统一 8 ↓ 9测试方式不统一 10 ↓ 11维护成本不断增加 12
RedLine 做的事情,就是在架构边界上建立一道防线:
1 ┌──────────────┐ 2 │ UI 层 │ 3 └──────┬───────┘ 4 ↓ 5 ┌──────────────┐ 6 │ ViewModel │ 7 │ StateFlow │ 8 └──────┬───────┘ 9 ↓ 10 ┌──────────────┐ 11 │ Domain │ 12 │ suspend/Flow │ 13 └──────┬───────┘ 14 ↓ 15 ┌────────────────────┐ 16 │ Data │ 17 │ suspend/Flow │ 18 └─────────┬──────────┘ 19 ↓ 20 ┌──────────────┐ 21 │ RedLine │ 22 │ Detekt │ 23 └──────────────┘ 24
一旦出现:
1LiveData 2Callback 3Future 4BlockingQueue 5ReentrantLock 6Thread.sleep 7allowMainThreadQueries 8
就由静态检查直接拦截。
十四、结语
RedLine 展示的并不是某一个 Kotlin API 的使用技巧,而是一种更重要的工程治理方式:
把架构原则固化成可执行规则。
当 Repository 的异步模型统一为:
1suspend + Flow 2
当:
1BlockingQueue 2Java Lock 3Thread.sleep 4allowMainThreadQueries 5
在提交阶段就被自动拦截时,团队就不需要在每一次 Code Review 中重复讨论同一类基础问题。
更重要的是,RedLine 证明了一件事情:
1架构规范 2 ↓ 3代码化 4 ↓ 5静态分析 6 ↓ 7自动阻断 8
这条链路是可以真正落地的。
源码:RedLine