跳转至

第 05 章 MVVM 架构与组件交互

MVVM 架构与组件交互图

学习目标:掌握 MVVM 架构模式,理解 Android 架构组件,构建可维护、可测试的应用。

预计学习时间: 7-10 天 实践时间: 3-4 天


目录

  1. MVVM 架构概述
  2. ViewModel 详解
  3. Repository 模式
  4. 依赖注入
  5. 组件间通信
  6. 实践练习

1. MVVM 架构概述

1.1 架构模式对比

架构 优点 缺点 适用场景
MVC 简单直观 View 和 Controller 耦合 小型项目
MVP 分离关注点 接口过多,样板代码多 中型项目
MVVM 数据绑定,测试友好 学习曲线陡峭 大型项目
MVI 单向数据流,状态可追溯 样板代码多 复杂状态管理

1.2 MVVM 核心组件

Text Only
┌─────────────────────────────────────────────────────────────┐
│                         View (UI)                           │
│                    (Activity/Fragment)                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  - 观察ViewModel的状态                               │   │
│  │  - 将用户操作转发给ViewModel                         │   │
│  │  - 不包含业务逻辑                                    │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                  │
│                           │ 观察 (Observe)                    │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    ViewModel                         │   │
│  │  ┌───────────────────────────────────────────────┐  │   │
│  │  │  - 持有UI状态 (StateFlow/LiveData)             │  │   │
│  │  │  - 处理用户交互                                  │  │   │
│  │  │  - 调用UseCase/Repository                        │  │   │
│  │  │  - 不引用View                                   │  │   │
│  │  └───────────────────────────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                  │
│                           │ 调用                              │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              UseCase / Repository                    │   │
│  │  ┌───────────────────────────────────────────────┐  │   │
│  │  │  - 业务逻辑                                      │  │   │
│  │  │  - 数据转换                                      │  │   │
│  │  │  - 协调多个数据源                                │  │   │
│  │  └───────────────────────────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                  │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Model (Data Layer)                      │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │   │
│  │  │   Entity    │  │    DTO      │  │    DAO      │  │   │
│  │  │  (Domain)   │  │  (Network)  │  │  (Local)    │  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

2. ViewModel 详解

2.1 ViewModel 基础

Kotlin
// 基本ViewModel
class UserViewModel : ViewModel() {
    private val _userName = MutableStateFlow("")
    val userName: StateFlow<String> = _userName.asStateFlow()

    fun updateUserName(name: String) {
        _userName.value = name
    }

    override fun onCleared() {
        super.onCleared()
        // 清理资源
    }
}

// 带SavedState的ViewModel
// SavedStateHandle 可在进程被系统回收后恢复状态(如旋转屏幕、后台被杀)
class TaskViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private val taskId: String = checkNotNull(savedStateHandle["taskId"]) // 从导航参数中提取

    // 使用SavedStateHandle保存状态,进程重建后自动恢复
    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set
}

// 在Compose中使用
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userName by viewModel.userName.collectAsState()

    TextField(
        value = userName,
        onValueChange = viewModel::updateUserName
    )
}

2.2 UI 状态管理

Kotlin
// 定义UI状态
data class NewsUiState(
    val isLoading: Boolean = false,
    val news: List<NewsItem> = emptyList(),
    val error: String? = null,
    val selectedCategory: NewsCategory = NewsCategory.ALL
)

// 定义UI事件
sealed class NewsEvent {
    data class SelectCategory(val category: NewsCategory) : NewsEvent()
    data class Refresh(val force: Boolean = false) : NewsEvent()
    data object LoadMore : NewsEvent()
}

// ViewModel实现
@HiltViewModel
class NewsViewModel @Inject constructor(
    private val getNewsUseCase: GetNewsUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    init {
        loadNews()
    }

    // 统一事件入口:将 UI 事件分发到对应处理函数
    fun onEvent(event: NewsEvent) {
        when (event) {
            is NewsEvent.SelectCategory -> selectCategory(event.category)
            is NewsEvent.Refresh -> refresh(event.force)
            is NewsEvent.LoadMore -> loadMore()
        }
    }

    private fun selectCategory(category: NewsCategory) {
        _uiState.update { it.copy(selectedCategory = category) }
        loadNews()
    }

    private fun loadNews() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true, error = null) }

            getNewsUseCase(_uiState.value.selectedCategory)
                .onSuccess { news ->
                    _uiState.update {
                        it.copy(news = news, isLoading = false)
                    }
                }
                .onFailure { error ->
                    // 加载失败时保留已有数据,只更新错误信息
                    _uiState.update {
                        it.copy(error = error.message, isLoading = false)
                    }
                }
        }
    }
}

3. Repository 模式

3.1 Repository 基础

Kotlin
// Repository接口
interface UserRepository {  // interface定义类型契约
    suspend fun getUser(userId: String): Result<User>
    suspend fun updateUser(user: User): Result<Unit>
    fun getUsersFlow(): Flow<List<User>>
}

// Repository实现
class UserRepositoryImpl @Inject constructor(
    private val userApi: UserApi,
    private val userDao: UserDao,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : UserRepository {

    // 离线优先策略:网络成功则缓存到本地,网络失败则回退到本地缓存
    override suspend fun getUser(userId: String): Result<User> =
        withContext(ioDispatcher) {
            try {  // try/catch捕获异常
                // 先尝试从网络获取
                val remoteUser = userApi.getUser(userId)
                userDao.insertUser(remoteUser.toEntity()) // 同步到本地数据库
                Result.success(remoteUser.toDomain())
            } catch (e: Exception) {
                // 网络失败则从本地获取
                userDao.getUserById(userId)?.let {
                    Result.success(it.toDomain())
                } ?: Result.failure(e) // 本地也无数据时返回失败
            }
        }

    override fun getUsersFlow(): Flow<List<User>> {
        return userDao.getUsersFlow()
            .map { entities -> entities.map { it.toDomain() } } // Entity → Domain 模型转换
            .flowOn(ioDispatcher) // 在 IO 线程执行数据库操作
    }
}

3.2 UseCase 模式

Kotlin
// UseCase基类
// 通过 operator fun invoke 实现类的函数式调用:useCase(params)
abstract class UseCase<in P, R> {
    operator fun invoke(parameters: P): Flow<Result<R>> = execute(parameters)
        .catch { e -> emit(Result.failure(e)) } // 统一捕获异常,转为 Result.failure
        .flowOn(Dispatchers.IO)                  // 确保在 IO 线程执行

    protected abstract fun execute(parameters: P): Flow<Result<R>>
}

// 具体UseCase
class GetNewsUseCase @Inject constructor(
    private val newsRepository: NewsRepository
) : UseCase<NewsCategory, List<NewsItem>>() {

    override fun execute(parameters: NewsCategory): Flow<Result<List<NewsItem>>> = flow {
        emit(Result.success(newsRepository.getNews(parameters)))
    }
}

// 无参数UseCase
class GetCurrentUserUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(): Result<User> {
        return userRepository.getCurrentUser()
    }
}

4. 依赖注入

4.1 Hilt 配置

Kotlin
// Application类
@HiltAndroidApp
class MyApplication : Application()

// 模块定义
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .addConverterFactory(Json.asConverterFactory())
            .build()
    }

    @Provides
    @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi {
        return retrofit.create(UserApi::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    abstract fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}

// 限定符
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher

@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {

    @Provides
    @IoDispatcher
    fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}

// 注入使用
@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: UserRepository,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel()

5. 组件间通信

5.1 SharedFlow 事件总线

Kotlin
// 事件定义
sealed class AppEvent {
    data class ShowToast(val message: String) : AppEvent()
    data class Navigate(val route: String) : AppEvent()
    object Logout : AppEvent()
}

// EventBus — 基于 SharedFlow 的全局事件总线
// SharedFlow 不会重放历史事件,适合一次性事件(Toast、导航等)
@Singleton
class AppEventBus @Inject constructor() {
    private val _events = MutableSharedFlow<AppEvent>() // 无 replay,新订阅者不会收到旧事件
    val events = _events.asSharedFlow()

    suspend fun emit(event: AppEvent) {
        _events.emit(event)
    }
}

// 收集事件
@Composable
fun EventHandler(eventBus: AppEventBus = hiltViewModel()) {
    val context = LocalContext.current

    LaunchedEffect(Unit) {
        eventBus.events.collect { event ->
            when (event) {
                is AppEvent.ShowToast -> {
                    Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
                }
                // 处理其他事件
            }
        }
    }
}

6. 实践练习

练习 1 :完整 MVVM 架构

任务:为待办事项应用实现完整的 MVVM 架构

要求: - 定义数据模型和 UI 状态 - 实现 Repository 和 UseCase - 配置 Hilt 依赖注入 - 编写 ViewModel 和 UI

练习 2 :状态管理优化

任务:优化一个存在状态管理问题的应用

要求: - 识别状态管理问题 - 应用状态提升原则 - 实现单向数据流 - 添加单元测试


本章小结

核心要点

  1. MVVM 架构分离关注点,提高代码可测试性和可维护性
  2. ViewModel管理 UI 状态,在配置变更时保持状态
  3. Repository 模式统一数据访问,支持多种数据源
  4. 依赖注入简化对象创建和依赖管理
  5. 单向数据流使状态变化可预测

下一步

完成本章学习后,请进入第 06 章:数据处理与 API 集成,学习网络请求和数据存储。


本章完成时间:预计 7-10 天