Compose + ViewModel + 互操作
Compose 与架构打通:viewModel() 获取 ViewModel、StateFlow collectAsStateWithLifecycle 与 LiveData observeAsState、向 ViewModel 传递事件、副作用 API(LaunchedEffect/DisposableEffect/rememberCoroutineScope)以及 AndroidView 与 ComposeView 双向互操作。
学习目标
- 在 Compose 中使用 viewModel() 获取并复用 ViewModel
- 掌握 StateFlow collectAsStateWithLifecycle 与 LiveData observeAsState 的接入方式
- 理解副作用 API 的使用边界:LaunchedEffect / DisposableEffect / rememberCoroutineScope
- 能够用 AndroidView 嵌入传统 View,用 ComposeView 将 Compose 嵌入 View 体系
学习目标
- 在 Compose 中使用
viewModel()获取并复用 ViewModel; - 掌握
StateFlow.collectAsStateWithLifecycle与LiveData.observeAsState的接入方式; - 理解副作用 API 的使用边界:
LaunchedEffect/DisposableEffect/rememberCoroutineScope; - 能够用
AndroidView嵌入传统 View,用ComposeView将 Compose 嵌入 View 体系。
Compose + ViewModel + 互操作
上一章讨论了 Compose 内部状态。真实工程中,跨屏幕共享、跨进程恢复的状态应交给 ViewModel。本章把 ViewModel、响应式流、副作用与新旧视图互操作串起来。
依赖配置
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.09.03")
implementation(composeBom)
implementation("androidx.compose.material3:material3")
// ViewModel for Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
// collectAsStateWithLifecycle 所在
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.6")
// LiveData 互操作(如已用 LiveData)
implementation("androidx.compose.runtime:runtime-livedata")
implementation("androidx.activity:activity-compose:1.9.2")
}
ViewModel in Compose:viewModel()
androidx.lifecycle:lifecycle-viewmodel-compose 提供了 viewModel() 可组合函数,它会从最近的 ViewModelStoreOwner(Activity / Fragment / Navigation 目标)拿到对应 ViewModel:
import androidx.lifecycle.viewmodel.compose.viewModel
class ProfileViewModel : ViewModel() {
private val _name = MutableStateFlow("Tom")
val name: StateFlow<String> = _name.asStateFlow()
fun update(name: String) { _name.value = name }
}
@Composable
fun ProfileScreen(vm: ProfileViewModel = viewModel()) {
val name by vm.name.collectAsStateWithLifecycle()
Column {
Text("姓名:$name")
Button(onClick = { vm.update("Jerry") }) { Text("改名") }
}
}
viewModel() 返回的实例与 ViewModelProvider(this).get(...) 等价,配置变更前后拿到的是 同一个实例。
带参数的 ViewModel:ViewModelProvider.Factory
如果 ViewModel 构造时需要参数(仓库、ID),需要提供 ViewModelProvider.Factory。Compose 提供了更简洁的 viewModel(factory = ...):
class DetailViewModel(private val itemId: String) : ViewModel() {
private val _state = MutableStateFlow(DetailUiState())
val state: StateFlow<DetailUiState> = _state.asStateFlow()
init { load() }
private fun load() {
viewModelScope.launch {
_state.value = _state.value.copy(loading = true)
// ... 异步加载
}
}
}
class DetailViewModelFactory(private val itemId: String) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(DetailViewModel::class.java)) {
return DetailViewModel(itemId) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
@Composable
fun DetailScreen(itemId: String) {
val vm: DetailViewModel = viewModel(factory = DetailViewModelFactory(itemId))
val state by vm.state.collectAsStateWithLifecycle()
// ...
}
Compose 1.6+ 推荐 Application 容器 + CreationExtras 的方式(
viewModelFactory { initializer { ... } })替代手写 Factory,可避免反射:
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModel
@Composable
fun DetailScreen(itemId: String) {
val vm: DetailViewModel = viewModel {
initializer { DetailViewModel(itemId) }
}
}
StateFlow.collectAsStateWithLifecycle
collectAsStateWithLifecycle 是 Compose 接 StateFlow/LiveData 的 推荐 方式。它会在 Composable 进入 STOPPED 以下生命周期时 停止收集,回到前台自动恢复,避免后台浪费资源:
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun CounterScreen(vm: CounterViewModel = viewModel()) {
// Lifecycle 感知:UI 不可见时不收集
val count by vm.count.collectAsStateWithLifecycle()
Text("$count")
}
旧 API
collectAsState()不感知生命周期,只要 Composable 在组合中就一直收集。新项目一律用collectAsStateWithLifecycle。
LiveData.observeAsState
对于历史代码里仍是 LiveData 的状态,用 runtime-livedata 工件提供的 observeAsState() 接入,无需迁移到 StateFlow:
import androidx.compose.runtime.livedata.observeAsState
class LegacyViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
fun load() { _user.value = User(1, "Tom") }
}
@Composable
fun LegacyScreen(vm: LegacyViewModel = viewModel()) {
val user by vm.user.observeAsState()
// 初次订阅前 user 为 null,注意处理
Text(user?.name ?: "加载中")
}
observeAsState内部已实现生命周期感知(LiveData 本身就有),不像StateFlow那样需要WithLifecycle变体。
向 ViewModel 传递事件:UDF
UI 把用户操作作为“事件”传给 ViewModel,状态反过来再流向 UI:
data class LoginUiState(
val username: String = "",
val password: String = "",
val loading: Boolean = false,
val error: String? = null
)
class LoginViewModel : ViewModel() {
private val _state = MutableStateFlow(LoginUiState())
val state: StateFlow<LoginUiState> = _state.asStateFlow()
fun onUsernameChange(v: String) { _state.value = _state.value.copy(username = v) }
fun onPasswordChange(v: String) { _state.value = _state.value.copy(password = v) }
fun submit() {
viewModelScope.launch {
_state.value = _state.value.copy(loading = true, error = null)
// ... 异步校验
_state.value = _state.value.copy(loading = false)
}
}
}
@Composable
fun LoginScreen(vm: LoginViewModel = viewModel()) {
val state by vm.state.collectAsStateWithLifecycle()
Column {
TextField(value = state.username, onValueChange = vm::onUsernameChange)
TextField(value = state.password, onValueChange = vm::onPasswordChange)
Button(onClick = vm::submit, enabled = !state.loading) {
Text(if (state.loading) "登录中" else "登录")
}
state.error?.let { Text(it, color = Color.Red) }
}
}
要点:UI 永远不直接修改 ViewModel 字段,只调用方法。
副作用 API
Compose 中的“副作用”指 不应在重组过程中执行的、对组合外产生影响 的操作,例如网络请求、订阅、动画、Toast。这些必须放到副作用 API 中。
LaunchedEffect:协程副作用
LaunchedEffect(key1) 在进入组合时启动一个协程,key 变化时取消旧的并重启新的:
@Composable
fun SnackDetailScreen(snackId: String) {
LaunchedEffect(snackId) {
// snackId 变化时重新加载;离开组合时自动取消
val snack = repository.fetch(snackId)
// 更新本地状态
}
}
特点:
- 离开组合时协程自动取消;
key变化时取消旧协程并启动新的;- 不要在协程外修改
MutableState持有的状态时跨线程——LaunchedEffect默认在主调度器。
LaunchedEffect不应只为了“启动一次”而省略 key——如果需要在特定条件下重启,写明 key。
DisposableEffect:需要清理的副作用
DisposableEffect 用于需要 onDispose 的场景,典型是注册监听、View 互操作:
@Composable
fun ConnectivityAware(network: ConnectivityManager) {
val state = remember { mutableStateOf(false) }
DisposableEffect(network) {
val callback = object : NetworkCallback() {
override fun onAvailable(network: Network) { state.value = true }
override fun onLost(network: Network) { state.value = false }
}
network.registerDefaultNetworkCallback(callback)
onDispose { network.unregisterNetworkCallback(callback) }
}
Text(if (state.value) "在线" else "离线")
}
rememberCoroutineScope
rememberCoroutineScope 返回一个绑定到 Dispatchers.Main、并在 Composable 离开组合时自动取消的 CoroutineScope,用于在事件回调里启动协程(例如按钮点击触发异步):
@Composable
fun SaveButton() {
val scope = rememberCoroutineScope()
var saving by remember { mutableStateOf(false) }
Button(
onClick = {
saving = true
scope.launch {
repository.save()
saving = false
}
},
enabled = !saving
) { Text("保存") }
}
rememberCoroutineScope与LaunchedEffect的差异:前者“启动时机由事件决定”,后者“启动时机由进入组合/key 变化决定”。
produceState:把异步流变成 State
produceState 把非 Compose 的异步源(如 callback、flow)转成 State,自带协程上下文与取消逻辑:
@Composable
fun loadImage(url: String): State<Bitmap?> = produceState(initialValue = null, url) {
value = CoilLoader.load(url) // 离开组合自动取消
}
@Composable
fun Avatar(url: String) {
val bitmap by loadImage(url)
if (bitmap != null) Image(bitmap.asImageBitmap(), null)
}
derivedStateOf 与 remember 的协作
回顾上一章,派生状态请配合 remember 使用,避免每次重组重新创建 derivedStateOf 实例。
AndroidView:在 Compose 中嵌入传统 View
并非所有组件都有 Compose 版本——地图、视频、广告、WebView 等仍需用 AndroidView 嵌入传统 View:
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun MapScreen() {
AndroidView(
factory = { ctx -> MapView(ctx).apply {
onCreate(null)
getMapAsync { /* 配置地图 */ }
} },
update = { map -> /* 每次 view 引用变化或重组时调用 */ },
onRelease = { map -> map.onDestroy() }
)
}
factory:首次创建 View 实例,仅在第一次组合时调用;update:每次重组(或 View 实例变化)时调用,用于同步 Compose 状态到 View;onRelease:View 离开组合时调用,用于释放资源。
同步 Compose 状态到 View
@Composable
fun WebViewScreen(url: String) {
AndroidView(
factory = { ctx -> WebView(ctx).apply {
settings.javaScriptEnabled = true
} },
update = { web -> web.loadUrl(url) } // url 变化时自动重新加载
)
}
update在每次url变化的重组时被调用,因此能“把 Compose 状态推到传统 View”。
ComposeView:把 Compose 嵌入 View 体系
反向场景:在传统 View/Fragment/Adapter 里嵌入一小段 Compose,用 ComposeView:
class LegacyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ComposeView(this).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
Text("我在传统布局里")
}
}
})
}
}
Fragment 中用 ComposeView 更常见,注意 viewLifecycleOwner:
class MyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, s: Bundle?
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
Surface {
Text("Fragment 中的 Compose")
}
}
}
}
}
setViewCompositionStrategy决定何时销毁 Compose:默认跟随 View 树的生命周期。务必显式设置以免泄漏。
AbstractComposeView:自定义 View 包装 Compose
如果要在传统 XML 里像普通 View 一样使用一段 Compose 内容,可以继承 AbstractComposeView:
class ComposeButtonView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var onClick: () -> Unit by mutableStateOf({})
var text: String by mutableStateOf("按钮")
@Composable
override fun Content() {
Button(onClick = onClick) { Text(text) }
}
}
这样在 XML 中能直接 <com.example.ComposeButtonView android:layout_width="wrap_content" />,把 Compose 包装成传统 View。
实战:带网络的列表页
下面把 ViewModel + collectAsStateWithLifecycle + 副作用 + 状态提升整合成一个完整的列表页骨架:
class ListViewModel : ViewModel() {
private val _state = MutableStateFlow(ListUiState())
val state: StateFlow<ListUiState> = _state.asStateFlow()
fun refresh() {
viewModelScope.launch {
_state.value = _state.value.copy(loading = true, error = null)
try {
val items = repo.fetch()
_state.value = _state.value.copy(loading = false, items = items)
} catch (e: Exception) {
_state.value = _state.value.copy(loading = false, error = e.message)
}
}
}
}
data class ListUiState(
val loading: Boolean = false,
val items: List<Item> = emptyList(),
val error: String? = null
)
@Composable
fun ListScreen(vm: ListViewModel = viewModel()) {
val state by vm.state.collectAsStateWithLifecycle()
// 进入屏幕自动加载一次
LaunchedEffect(Unit) { vm.refresh() }
Box {
if (state.loading) CircularProgressIndicator(Modifier.align(Center))
LazyColumn {
items(state.items) { item -> ItemRow(item) }
}
state.error?.let {
Text(it, color = Color.Red, modifier = Modifier.align(BottomCenter))
}
}
}
常见坑与最佳实践
- 在 Composable 里
new ViewModel():每次重组都创建新实例。务必用viewModel(),让它复用 Activity 范围内的实例。 - 用
collectAsState而非collectAsStateWithLifecycle:后台仍在收集,浪费电量和流量。新项目一律WithLifecycle。 LaunchedEffect的 key 错位:传了Unit但又依赖外部参数,导致 key 变化时不重启。仔细想清楚 key 与副作用的依赖关系。- 在副作用外启动协程:直接
GlobalScope或在remember内创建CoroutineScope,会泄漏。用LaunchedEffect或rememberCoroutineScope。 AndroidView里在factory中读取状态:factory仅执行一次,状态变化要在update中同步。ComposeView没设ViewCompositionStrategy:默认行为不一定符合预期,Fragment 里尤其要绑定viewLifecycleOwner。- ViewModel 暴露
MutableStateFlow:对外只暴露StateFlow/SharedFlow,自己持有Mutable*,避免 UI 反向修改。 - 在
LaunchedEffect中长时间持有 Composable 引用:协程取消前若持有 View 引用会泄漏。副作用块里尽量只引用 ViewModel 或不可变状态。 observeAsState默认值缺失:UI 上对null没处理会崩溃或显示空。给LiveData初始值或 UI 兜底。- 混合使用
LiveData/StateFlow导致双数据源:迁移期可以并存,但同一状态只应有一个可信源,否则会出现状态不一致。
章节小结
本章把 Compose 与架构层打通:viewModel() 复用 ViewModel 实例;StateFlow.collectAsStateWithLifecycle 是生命周期感知的推荐接入方式,LiveData.observeAsState 用于历史代码迁移;UI 通过方法调用把事件上传 ViewModel;副作用 API(LaunchedEffect、DisposableEffect、rememberCoroutineScope、produceState)规范了“不该在重组里做的事”的写法;AndroidView 用于把传统 View 嵌入 Compose,ComposeView/AbstractComposeView 反向嵌入,是迁移期的关键工具。
下一章预告
下一章进入多页面导航:用 navigation-compose 搭建 NavHost 与 composable 目标,掌握 NavController、参数传递(必选/可选)、navigation {} 嵌套 NavGraph、底部导航栏 BottomBar 集成以及深度链接(deep link)的接入。