Administrator
发布于 2026-08-25 / 25 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 70 篇:Android 应用架构:从 MVP 到 Clean Architecture

上一篇讲了 Scoped Storage,回到工程方法学——Android 应用架构。这是系列文章第 70 篇,正好用一篇文章把"架构演进"系统梳理一遍。从 MVP 到 MVVM 到 MVI 到 Clean Architecture,每个阶段的痛点、收益、适用场景都讲清楚。

为什么架构重要

好的架构不解决"代码能跑"的问题,解决"代码能持续演进"的问题。架构差的项目常见表现:

现象

原因

加新功能要改 20 个文件

没有清晰的分层

单元测试覆盖 30% 跑不起来

业务逻辑和 Android 框架耦合

一个类 2000 行

没有单一职责

改一个 P0 bug 引发 5 个 P0

调用链路乱

团队新人看不懂项目

缺乏统一约定

架构的本质:约束。让团队"不那么容易写烂代码"。

演进路径

阶段 1:直接写 Activity(2008-2012 早期)

class LoginActivity : AppCompatActivity() {
    fun onLoginClick() {
        val username = findViewById<EditText>(R.id.username).text.toString()
        val password = findViewById<EditText>(R.id.password).text.toString()
        
        // 网络请求
        Thread {
            val response = HttpURLConnection(...)
            // ...
        }.start()
    }
}

优点:直观。

缺点:

  • 业务逻辑和 UI 强耦合

  • 不能测试

  • 复用差

  • 内存泄漏(Thread + Activity 引用)

阶段 2:MVC(2012-2014)

Model-View-Controller 把代码分层:

Model - 数据(POJO)
View - XML + Activity(同时也是 Controller)
Controller - Activity

实践中 Controller 和 View 重合,没有解决耦合。Android 的 MVC 实质上是"假分层",这阶段架构意义不大。

阶段 3:MVP(2014-2017)

Model-View-Presenter:

Model - 数据
View - Activity(被动接收指令)
Presenter - 处理业务逻辑
interface LoginContract {
    interface View {
        fun showLoading()
        fun showError(message: String)
        fun navigateToHome()
    }
    
    interface Presenter {
        fun attach(view: View)
        fun detach()
        fun onLoginClick(username: String, password: String)
    }
}

class LoginPresenter : LoginContract.Presenter {
    private var view: LoginContract.View? = null
    
    override fun attach(view: LoginContract.View) {
        this.view = view
    }
    
    override fun detach() {
        this.view = null
    }
    
    override fun onLoginClick(username: String, password: String) {
        view?.showLoading()
        loginUseCase(username, password) { result ->
            view?.showLoading(false)
            when (result) {
                is Success -> view?.navigateToHome()
                is Failure -> view?.showError(result.error)
            }
        }
    }
}

MVP 真正把业务逻辑从 Android 抽出来了,可以写 Presenter 单元测试。

缺点:契约 Interface 写起来繁琐;Contract 通常放在一个文件上百行。

阶段 4:MVVM(2017-2020)

Model-View-ViewModel:

class LoginViewModel : ViewModel() {
    private val _loading = MutableLiveData<Boolean>()
    val loading: LiveData<Boolean> = _loading
    
    private val _error = MutableLiveData<String?>()
    val error: LiveData<String?> = _error
    
    fun onLoginClick(username: String, password: String) {
        viewModelScope.launch {
            _loading.value = true
            try {
                loginUseCase(username, password)
                _loading.value = false
                // 跳转
            } catch (e: Exception) {
                _loading.value = false
                _error.value = e.message
            }
        }
    }
}

class LoginActivity : AppCompatActivity() {
    private val vm: LoginViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        
        vm.loading.observe(this) { showLoading(it) }
        vm.error.observe(this) { showError(it) }
    }
}

MVVM 的革命性突破:

  • 双向绑定(DataBinding)实现 View 自动化

  • LiveData 自动响应生命周期

  • ViewModel 跨配置变化保留状态

现在已经成为大多数 Android 项目的默认架构。

阶段 5:MVI(2020-2025)

参考前几篇详谈的 MVI(Orbit 实战):

  • 单一状态对象

  • Reducer 集中处理

  • 副作用显式表达

阶段 6:Clean Architecture(Uncle Bob / Ivar 大神)

把"业务实体"放在架构中心,与框架无关:

[Entities]  <- [Use Cases]  <- [Interface Adapters]  <- [Frameworks]

最内层:纯业务实体(领域模型)
中间层:用例(应用特有的业务规则)
外层:接口适配器(Repository 实现、Presenter、ViewModel)
最外层:框架(Android Framework、数据库、网络)

依赖方向:外向内(外层依赖内层)。最内层不知道外层存在。

Clean Architecture 在 Android 的实现

代码结构:

app/
├── presentation/                # UI 层
│   ├── login/
│   │   ├── LoginActivity.kt
│   │   ├── LoginViewModel.kt
│   │   ├── LoginState.kt
│   │   └── LoginIntent.kt
├── domain/                      # 业务层(纯 Kotlin)
│   ├── model/
│   │   └── User.kt
│   ├── repository/              # 接口(不要实现)
│   │   └── UserRepository.kt
│   └── usecase/
│       ├── LoginUseCase.kt
│       └── GetUserUseCase.kt
├── data/                        # 数据层(实现 domain 接口)
│   ├── repository/              
│   │   └── UserRepositoryImpl.kt
│   ├── remote/                  # Retrofit API
│   │   └── UserApi.kt
│   ├── local/                   # Room Database
│   │   └── UserDao.kt
│   └── mapper/                  # DTO ↔ Domain 转换
│       └── UserMapper.kt

关键代码

domain/model/User.kt:

data class User(
    val id: String,
    val name: String,
    val email: String
)

domain/repository/UserRepository.kt(接口):

interface UserRepository {
    suspend fun login(username: String, password: String): Result<User>
    suspend fun getUser(id: String): User
    suspend fun saveUser(user: User)
}

domain/usecase/LoginUseCase.kt:

class LoginUseCase(private val userRepository: UserRepository) {
    suspend operator fun invoke(username: String, password: String): Result<User> {
        if (!isValid(username, password)) {
            return Result.failure(IllegalArgumentException("Invalid credentials"))
        }
        return userRepository.login(username, password)
    }
    
    private fun isValid(username: String, password: String): Boolean {
        return username.contains("@") && password.length >= 6
    }
}

data/repository/UserRepositoryImpl.kt:

class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao
) : UserRepository {
    override suspend fun login(username: String, password: String): Result<User> {
        return try {
            val response = api.login(username, password)
            val user = response.toDomain()
            dao.insert(user.toEntity())
            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

关键点:Domain 不依赖 Android

domain 目录是纯 Kotlin,不应该 import android.*。

// 错
import android.content.Context
import androidx.room.Room

class UserRepository {
    fun getDatabase(context: Context) { ... } // domain 永远不应该这样写
}

这个约束保证业务逻辑可以独立测试、跨平台共享(KMP)。

依赖注入让层级清晰

用 Hilt 连接各层:

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    abstract fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}

class LoginViewModel @Inject constructor(
    private val loginUseCase: LoginUseCase
) : ViewModel() { ... }

@Binds 让 domain 接口和 data 实现解耦,ViewModel 只知道 interface。

测试策略

Clean Architecture 最大的收益是可测试:

层级

测试类型

速度

domain

纯 JVM 单元测试

极快

data

Repository 测试(Mock API)

快

presentation

ViewModel 单元测试

快

// domain 单元测试
class LoginUseCaseTest {
    @Test
    fun `invalid credentials returns failure`() = runTest {
        val useCase = LoginUseCase(FakeUserRepository())
        
        val result = useCase("invalid", "123")
        
        assertTrue(result.isFailure)
    }
}

不需要 Robolectric、不需要真机。毫秒级跑完。

Clean Architecture 的真实收益

收益

实证

单元测试覆盖率

80%+ 容易达到

业务逻辑复用

KMP 共享

框架升级

改动集中在 presentation 层

团队协作

分层清晰,新人易上手

性能优化

替换 UseCase/Repository 实现,不动 ViewModel

Clean Architecture 的代价

代价

现实

文件多

小项目 5 倍文件数

Boilerplate

Mapper、Interface 维护

学习曲线

团队要懂 DI、Mapper、分层

过度设计

CRDU App 不需要

结论:业务逻辑超过 10 个 UseCase 的项目才有 Clean 的价值。

选型决策树

项目复杂度:
  低(CRUD、个人项目)
    → MVVM(含 Compose 直接用 ViewModel)
    
  中(业务较复杂)
    → MVVM + Repository + UseCase(轻量 Clean)
    → MVI(如果状态复杂)
    
  高(金融、电商、生产工具)
    → Clean Architecture + MVI + 多模块

不要盲目追求最复杂的架构。架构服务于业务,不是业务的展示。

模块化演进

代码越来越多时,单 module 不够用:

:app                # 主入口
:feature:login      # 登录模块
:feature:home       # 首页模块
:feature:profile    # 个人中心模块
:core:network       # 网络基础设施
:core:database      # 数据库基础设施
:core:designsystem  # 设计系统

各模块通过 DI(Hilt、Koin)通信,避免互相依赖。

模块化收益:

  • 编译时间下降 60%(只编译改的模块)

  • 强制模块边界

  • 团队可并行开发不同模块

多 module 的真实坑

  1. 包名重复:每个 module 有独立包名,避免误访问内部类

  2. 依赖管理:用 Version Catalogs 统一版本

  3. 模块间通信:通过 EventBus 或 Flow,而不是直接依赖

  4. 测试:每个模块独立跑测试

  5. 构建脚本重复:用 convention plugins 抽公共配置

终极建议

  1. 新项目:MVVM + Compose + Repository,复杂度中等再加 UseCase

  2. 复杂业务:Clean Architecture 起步,不要一开始就重

  3. MVI:复杂状态场景(订单、IM、电商详情)用,普通页面 MVVM 足够

  4. 多模块:代码超过 3 万行就考虑

  5. 测试:单元测试覆盖业务 UseCase,UI 测试覆盖关键流程

小结

这是系列文章第 70 篇。Android 应用架构没有"银弹",从 MVP 到 Clean,每个阶段都是为了让代码"更可持续"。建议团队写一份《架构规范》,明确:

  • 哪些层用什么模式

  • 模块划分规则

  • 测试要求

  • 命名规范

让架构约束变成团队的"肌肉记忆",远比每次讨论用什么模式高效得多。

接下来会进入系列文章的第三阶段:从架构扩展到大模型客户端集成、车机更深的内容、更前沿的系统服务。保持阅读。


评论