一、为什么我坚持用MVVM + Clean Architecture?
说实话,刚入行那几年我也踩过不少坑。项目越做越大,Activity里塞了上千行代码,改一个需求要翻半天文件。那时候我就想,一定得找个靠谱的架构。
后来接触了MVVM和Clean Architecture,一试就回不去了。说白了,这套组合拳解决的是三个核心问题:
- 代码职责清晰 —— 每个文件只管一件事
- 可测试性拉满 —— 业务逻辑脱离Android框架
- 维护成本低 —— 改需求时不用牵一发动全身
我在团队里推这套架构时,一开始也有人嫌麻烦。但跑完一个迭代后,大家都真香了。
二、项目结构长什么样?
先给你看看我习惯的包结构。嗯,这里要注意,包名不是随便起的,每一层都有它的使命。
com.example.cleanmvvm/
├── data/ # 数据层
│ ├── local/ # 本地数据源(Room、SharedPreferences)
│ ├── remote/ # 远程数据源(Retrofit)
│ └── repository/ # 仓库实现
├── domain/ # 领域层
│ ├── model/ # 业务模型
│ ├── repository/ # 仓库接口
│ └── usecase/ # 用例
├── presentation/ # 表现层
│ ├── viewmodel/ # ViewModel
│ ├── ui/ # 页面、Fragment、Adapter
│ └── di/ # 依赖注入
└── di/ # 全局DI配置
你可能会问,为什么domain层要单独抽出来?
我举个例子你就明白了。有一次项目要从本地数据切到远程API,如果业务逻辑写在ViewModel里,那得改两个地方。但放在domain层的UseCase里,只需要换仓库实现就行。这就是Clean Architecture的威力。
三、核心依赖怎么选?
选库这件事,我个人习惯是「够用就好,别追新」。下面是我常用的技术栈:
| 层级 | 技术选型 | 说明 |
|---|---|---|
| DI | Hilt | Google官方推荐,上手快 |
| 网络 | Retrofit + OkHttp | 稳定,生态好 |
| 本地存储 | Room | 类型安全,支持Flow |
| 异步 | Kotlin Coroutines + Flow | 轻量,协程真香 |
| 图片加载 | Coil | Kotlin原生,体积小 |
| 导航 | Navigation Component | 官方支持,类型安全 |
小提示: 如果你项目刚起步,别一股脑全加上。先搭好骨架,再按需引入。我曾经见过一个空项目就引了20多个依赖,结果编译一次要5分钟。
四、数据流怎么走?
这是整个架构的灵魂。我画了一张图,你看完就明白了:
看到没?数据是单向流动的。UI只观察ViewModel暴露的State,ViewModel调用UseCase,UseCase再通过Repository接口拿数据。每一层都只依赖它的下一层,绝不越级。
核心原则: 依赖倒置。domain层不依赖任何Android框架,它只定义接口。这样换数据库、换网络库,domain层纹丝不动。
五、实战:写一个登录模块
光说不练假把式。咱们用登录功能走一遍完整流程。
5.1 定义业务模型
// domain/model/User.kt
data class User(
val id: String,
val name: String,
val email: String,
val token: String
)
5.2 定义仓库接口
// domain/repository/AuthRepository.kt
interface AuthRepository {
suspend fun login(username: String, password: String): Result<User>
suspend fun logout()
}
5.3 写UseCase
// domain/usecase/LoginUseCase.kt
class LoginUseCase(
private val authRepository: AuthRepository
) {
suspend operator fun invoke(username: String, password: String): Result<User> {
// 这里可以做参数校验
if (username.isBlank() || password.isBlank()) {
return Result.failure(IllegalArgumentException("用户名或密码不能为空"))
}
return authRepository.login(username, password)
}
}
注意: UseCase里不要直接处理UI逻辑。我曾经见过有人在UseCase里更新UI状态,结果测试时根本没法mock。UseCase只做一件事:协调业务逻辑。
5.4 实现仓库
// data/repository/AuthRepositoryImpl.kt
class AuthRepositoryImpl(
private val remoteDataSource: AuthRemoteDataSource,
private val localDataSource: AuthLocalDataSource
) : AuthRepository {
override suspend fun login(username: String, password: String): Result<User> {
return try {
val response = remoteDataSource.login(username, password)
localDataSource.saveUser(response.toUser())
Result.success(response.toUser())
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun logout() {
localDataSource.clearUser()
}
}
5.5 ViewModel层
// presentation/viewmodel/LoginViewModel.kt
@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<LoginUiState>(LoginUiState.Idle)
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun login(username: String, password: String) {
viewModelScope.launch {
_uiState.value = LoginUiState.Loading
val result = loginUseCase(username, password)
_uiState.value = result.fold(
onSuccess = { LoginUiState.Success(it) },
onFailure = { LoginUiState.Error(it.message ?: "登录失败") }
)
}
}
}
sealed class LoginUiState {
object Idle : LoginUiState()
object Loading : LoginUiState()
data class Success(val user: User) : LoginUiState()
data class Error(val message: String) : LoginUiState()
}
5.6 UI层观察状态
// presentation/ui/login/LoginFragment.kt
@AndroidEntryPoint
class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.uiState.collectLatest { state ->
when (state) {
is LoginUiState.Loading -> showLoading()
is LoginUiState.Success -> navigateToHome()
is LoginUiState.Error -> showError(state.message)
else -> {}
}
}
}
}
六、避坑指南
这套架构我用了三年,踩过的坑不少。挑几个典型的说说:
- 别在ViewModel里直接持有Context —— 我曾经这么干过,结果内存泄漏查了两天。用Hilt注入Application或者用SavedStateHandle。
- UseCase别写太大 —— 一个UseCase只做一件事。如果发现UseCase里调了三个Repository,那八成是设计有问题。
- StateFlow用对了没? —— 记得用stateIn或者MutableStateFlow,别用SharedFlow做UI状态,除非你需要事件消费。
- 单元测试别偷懒 —— domain层是最好测的,没有Android依赖。我要求团队domain层覆盖率必须到90%以上。
我的习惯: 每次新建一个功能模块,先写domain层的接口和Model,再写测试。测试通过了,才去实现data层和UI层。这叫「测试驱动开发」,虽然慢一点,但后期改需求时你就知道有多香了。
七、总结
MVVM + Clean Architecture不是银弹,但它确实能帮你把项目组织得井井有条。刚开始可能会觉得「多此一举」,但等你项目到了10万行、20万行代码时,你会感谢当初的选择。
记住几个关键点:
- 数据单向流动
- 依赖倒置,domain层不依赖框架
- UseCase只做一件事
- ViewModel只管理UI状态
好了,这一章就到这里。代码都在上面了,建议你照着敲一遍。下一章咱们聊聊依赖注入的具体配置,到时候见。