一、为什么我坚持用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分钟。

四、数据流怎么走?

这是整个架构的灵魂。我画了一张图,你看完就明白了:

MVVM + Clean Architecture 数据流 表现层 UI / Fragment ↓ 观察 ↑ ViewModel (持有StateFlow) 调用 领域层 UseCase Repository接口 业务Model 实现 数据层 Repository实现 Remote数据源 Local数据源 数据流方向:UI → ViewModel → UseCase → Repository → 数据源

看到没?数据是单向流动的。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状态

好了,这一章就到这里。代码都在上面了,建议你照着敲一遍。下一章咱们聊聊依赖注入的具体配置,到时候见。

公众号:蓝海资料掘金营,微信deep3321