预防远大于治理:Android 架构指南

一、为什么"治理型架构"越来越吃力

很多团队谈架构时,最终都会走向"治理":

  • 技术债治理
  • 模块拆分重构
  • 大规模 Refactor
  • Code Review 补规则
  • 架构升级方案

但现实往往是:项目越大,治理成本越高;治理越多,系统越脆弱。 治理本质上是"救火",而非"防火"。古人讲"凡事豫则立,不预则废",顶尖的架构设计,从来不把希望寄托于后期的治理,而是将风险扼杀在最初的预防。


二、治理为什么总是失效?

我们来看几个 Android 项目中非常典型的失控场景,你一定似曾相识。

1. Dispatcher 失控

项目早期:

kotlin 复制代码
viewModelScope.launch(Dispatchers.IO) { ... }

后来业务复杂了:

kotlin 复制代码
withContext(Dispatchers.IO) { ... }

再后来,新来的人不确定前人有没有切线程:

kotlin 复制代码
launch(Dispatchers.Default) { ... }

最后演变成:谁都在切线程,但没人知道谁该负责线程。

2. Repository 不 main-safe

因为 Repository 没有底线,UI 层开始出现这种代码:

kotlin 复制代码
// UI 层(Fragment/ViewModel)
withContext(Dispatchers.IO) {
    repository.loadUser()
}

久而久之,IO 线程池的复杂度泄漏到了所有的调用方。几年后你会发现:系统里充斥着密密麻麻的 withContext(IO),没人敢删任何一个,因为谁也不知道去掉之后会不会报 NetworkOnMainThreadException

3. 异常处理碎片化

每一层都在 try-catch:Repository 怕崩,try 一下;UseCase 怕崩,try 一下;ViewModel 怕崩,再 try 一下。 最后变成:异常没有归属,谁都在处理,但谁都处理不完整,UI 很难做出精准的错误提示。

4. Flow 生命周期混乱

项目中同时存在 launch { }launchIn(scope)stateIn(scope)shareIn(scope)。最终问题变成:没人能解释"这个流到底是谁在管生命周期",内存泄漏和冷流重复激活动作悄然发生。


三、治理的本质:只能"止损",不能"防错"

Code Review、Lint、Sonar 这些工具本质上都是:

  1. 发现问题
  2. 限制问题扩散
  3. 降低错误概率

但它们做不到一件事:阻止错误被写出来。

架构的核心思考 优秀的架构应该反过来:让错误在设计层面就变得极其难以发生。 架构的本质,不是规定"怎么写代码",而是规定**"复杂度不能泄漏到哪里"**。不是控制写法,而是控制边界。


四、预防型架构的核心实践

要做到"预防",我们需要在各层建立铁律般的契约

1. Repository:屏蔽线程复杂度(Main-Safe)

  • 铁律 :Main-safe 是契约,不是建议。所有 Repository 暴露的 suspend 方法,必须保证可以在主线程直接调用。
kotlin 复制代码
//  治理型/错误做法:把线程调度抛给调用方,导致 IO 泄漏
class UserRepository {
    // 隐式要求调用方必须记得切线程,这里 API 是阻塞用法,外部不IO的话,会crash
    fun loadUser(): User = api.getUser().toModel() 
}

//  预防型/正确做法:Repository 内部自包含 main-safe 契约
class UserRepository(
) {
    // 调用方在主线程直接调用此方法,这里的api是retrofit 的suspend 
    suspend fun loadUser(): User =  {
        api.getUser().toModel()
    }
}

2. Repository:统一异常模型

如果每一层都 try-catch,最终异常一定碎片化。更合理的方式是在数据层(Data Layer)将异常收敛为领域模型/状态模型

kotlin 复制代码
//  治理型/错误做法:各层裸奔抛出未知 Exception,引发 try-catch 大战
// UI 层不得不写:
try { repository.loadUser() } catch(e: HttpException) { ... }

//  预防型/正确做法:利用 Kotlin 的 Result 或 自定义 Seal Class 承载结果
suspend fun loadUser(): Result<User> =  {
    try {
        Result.success(
            api.getUser().toModel()
        )
    } catch (e: CancellationException) {
        // 协程取消异常必须继续抛出
        throw e
    } catch (e: Exception) {
        // 其他异常统一转换
        Result.failure(e)
    }
}

此时 UI 层或 ViewModel 只需要极其干净的:

kotlin 复制代码
repository.loadUser()
    .onSuccess { user -> updateUI(user) }
    .onFailure { error -> showErrorMessage(error) }

3. UseCase:只做组合,不做实现

UseCase(领域层)的价值不是"为了多包一层而包一层",它的核心价值是:把业务组合从 UI 中抽离,防止 UI 膨胀。

kotlin 复制代码
//  治理型/错误做法:在 ViewModel 里临时拼装多源数据,导致业务逻辑泄漏
class UserViewModel : ViewModel() {
    fun loadProfile() {
        viewModelScope.launch {
            val user = userRepo.loadUser()
            val badge = badgeRepo.getBadge(user.id) // 缝合逻辑在 UI 层
            _uiState.value = combineData(user, badge)
        }
    }
}

//  预防型/正确做法:用 UseCase 锁死业务边界
class GetUserProfileUseCase(
    private val userRepo: UserRepository,
    private val badgeRepo: BadgeRepository
) {
    // 缝合逻辑收敛在此,UI 层(ViewModel)对底层逻辑一无所知
    suspend operator fun invoke(): Result<UserProfile> { ... }
}

4. Flow/ViewModel:明确冷热边界与职责

  • 冷流(Cold Flow):数据源(Data/Domain),只负责按需生产数据。
  • 热流(Hot Flow / StateFlow) :ViewModel 负责收敛。ViewModel 只负责状态转换,不负责调度
kotlin 复制代码
// ViewModel 保持绝对纯净:无 Dispatchers,只做状态映射
class UserViewModel(
    getUserProfileUseCase: GetUserProfileUseCase
) : ViewModel() {

    // 将底层的冷流,在 UI 顶层转换为与生命周期安全挂钩的 Hot Flow (StateFlow)
    val uiState: StateFlow<UserUiState> = flow { emit(getUserProfileUseCase()) }
        .map { result -> 
            result.fold(
                onSuccess = { UserUiState.Success(it) },
                onFailure = { UserUiState.Error(it) }
            )
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000), // 预防后台重建耗费资源
            initialValue = UserUiState.Loading
        )
}

五、架构的真正目标:复杂度收敛

所有设计原则最终都指向同一件事:把复杂度收敛到正确的层,而不是扩散到所有层。

核心问题 错位蔓延(治理型) 正确收敛位置(预防型)
线程切分 泄漏到 ViewModel、Fragment、甚至 Adapter Data 层 (Repository) 内部
异常拦截 每一层都在 try-catch,异常信息断层 Data 层统一拦截,转换为 Result
业务组合 在 ViewModel 甚至是 View 中强行缝合数据 Domain 层 (UseCase)
UI 状态 散落在各个变量中、或者在各层随意 stateIn ViewModel 聚合为唯一 UI State

六、预防型架构的代价

世界没有免费的午餐,预防型架构同样有代价。

实行严苛的预防型架构,意味着在项目初期或写简单业务时,你需要写看似多余的封装(如:每个简单业务也要写 UseCase、要写 Result 包装、要注入 Dispatcher)。部分追求"快"的同学可能会觉得这加重了心智负担。

但这正是技术专家与普通程序员的边界所在:架构师的职责,就是用前期的"设计/约束成本",去对冲后期的"灾难治理成本"。 这是一笔稳赚不赔的长远技术投资。


七、结语

很多人理解架构的价值在于:卓越的重构能力、高超的演进能力、强大的治理能力。

但更本质的是:优秀的架构 就是让技术型Bug很难出现。

Main-safe、统一异常模型、清晰职责边界、稳定数据流......这些不是什么精妙的"高级套路",而是系统最基础的预防机制

预防远大于治理, 完。

推荐团队技术负责人将其作为内部架构规范与 Code Review 的底层原则参考。

Main-safe:现代Android架构真正的分水岭

"结构化"这个词,本质上就是------把混乱的东西变成有组织、有规则、有边界的东西

从"调用方的如履薄冰"到"接口的天然语义":Room/DataStore/Retrofit 的启示

现代 Android 官方为什么更推荐 Repository 暴露 suspend fun,而不是在内部 launch

# Android 架构指南之Data 层不要再暴露 start/stop 了:用 Flow 接管生命周期

别再 launch(IO) 了:协程线程切换的 3隐藏反模式

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

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

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

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

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui