第05章 MVVM架构与组件交互¶
学习目标:掌握MVVM架构模式,理解Android架构组件,构建可维护、可测试的应用。
预计学习时间:7-10天 实践时间:3-4天
目录¶
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:状态管理优化¶
任务:优化一个存在状态管理问题的应用
要求: - 识别状态管理问题 - 应用状态提升原则 - 实现单向数据流 - 添加单元测试
本章小结¶
核心要点¶
- MVVM架构分离关注点,提高代码可测试性和可维护性
- ViewModel管理UI状态,在配置变更时保持状态
- Repository模式统一数据访问,支持多种数据源
- 依赖注入简化对象创建和依赖管理
- 单向数据流使状态变化可预测
下一步¶
完成本章学习后,请进入第06章:数据处理与API集成,学习网络请求和数据存储。
本章完成时间:预计7-10天