上一篇讲了 Scoped Storage,回到工程方法学——Android 应用架构。这是系列文章第 70 篇,正好用一篇文章把"架构演进"系统梳理一遍。从 MVP 到 MVVM 到 MVI 到 Clean Architecture,每个阶段的痛点、收益、适用场景都讲清楚。
为什么架构重要
好的架构不解决"代码能跑"的问题,解决"代码能持续演进"的问题。架构差的项目常见表现:
架构的本质:约束。让团队"不那么容易写烂代码"。
演进路径
阶段 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 单元测试
class LoginUseCaseTest {
@Test
fun `invalid credentials returns failure`() = runTest {
val useCase = LoginUseCase(FakeUserRepository())
val result = useCase("invalid", "123")
assertTrue(result.isFailure)
}
}
不需要 Robolectric、不需要真机。毫秒级跑完。
Clean Architecture 的真实收益
Clean Architecture 的代价
结论:业务逻辑超过 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 的真实坑
包名重复:每个 module 有独立包名,避免误访问内部类
依赖管理:用 Version Catalogs 统一版本
模块间通信:通过 EventBus 或 Flow,而不是直接依赖
测试:每个模块独立跑测试
构建脚本重复:用 convention plugins 抽公共配置
终极建议
新项目:MVVM + Compose + Repository,复杂度中等再加 UseCase
复杂业务:Clean Architecture 起步,不要一开始就重
MVI:复杂状态场景(订单、IM、电商详情)用,普通页面 MVVM 足够
多模块:代码超过 3 万行就考虑
测试:单元测试覆盖业务 UseCase,UI 测试覆盖关键流程
小结
这是系列文章第 70 篇。Android 应用架构没有"银弹",从 MVP 到 Clean,每个阶段都是为了让代码"更可持续"。建议团队写一份《架构规范》,明确:
哪些层用什么模式
模块划分规则
测试要求
命名规范
让架构约束变成团队的"肌肉记忆",远比每次讨论用什么模式高效得多。
接下来会进入系列文章的第三阶段:从架构扩展到大模型客户端集成、车机更深的内容、更前沿的系统服务。保持阅读。