预防远大于治理: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 中直接处理异常?

相关推荐
恋猫de小郭1 小时前
Flutter 3.47 发布,快来看看有什么更新吧
android·前端·flutter
2501_937860943 小时前
MySQL表的操作:创建、查看、修改、删除完整实战
android·数据库·mysql
我就是妖怪13 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·人工智能
心平气和量大福大14 小时前
android-实例-蒲公英-更新与安装-5-签名与生成APK
android·java·开发语言
阿pin15 小时前
Android随笔-Serializable与Parcelable
android·serializable·parcelable
天空之城--16 小时前
Android 事件分发机制深度解析——原理、源码与实战综合总结
android
阿pin16 小时前
Android随笔-AIDL
android·开发语言·aidl
2501_9160074717 小时前
Bundle ID 注册与管理 App ID 创建指南 命名规则 权限开关与删除注意事项
android·ios·小程序·https·uni-app·iphone·webview
雨白17 小时前
面向对象六大基本原则实战:从零重构支持引擎切换的网络框架
android·架构