Skip to content

Compose 与 ViewModel

欢迎来到 Compose 与 ViewModel 的世界!ViewModel 是 Android 架构组件中的重要一员,它可以帮助我们管理 UI 状态并处理业务逻辑。Compose 与 ViewModel 结合使用,可以创建出更加健壮、可测试的应用。让我们一起探索吧!

🎯 什么是 ViewModel?

ViewModel 是一个架构组件,它的主要作用是:

  • 管理 UI 相关的状态
  • 处理业务逻辑
  • 存活于配置变化(如屏幕旋转)
  • 提供数据给 UI 层
  • 与数据层交互(如网络请求、数据库操作等)

🧩 ViewModel 的优势

1. 配置变化时状态保留

ViewModel 在配置变化时不会被销毁,因此可以保留状态:

kotlin
@Composable
fun MyScreen() {
    val viewModel = viewModel<MyViewModel>()
    val count = viewModel.count
    
    Button(onClick = { viewModel.increment() }) {
        Text(text = "Count: $count")
    }
}

class MyViewModel : ViewModel() {
    var count by mutableStateOf(0) // 配置变化时会保留
    
    fun increment() {
        count++
    }
}

2. 关注点分离

ViewModel 可以帮助我们将 UI 逻辑和业务逻辑分离:

kotlin
// UI 层
@Composable
fun MyScreen() {
    val viewModel = viewModel<MyViewModel>()
    val state by viewModel.state
    
    if (state.loading) {
        CircularProgressIndicator()
    } else if (state.error != null) {
        Text(text = state.error!!, color = Color.Red)
    } else {
        // 渲染数据
    }
}

// ViewModel 层
class MyViewModel : ViewModel() {
    val state = mutableStateOf(MyState())
    
    init {
        fetchData()
    }
    
    private fun fetchData() {
        viewModelScope.launch {
            try {
                state.value = state.value.copy(loading = true)
                val data = dataRepository.fetchData()
                state.value = state.value.copy(data = data, loading = false)
            } catch (e: Exception) {
                state.value = state.value.copy(error = e.message, loading = false)
            }
        }
    }
}

// 数据层
class DataRepository {
    suspend fun fetchData(): List<Item> {
        // 网络请求或数据库操作
    }
}

3. 可测试性

ViewModel 可以独立于 UI 进行测试:

kotlin
@Test
fun testIncrement() {
    val viewModel = MyViewModel()
    viewModel.increment()
    assertEquals(1, viewModel.count)
}

🧩 在 Compose 中使用 ViewModel

1. 导入依赖

首先,确保你的项目中包含 ViewModel 依赖:

gradle
dependencies {
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
    
    // 协程支持
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
}

2. 创建 ViewModel

kotlin
class MyViewModel : ViewModel() {
    // 使用 MutableStateFlow 存储状态
    private val _state = MutableStateFlow(MyState())
    val state: StateFlow<MyState> = _state.asStateFlow()
    
    // 或者使用 mutableStateOf
    var count by mutableStateOf(0)
    
    fun increment() {
        count++
    }
    
    fun fetchData() {
        viewModelScope.launch {
            try {
                _state.value = _state.value.copy(loading = true)
                val data = fetchDataFromNetwork()
                _state.value = _state.value.copy(data = data, loading = false)
            } catch (e: Exception) {
                _state.value = _state.value.copy(error = e.message, loading = false)
            }
        }
    }
}

// 定义状态类
data class MyState(
    val loading: Boolean = false,
    val data: List<Item> = emptyList(),
    val error: String? = null
)

3. 在 Compose 中获取 ViewModel

使用 viewModel() 函数获取 ViewModel 实例:

kotlin
@Composable
fun MyScreen() {
    val viewModel = viewModel<MyViewModel>()
    // ...
}

4. 观察 ViewModel 中的状态

使用 mutableStateOf

kotlin
@Composable
fun MyScreen() {
    val viewModel = viewModel<MyViewModel>()
    val count = viewModel.count // 自动重组
    
    Button(onClick = { viewModel.increment() }) {
        Text(text = "Count: $count")
    }
}

使用 StateFlow

kotlin
@Composable
fun MyScreen() {
    val viewModel = viewModel<MyViewModel>()
    val state by viewModel.state.collectAsState() // 需要导入 androidx.lifecycle.compose.collectAsState
    
    if (state.loading) {
        CircularProgressIndicator()
    } else if (state.error != null) {
        Text(text = state.error!!, color = Color.Red)
    } else {
        LazyColumn {
            items(state.data) {
                Text(text = it.name)
            }
        }
    }
}

🧩 ViewModel 与状态管理

1. UI 状态的建模

使用数据类建模 UI 状态是一种很好的做法:

kotlin
data class LoginState(
    val email: String = "",
    val password: String = "",
    val loading: Boolean = false,
    val error: String? = null,
    val isLoggedIn: Boolean = false
)

class LoginViewModel : ViewModel() {
    var state by mutableStateOf(LoginState())
    
    fun onEmailChange(email: String) {
        state = state.copy(email = email)
    }
    
    fun onPasswordChange(password: String) {
        state = state.copy(password = password)
    }
    
    fun login() {
        viewModelScope.launch {
            try {
                state = state.copy(loading = true)
                authRepository.login(state.email, state.password)
                state = state.copy(isLoggedIn = true, loading = false)
            } catch (e: Exception) {
                state = state.copy(error = e.message, loading = false)
            }
        }
    }
}

2. 单向数据流

ViewModel 可以帮助我们实现单向数据流:

kotlin
// UI 事件 -> ViewModel
Button(onClick = { viewModel.login() }) {
    Text(text = "Login")
}

// ViewModel -> UI 状态
val state by viewModel.state
if (state.loading) {
    CircularProgressIndicator()
}

🧩 在 ViewModel 中使用协程

1. viewModelScope

ViewModel 提供了 viewModelScope,它会在 ViewModel 销毁时自动取消所有协程:

kotlin
class MyViewModel : ViewModel() {
    fun fetchData() {
        // 在 viewModelScope 中启动协程
        viewModelScope.launch {
            val data = dataRepository.fetchData()
            // 更新状态
        }
    }
}

2. 异常处理

在 ViewModel 中处理异常:

kotlin
class MyViewModel : ViewModel() {
    var state by mutableStateOf(MyState())
    
    fun fetchData() {
        viewModelScope.launch {
            try {
                state = state.copy(loading = true)
                val data = dataRepository.fetchData()
                state = state.copy(data = data, loading = false)
            } catch (e: Exception) {
                state = state.copy(error = e.message, loading = false)
            }
        }
    }
}

🧩 ViewModel 与导航

1. 导航事件

在 ViewModel 中处理导航事件:

kotlin
sealed class NavigationEvent {
    object Home : NavigationEvent()
    object Detail : NavigationEvent()
    class Profile(val userId: Int) : NavigationEvent()
}

class MyViewModel : ViewModel() {
    private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
    val navigationEvent = _navigationEvent.asSharedFlow()
    
    fun navigateToDetail() {
        viewModelScope.launch {
            _navigationEvent.emit(NavigationEvent.Detail)
        }
    }
    
    fun navigateToProfile(userId: Int) {
        viewModelScope.launch {
            _navigationEvent.emit(NavigationEvent.Profile(userId))
        }
    }
}

在 Compose 中观察导航事件:

kotlin
@Composable
fun MyScreen() {
    val viewModel = viewModel<MyViewModel>()
    val navController = rememberNavController()
    
    // 观察导航事件
    LaunchedEffect(Unit) {
        viewModel.navigationEvent.collect {
            when (it) {
                NavigationEvent.Home -> navController.navigate("home")
                NavigationEvent.Detail -> navController.navigate("detail")
                is NavigationEvent.Profile -> navController.navigate("profile/${it.userId}")
            }
        }
    }
    
    Button(onClick = { viewModel.navigateToDetail() }) {
        Text(text = "Go to Detail")
    }
}

🧩 ViewModel 的测试

1. 单元测试

使用 JUnit 和 Mockito 测试 ViewModel:

kotlin
@Test
fun testLoginSuccess() {
    // 创建模拟的依赖
    val mockAuthRepository = mock(AuthRepository::class.java)
    
    // 设置模拟行为
    runBlocking {
        `when`(mockAuthRepository.login(anyString(), anyString())).thenReturn(true)
    }
    
    // 创建 ViewModel 实例
    val viewModel = LoginViewModel(mockAuthRepository)
    
    // 设置状态
    viewModel.onEmailChange("test@example.com")
    viewModel.onPasswordChange("password")
    
    // 执行测试
    runBlockingTest {
        viewModel.login()
    }
    
    // 验证结果
    assertEquals(true, viewModel.state.isLoggedIn)
    assertNull(viewModel.state.error)
}

2. Compose 测试

使用 Compose Testing 测试 UI 与 ViewModel 的交互:

kotlin
@Test
fun testLoginButton() {
    // 创建模拟的 ViewModel
    val mockViewModel = mock(LoginViewModel::class.java)
    `when`(mockViewModel.state).thenReturn(MutableStateFlow(LoginState()).asStateFlow())
    
    composeTestRule.setContent {
        MyComposeTheme {
            LoginScreen(viewModel = mockViewModel)
        }
    }
    
    // 点击登录按钮
    composeTestRule.onNodeWithText("Login").performClick()
    
    // 验证 ViewModel 的方法是否被调用
    verify(mockViewModel).login()
}

🧩 ViewModel 的最佳实践

1. 单一职责原则

每个 ViewModel 应该只负责一个功能模块:

kotlin
// 好的做法:每个功能模块一个 ViewModel
class LoginViewModel : ViewModel() { /* ... */ }
class HomeViewModel : ViewModel() { /* ... */ }
class DetailViewModel : ViewModel() { /* ... */ }

// 不好的做法:一个 ViewModel 负责所有功能
class AppViewModel : ViewModel() {
    // 登录相关
    // 首页相关
    // 详情页相关
    // ...
}

2. 使用依赖注入

使用依赖注入框架(如 Hilt)注入 ViewModel 的依赖:

kotlin
@HiltViewModel
class LoginViewModel @Inject constructor(
    private val authRepository: AuthRepository
) : ViewModel() {
    // ...
}

3. 避免引用 View 或 Context

ViewModel 中不应该引用 View 或 Context,否则可能导致内存泄漏:

kotlin
// 不好的做法:引用 Context
class MyViewModel(private val context: Context) : ViewModel() {
    // ...
}

// 好的做法:不引用 Context
class MyViewModel(private val dataRepository: DataRepository) : ViewModel() {
    // ...
}

🎨 练习

现在,让我们来练习一下:创建一个带有 ViewModel 的计数器应用,并实现配置变化时状态保留。

kotlin
// ViewModel
class CounterViewModel : ViewModel() {
    var count by mutableStateOf(0)
    var showToast by mutableStateOf(false)
    
    fun increment() {
        count++
        if (count % 5 == 0) {
            showToast = true
        }
    }
    
    fun decrement() {
        if (count > 0) {
            count--
        }
    }
    
    fun reset() {
        count = 0
    }
    
    fun dismissToast() {
        showToast = false
    }
}

// UI 层
@Composable
fun CounterScreen() {
    val viewModel = viewModel<CounterViewModel>()
    val count = viewModel.count
    val showToast = viewModel.showToast
    
    // 显示 Toast
    if (showToast) {
        Toast.makeText(LocalContext.current, "Count is $count!", Toast.LENGTH_SHORT).show()
        LaunchedEffect(Unit) {
            delay(2000)
            viewModel.dismissToast()
        }
    }
    
    Scaffold(
        topBar = {
            TopAppBar(title = { Text(text = "Counter") })
        }
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            // 计数器显示
            Text(
                text = "$count",
                style = MaterialTheme.typography.h1,
                modifier = Modifier.padding(16.dp)
            )
            
            // 控制按钮
            Row(modifier = Modifier.padding(16.dp)) {
                Button(onClick = { viewModel.decrement() }) {
                    Text(text = "-")
                }
                Button(onClick = { viewModel.increment() }, modifier = Modifier.padding(8.dp)) {
                    Text(text = "+" )
                }
                Button(onClick = { viewModel.reset() }) {
                    Text(text = "Reset" )
                }
            }
        }
    }
}

🎉 恭喜

你已经学习了如何在 Compose 中使用 ViewModel!ViewModel 是 Compose 开发中的重要工具,它可以帮助我们管理状态、处理业务逻辑,并在配置变化时保留状态。

下一节,我们将学习 Compose 中的测试,了解如何测试 Compose 应用。

🚀 继续前进吧!