MVVM_ARCHITECTURE.md 21 KB

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<LoginResponse>
   ↓
9. AuthRepository 保存 Token 到 LocalDataSource
   ↓
10. AuthRepository 返回 Result<LoginResponse>
    ↓
11. LoginUseCase 返回 Result<Unit>
    ↓
12. LoginViewModel 更新 _loginState (Success/Error)
    ↓
13. LoginFragment 观察 loginState,更新 UI

五、代码示例

5.1 UI 层示例

LoginFragment.kt

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 -> { /* 显示错误 */ }
                }
            }
        }
    }
}

LoginViewModel.kt

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 ?: "登录失败")
                }
        }
    }
}

5.2 Domain 层示例

LoginUseCase.kt

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 { }
    }
}

5.3 Repository 层示例

AuthRepository.kt

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)
        }
    }
}

5.4 Data 层示例

AuthRemoteDataSource.kt

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)
        }
    }
}

AuthLocalDataSource.kt

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)管理依赖关系:

// 示例:使用 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()) }
}

七、最佳实践

7.1 状态管理

使用 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()
}

7.2 错误处理

统一使用 Result 类型处理错误:

suspend fun login(mobile: String, password: String): Result<LoginResponse> {
    return try {
        // 成功
        Result.success(data)
    } catch (e: Exception) {
        // 失败
        Result.failure(e)
    }
}

7.3 数据转换

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,
        // ...
    )
}

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
维护者: 开发团队