跳转至

第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天