# MVVM 四层架构设计文档 > 本文档说明新大洲 Android 项目采用的 MVVM 四层架构设计 ## 目录 - [一、架构概述](#一架构概述) - [二、四层架构详解](#二四层架构详解) - [三、目录结构规范](#三目录结构规范) - [四、数据流向](#四数据流向) - [五、代码示例](#五代码示例) - [六、依赖关系](#六依赖关系) - [七、最佳实践](#七最佳实践) --- ## 一、架构概述 ### 1.1 架构说明 本项目采用 **MVVM 四层架构**,基于 Clean Architecture 和 Android 架构组件,将代码分为以下四层: ``` ┌─────────────────────────────────────────┐ │ UI/Presentation 层 │ │ (Fragment + ViewModel) │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Domain 层 │ │ (UseCase + Model) │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Repository 层 │ │ (数据仓库,统一数据源) │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Data 层 │ │ (Remote + Local DataSource) │ └─────────────────────────────────────────┘ ``` ### 1.2 架构优势 1. **职责分离**:每一层都有明确的职责,互不干扰 2. **易于测试**:各层独立,便于单元测试 3. **可维护性**:代码结构清晰,易于维护和扩展 4. **可复用性**:Domain 层不依赖 Android 框架,可在其他平台复用 5. **数据统一**:Repository 层统一管理数据源,处理缓存策略 ### 1.3 与传统 MVVM 的区别 **传统三层 MVVM:** ``` View → ViewModel → Repository → API ``` **四层 MVVM(Clean Architecture):** ``` View → ViewModel → UseCase → Repository → DataSource ``` **主要改进:** - 新增 **Domain 层**:封装业务逻辑,与 UI 和 Data 解耦 - 新增 **DataSource 层**:将数据源(Remote/Local)与 Repository 分离 - 更好的可测试性和可维护性 --- ## 二、四层架构详解 ### 2.1 UI/Presentation 层 **职责:** - 展示 UI,处理用户交互 - 观察 ViewModel 的状态变化,更新 UI - 不包含业务逻辑 **包含内容:** - Fragment/Activity - ViewModel - UI State(Sealed Class) **依赖关系:** - 只依赖 Domain 层(通过 UseCase) - 不直接依赖 Data 层 **示例结构:** ``` ui/ ├── login/ │ └── LoginFragment.kt ├── viewmodel/ │ ├── LoginViewModel.kt │ └── LoginViewModelFactory.kt └── state/ └── LoginState.kt ``` ### 2.2 Domain 层 **职责:** - 封装业务逻辑 - 定义业务实体模型 - 定义 UseCase(用例) **包含内容:** - UseCase(用例) - Domain Model(业务模型) - Repository Interface(可选) **依赖关系:** - 不依赖任何 Android 框架 - 只依赖 Repository Interface(接口,不依赖实现) **示例结构:** ``` domain/ ├── model/ │ └── User.kt └── usecase/ ├── LoginUseCase.kt └── RegisterUseCase.kt ``` ### 2.3 Repository 层 **职责:** - 统一数据源管理 - 协调 Remote 和 Local 数据源 - 处理数据缓存策略 - 数据转换(Data Model → Domain Model) **包含内容:** - Repository 实现类 - 数据转换逻辑 **依赖关系:** - 依赖 Domain 层(UseCase) - 依赖 Data 层(RemoteDataSource + LocalDataSource) **示例结构:** ``` data/ └── repository/ └── AuthRepository.kt ``` ### 2.4 Data 层 **职责:** - 提供数据源接口和实现 - 处理网络请求(Remote) - 处理本地存储(Local) - 定义数据模型(Data Model) **包含内容:** - RemoteDataSource(远程数据源) - LocalDataSource(本地数据源) - Data Model(数据模型) - API 接口 **依赖关系:** - 只依赖 Android 框架和第三方库 - 不依赖其他业务层 **示例结构:** ``` data/ ├── remote/ │ └── AuthRemoteDataSource.kt ├── local/ │ ├── AuthLocalDataSource.kt │ └── TokenManager.kt ├── model/ │ ├── LoginRequest.kt │ └── LoginResponse.kt └── api/ └── AuthApi.kt ``` --- ## 三、目录结构规范 ### 3.1 模块目录结构 每个功能模块应遵循以下目录结构: ``` 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 ``` ### 3.2 实际示例(用户模块) ``` 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 ``` --- ## 四、数据流向 ### 4.1 请求数据流向 ``` 用户操作 ↓ 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) ``` ### 4.2 具体示例(登录流程) ``` 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 ↓ 9. AuthRepository 保存 Token 到 LocalDataSource ↓ 10. AuthRepository 返回 Result ↓ 11. LoginUseCase 返回 Result ↓ 12. LoginViewModel 更新 _loginState (Success/Error) ↓ 13. LoginFragment 观察 loginState,更新 UI ``` --- ## 五、代码示例 ### 5.1 UI 层示例 #### LoginFragment.kt ```kotlin 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(R.id.btnLogin).setOnClickListener { val mobile = view.findViewById(R.id.etMobile).text.toString() val password = view.findViewById(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 -> { /* 显示错误 */ } } } } } } ``` #### LoginViewModel.kt ```kotlin 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.Idle) val loginState: StateFlow = _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 ?: "登录失败") } } } } ``` ### 5.2 Domain 层示例 #### LoginUseCase.kt ```kotlin 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 { // 业务逻辑验证 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 { } } } ``` ### 5.3 Repository 层示例 #### AuthRepository.kt ```kotlin 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 { 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) } } } ``` ### 5.4 Data 层示例 #### AuthRemoteDataSource.kt ```kotlin 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 } /** * 认证远程数据源实现 */ class AuthRemoteDataSourceImpl( private val authApi: AuthApi ) : AuthRemoteDataSource { companion object { private const val TAG = "AuthRemoteDataSource" } override suspend fun login(request: LoginRequest): Result { 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) } } } ``` #### AuthLocalDataSource.kt ```kotlin 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() } } ``` --- ## 六、依赖关系 ### 6.1 依赖方向 ``` UI 层 ↓ 依赖 Domain 层 ↑ 被依赖 Repository 层 ↓ 依赖 Data 层 ``` **原则:** - 内层不依赖外层 - 外层可以依赖内层 - Domain 层不依赖任何 Android 框架 ### 6.2 依赖注入 建议使用依赖注入框架(如 Hilt/Koin)管理依赖关系: ```kotlin // 示例:使用 Koin val authModule = module { // Data 层 factory { AuthRemoteDataSourceImpl(get()) } factory { AuthLocalDataSourceImpl() } single { AuthApi.create() } // Repository 层 single { AuthRepository(get(), get()) } // Domain 层 factory { LoginUseCase(get()) } factory { RegisterUseCase(get()) } // UI 层 viewModel { LoginViewModel(get(), get()) } } ``` --- ## 七、最佳实践 ### 7.1 状态管理 使用 Sealed Class 定义 UI 状态: ```kotlin sealed class LoginState { object Idle : LoginState() object Loading : LoginState() data class Success(val message: String) : LoginState() data class Error(val message: String) : LoginState() } ``` ### 7.2 错误处理 统一使用 Result 类型处理错误: ```kotlin suspend fun login(mobile: String, password: String): Result { return try { // 成功 Result.success(data) } catch (e: Exception) { // 失败 Result.failure(e) } } ``` ### 7.3 数据转换 Repository 层负责 Data Model 到 Domain Model 的转换: ```kotlin // 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, // ... ) } ``` ### 7.4 测试建议 - **UI 层**:使用 AndroidX Test 测试 Fragment/Activity - **ViewModel**:使用 JUnit 测试,不依赖 Android 框架 - **UseCase**:使用 JUnit 测试,Mock Repository - **Repository**:使用 JUnit 测试,Mock DataSource - **DataSource**:使用 JUnit 测试,Mock API ### 7.5 命名规范 - **ViewModel**:`功能ViewModel`,如 `LoginViewModel` - **UseCase**:`功能UseCase`,如 `LoginUseCase` - **Repository**:`功能Repository`,如 `AuthRepository` - **DataSource**:`功能RemoteDataSource` / `功能LocalDataSource` - **State**:`功能State`,如 `LoginState` --- ## 附录:架构检查清单 ### 代码审查检查项 - [ ] UI 层是否只包含 UI 逻辑,业务逻辑是否在 UseCase 中 - [ ] ViewModel 是否只调用 UseCase,不直接调用 Repository - [ ] UseCase 是否封装了业务逻辑验证 - [ ] Repository 是否统一管理数据源(Remote + Local) - [ ] DataSource 是否分离为 Remote 和 Local - [ ] 是否遵循依赖方向(内层不依赖外层) - [ ] 是否使用了 Result 类型处理错误 - [ ] 是否使用 Sealed Class 管理 UI 状态 - [ ] 命名是否规范(ViewModel、UseCase、Repository、DataSource) - [ ] 是否添加了必要的注释(类、方法) --- **文档版本**: v1.0 **最后更新**: 2024-01-01 **维护者**: 开发团队