前言
ViewModel一般都是用于数据的处理,Compose中渲染数据,处理消息提示或路由跳转。
但是有时我们想在ViewModel中触发某个逻辑后触发页面消息,或者路由跳转,该怎样实现呢?
- 一种是在vm中创建状态,页面中监听状态,只不过这样的写法不太优雅。
- 另一种是vm中发送事件,页面中监听事件。
下面就以全局消息提示来演示用法:
全局消息提示
在 Jetpack Compose 中,通常不建议直接在 ViewModel 中处理 UI 相关操作(如弹出消息提示),因为这违反了单一职责原则和关注点分离。
ViewModel 应该只负责管理数据和业务逻辑,而 UI 相关的操作应该由 Composable 组件处理。
正确的做法是:
- 在 ViewModel 中定义一个状态来表示是否需要显示消息
- 在 Composable 中观察这个状态
- 当状态变化时,在 Composable 中显示消息提示
以下是一个实现示例:
事件类
首先,创建一个密封类来表示 UI 事件:
1 2 3
| sealed class UiEvent { data class ShowSnackbar(val message: String) : UiEvent() }
|
VM发送事件
然后,在 ViewModel 中使用 Channel 或 StateFlow 来发送事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch
class BaseViewModel : ViewModel() { private val _uiEvents = Channel<UiEvent>() val uiEvents = _uiEvents.receiveAsFlow() fun showSnackbarAction(msg: String) { viewModelScope.launch { _uiEvents.send(UiEvent.ShowSnackbar(msg)) } } }
|
页面事件接收
在 Composable 中观察事件并显示消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.xhkjedu.zxs_android.model.UiEvent import com.xhkjedu.zxs_android.vm.BaseViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch
@Composable fun ZMsgReceiveComp( vm: BaseViewModel ) { val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { vm.uiEvents.collect { event -> when (event) { is UiEvent.ShowSnackbar -> { snackbarHostState.currentSnackbarData?.dismiss() scope.launch { snackbarHostState.showSnackbar( message = event.message, duration = SnackbarDuration.Indefinite ) } scope.launch { delay(1600)
snackbarHostState.currentSnackbarData?.dismiss() } }
else -> {} } } }
Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { val mShape = RoundedCornerShape(8.dp) SnackbarHost( hostState = snackbarHostState, modifier = Modifier.align(Alignment.Center) ) { snackbarData -> Popup( alignment = Alignment.Center, onDismissRequest = {}, properties = PopupProperties( focusable = false, dismissOnBackPress = true, dismissOnClickOutside = false ) ) { Box( modifier = Modifier .padding(30.dp) .wrapContentWidth() .height(46.dp) .background(color = Color(0x66000000), shape = mShape) .padding(start = 20.dp, end = 20.dp) ) { Text( text = snackbarData.visuals.message, fontSize = 14.sp, color = Color.White, modifier = Modifier.align(Alignment.Center) ) } }
} } }
|
注意
其中使用Popup,是为了防止消息被其他的Popup遮挡。
组件引用
直接使用
1
| MsgReceiveComp(loginViewModel)
|
这种方式遵循了 Jetpack 的最佳实践,保持了 ViewModel 和 UI 层的分离,同时能够从 ViewModel 触发消息提示。
如果你需要使用 Toast 而不是 Snackbar,方法类似,只需在 Composable 中收到事件时调用 Toast.makeText() 即可。
全局使用
上面说的是组件内用,但是每个组件都这样整,优点繁琐,并且位置也不一定是整个屏幕,所以这里配置全局使用。
在App的组件中添加接收组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.xhkjedu.zxs_android.common.CommonData import com.xhkjedu.zxs_android.componts.MsgReceiveComp import com.xhkjedu.zxs_android.route.AppNavigation import com.xhkjedu.zxs_android.ui.theme.AppAndroidTheme import com.xhkjedu.zxs_android.vm.AppViewModel
@Composable fun MyApp() { val mViewModel: AppViewModel = viewModel() CommonData.appViewModel = mViewModel AppAndroidTheme { Surface(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) { AppNavigation() MsgReceiveComp(mViewModel) } } } }
|
全局变量中添加
1 2 3
| object CommonData { var appViewModel: AppViewModel? = null }
|
使用的时候这样调用
1
| CommonData.appViewModel?.showSnackbarAction("未选择教材")
|