本文档说明新大洲 Android 项目采用的 MVVM 四层架构设计
本项目采用 MVVM 四层架构,基于 Clean Architecture 和 Android 架构组件,将代码分为以下四层:
┌─────────────────────────────────────────┐
│ UI/Presentation 层 │
│ (Fragment + ViewModel) │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Domain 层 │
│ (UseCase + Model) │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Repository 层 │
│ (数据仓库,统一数据源) │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Data 层 │
│ (Remote + Local DataSource) │
└─────────────────────────────────────────┘
传统三层 MVVM:
View → ViewModel → Repository → API
四层 MVVM(Clean Architecture):
View → ViewModel → UseCase → Repository → DataSource
主要改进:
职责:
包含内容:
依赖关系:
示例结构:
ui/
├── login/
│ └── LoginFragment.kt
├── viewmodel/
│ ├── LoginViewModel.kt
│ └── LoginViewModelFactory.kt
└── state/
└── LoginState.kt
职责:
包含内容:
依赖关系:
示例结构:
domain/
├── model/
│ └── User.kt
└── usecase/
├── LoginUseCase.kt
└── RegisterUseCase.kt
职责:
包含内容:
依赖关系:
示例结构:
data/
└── repository/
└── AuthRepository.kt
职责:
包含内容:
依赖关系:
示例结构:
data/
├── remote/
│ └── AuthRemoteDataSource.kt
├── local/
│ ├── AuthLocalDataSource.kt
│ └── TokenManager.kt
├── model/
│ ├── LoginRequest.kt
│ └── LoginResponse.kt
└── api/
└── AuthApi.kt
每个功能模块应遵循以下目录结构:
module_name/
├── ui/ # UI 层
│ ├── feature_name/
│ │ └── FeatureFragment.kt
│ ├── viewmodel/
│ │ ├── FeatureViewModel.kt
│ │ └── FeatureViewModelFactory.kt
│ └── state/
│ └── FeatureState.kt
│
├── domain/ # Domain 层
│ ├── model/
│ │ └── DomainModel.kt
│ └── usecase/
│ └── FeatureUseCase.kt
│
├── data/ # Data 层
│ ├── repository/
│ │ └── FeatureRepository.kt
│ ├── remote/
│ │ └── FeatureRemoteDataSource.kt
│ ├── local/
│ │ └── FeatureLocalDataSource.kt
│ ├── model/
│ │ ├── FeatureRequest.kt
│ │ └── FeatureResponse.kt
│ └── api/
│ └── FeatureApi.kt
│
├── di/ # 依赖注入(可选)
│ └── FeatureModule.kt
│
└── constant/ # 常量(可选)
└── FeatureConstants.kt
user/
├── ui/
│ ├── login/
│ │ └── LoginFragment.kt
│ ├── register/
│ │ └── RegisterFragment.kt
│ ├── viewmodel/
│ │ ├── LoginViewModel.kt
│ │ ├── LoginViewModelFactory.kt
│ │ ├── RegisterViewModel.kt
│ │ └── RegisterViewModelFactory.kt
│ └── state/
│ ├── LoginState.kt
│ └── RegisterState.kt
│
├── domain/
│ ├── model/
│ │ └── User.kt
│ └── usecase/
│ ├── LoginUseCase.kt
│ └── RegisterUseCase.kt
│
├── data/
│ ├── repository/
│ │ └── AuthRepository.kt
│ ├── remote/
│ │ └── AuthRemoteDataSource.kt
│ ├── local/
│ │ ├── AuthLocalDataSource.kt
│ │ └── TokenManager.kt
│ ├── model/
│ │ ├── LoginRequest.kt
│ │ ├── LoginResponse.kt
│ │ └── RegisterRequest.kt
│ └── api/
│ └── AuthApi.kt
│
└── constant/
├── ApiConstants.kt
└── ValidationConstants.kt
用户操作
↓
Fragment (UI)
↓ 调用
ViewModel.login()
↓ 调用
UseCase.invoke()
↓ 调用
Repository.login()
↓ 调用
RemoteDataSource.login() 或 LocalDataSource.getData()
↓
API/数据库
↓ 返回数据
Repository (转换 Data Model → Domain Model)
↓ 返回
UseCase (业务逻辑处理)
↓ 返回
ViewModel (转换为 UI State)
↓ 更新 StateFlow
Fragment (观察 StateFlow,更新 UI)
1. 用户在 LoginFragment 点击登录按钮
↓
2. LoginFragment 调用 viewModel.login(mobile, password)
↓
3. LoginViewModel 调用 loginUseCase(mobile, password)
↓
4. LoginUseCase 验证输入,调用 authRepository.login()
↓
5. AuthRepository 调用 remoteDataSource.login()
↓
6. AuthRemoteDataSource 调用 authApi.login() (Retrofit)
↓
7. 网络请求返回 LoginResponse (Data Model)
↓
8. AuthRemoteDataSource 返回 Result<LoginResponse>
↓
9. AuthRepository 保存 Token 到 LocalDataSource
↓
10. AuthRepository 返回 Result<LoginResponse>
↓
11. LoginUseCase 返回 Result<Unit>
↓
12. LoginViewModel 更新 _loginState (Success/Error)
↓
13. LoginFragment 观察 loginState,更新 UI
package com.narutohuo.xindazhou.user.ui.login
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.narutohuo.xindazhou.R
import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModel
import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModelFactory
import kotlinx.coroutines.launch
/**
* 登录Fragment
*/
class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by viewModels {
LoginViewModelFactory(requireContext().applicationContext as android.app.Application)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_login, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 初始化视图
initViews(view)
// 观察状态
observeState()
}
private fun initViews(view: View) {
// 绑定视图和设置点击事件
view.findViewById<View>(R.id.btnLogin).setOnClickListener {
val mobile = view.findViewById<EditText>(R.id.etMobile).text.toString()
val password = view.findViewById<EditText>(R.id.etPassword).text.toString()
viewModel.login(mobile, password)
}
}
private fun observeState() {
lifecycleScope.launch {
viewModel.loginState.collect { state ->
when (state) {
is LoginState.Idle -> { /* 初始状态 */ }
is LoginState.Loading -> { /* 显示加载 */ }
is LoginState.Success -> { /* 登录成功 */ }
is LoginState.Error -> { /* 显示错误 */ }
}
}
}
}
}
package com.narutohuo.xindazhou.user.ui.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.narutohuo.xindazhou.user.domain.usecase.LoginUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
* 登录状态
*/
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Success(val message: String) : LoginState()
data class Error(val message: String) : LoginState()
}
/**
* 登录ViewModel
*/
class LoginViewModel(
application: Application,
private val loginUseCase: LoginUseCase
) : AndroidViewModel(application) {
private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
val loginState: StateFlow<LoginState> = _loginState
fun login(mobile: String, password: String) {
viewModelScope.launch {
_loginState.value = LoginState.Loading
loginUseCase(mobile, password)
.onSuccess {
_loginState.value = LoginState.Success("登录成功")
}
.onFailure { e ->
_loginState.value = LoginState.Error(e.message ?: "登录失败")
}
}
}
}
package com.narutohuo.xindazhou.user.domain.usecase
import com.narutohuo.xindazhou.user.data.repository.AuthRepository
import com.narutohuo.xindazhou.user.domain.constant.ValidationConstants
import timber.log.Timber
/**
* 登录用例
*
* 封装登录业务逻辑
*/
class LoginUseCase(
private val authRepository: AuthRepository
) {
suspend operator fun invoke(mobile: String, password: String): Result<Unit> {
// 业务逻辑验证
if (mobile.isBlank()) {
return Result.failure(IllegalArgumentException("手机号不能为空"))
}
if (password.length < ValidationConstants.MIN_PASSWORD_LENGTH
|| password.length > ValidationConstants.MAX_PASSWORD_LENGTH) {
return Result.failure(
IllegalArgumentException("密码长度应为${ValidationConstants.MIN_PASSWORD_LENGTH}-${ValidationConstants.MAX_PASSWORD_LENGTH}位")
)
}
// 调用 Repository
return authRepository.login(mobile, password).map { }
}
}
package com.narutohuo.xindazhou.user.data.repository
import com.narutohuo.xindazhou.user.data.local.AuthLocalDataSource
import com.narutohuo.xindazhou.user.data.model.LoginRequest
import com.narutohuo.xindazhou.user.data.model.LoginResponse
import com.narutohuo.xindazhou.user.data.remote.AuthRemoteDataSource
import timber.log.Timber
/**
* 认证数据仓库
*
* 统一管理认证相关的数据获取和缓存
*/
class AuthRepository(
private val remoteDataSource: AuthRemoteDataSource,
private val localDataSource: AuthLocalDataSource
) {
companion object {
private const val TAG = "AuthRepository"
}
/**
* 用户登录
*/
suspend fun login(mobile: String, password: String): Result<LoginResponse> {
return try {
val request = LoginRequest(mobile, password)
// 先尝试从远程获取
val result = remoteDataSource.login(request)
// 如果成功,保存到本地
result.onSuccess { response ->
localDataSource.saveToken(response)
Timber.d("$TAG - login: Token已保存")
}
result
} catch (e: Exception) {
Timber.e(e, "$TAG - login: 登录异常")
Result.failure(e)
}
}
}
package com.narutohuo.xindazhou.user.data.remote
import com.narutohuo.xindazhou.user.api.AuthApi
import com.narutohuo.xindazhou.user.data.constant.ApiConstants
import com.narutohuo.xindazhou.user.data.model.LoginRequest
import com.narutohuo.xindazhou.user.data.model.LoginResponse
import timber.log.Timber
/**
* 认证远程数据源接口
*/
interface AuthRemoteDataSource {
suspend fun login(request: LoginRequest): Result<LoginResponse>
}
/**
* 认证远程数据源实现
*/
class AuthRemoteDataSourceImpl(
private val authApi: AuthApi
) : AuthRemoteDataSource {
companion object {
private const val TAG = "AuthRemoteDataSource"
}
override suspend fun login(request: LoginRequest): Result<LoginResponse> {
return try {
val response = authApi.login(request)
if (!response.isSuccessful) {
val errorMsg = response.message() ?: "登录失败"
Timber.w("$TAG - login: HTTP请求失败,code=${response.code()}")
return Result.failure(Exception(errorMsg))
}
val commonResult = response.body()
?: return Result.failure(Exception("响应体为空"))
if (commonResult.code != ApiConstants.SUCCESS_CODE) {
val errorMsg = commonResult.msg ?: "登录失败 (code: ${commonResult.code})"
Timber.w("$TAG - login: 业务失败,code=${commonResult.code}")
return Result.failure(Exception(errorMsg))
}
val loginResponse = commonResult.data
?: return Result.failure(Exception("服务器返回数据为空"))
Timber.d("$TAG - login: 登录成功")
Result.success(loginResponse)
} catch (e: Exception) {
Timber.e(e, "$TAG - login: 登录异常")
Result.failure(e)
}
}
}
package com.narutohuo.xindazhou.user.data.local
import com.narutohuo.xindazhou.user.data.model.LoginResponse
/**
* 认证本地数据源接口
*/
interface AuthLocalDataSource {
suspend fun saveToken(loginResponse: LoginResponse)
suspend fun getToken(): String?
suspend fun clearToken()
}
/**
* 认证本地数据源实现
*/
class AuthLocalDataSourceImpl : AuthLocalDataSource {
override suspend fun saveToken(loginResponse: LoginResponse) {
TokenManager.saveToken(
loginResponse.accessToken,
loginResponse.refreshToken,
loginResponse.userId
)
}
override suspend fun getToken(): String? {
return TokenManager.getAccessToken()
}
override suspend fun clearToken() {
TokenManager.clearToken()
}
}
UI 层
↓ 依赖
Domain 层
↑ 被依赖
Repository 层
↓ 依赖
Data 层
原则:
建议使用依赖注入框架(如 Hilt/Koin)管理依赖关系:
// 示例:使用 Koin
val authModule = module {
// Data 层
factory<AuthRemoteDataSource> { AuthRemoteDataSourceImpl(get()) }
factory<AuthLocalDataSource> { AuthLocalDataSourceImpl() }
single { AuthApi.create() }
// Repository 层
single<AuthRepository> {
AuthRepository(get(), get())
}
// Domain 层
factory { LoginUseCase(get()) }
factory { RegisterUseCase(get()) }
// UI 层
viewModel { LoginViewModel(get(), get()) }
}
使用 Sealed Class 定义 UI 状态:
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Success(val message: String) : LoginState()
data class Error(val message: String) : LoginState()
}
统一使用 Result 类型处理错误:
suspend fun login(mobile: String, password: String): Result<LoginResponse> {
return try {
// 成功
Result.success(data)
} catch (e: Exception) {
// 失败
Result.failure(e)
}
}
Repository 层负责 Data Model 到 Domain Model 的转换:
// Data Model (API 响应)
data class LoginResponseData(...)
// Domain Model (业务模型)
data class User(...)
// Repository 中转换
fun toDomainModel(data: LoginResponseData): User {
return User(
id = data.userId,
name = data.username,
// ...
)
}
功能ViewModel,如 LoginViewModel功能UseCase,如 LoginUseCase功能Repository,如 AuthRepository功能RemoteDataSource / 功能LocalDataSource功能State,如 LoginState文档版本: v1.0
最后更新: 2024-01-01
维护者: 开发团队