前言
除了布局组件外,Jetpack Compose 还提供了一系列其他常用的 UI 组件。
https://developer.android.google.cn/jetpack/compose/components?hl=zh-cn
https://developer.android.google.cn/courses/pathways/compose?hl=zh-cn
消息框
提示消息有以下几种方式
Toast
组件化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import android.annotation.SuppressLint import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch
@SuppressLint("CoroutineCreationDuringComposition") @Composable fun ShowToast(message: String) { val context = LocalContext.current CoroutineScope(Dispatchers.Main).launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } }
|
调用
1 2 3 4 5
| var showToast by remember { mutableStateOf(false) }
if (showToast) { ShowToast("哈哈"); }
|
组件内方法
1 2 3 4 5 6 7 8
| val context = LocalContext.current
fun showToast(message: String) { CoroutineScope(Dispatchers.Main).launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } }
|
Snackbar
基本示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Composable fun SnackbarExample() { var snackbarVisible by remember { mutableStateOf(false) }
Box { Button(onClick = { snackbarVisible = true }) { Text("显示 Snackbar") } if (snackbarVisible) { Snackbar( content = { Text(text = "文本") }, action = { Button(onClick = { snackbarVisible = false }) { Text(text = "关闭") } } ) } } }
|
使用snackbarHost
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
| val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = {
} ) { contentPadding -> Box { Button( modifier = Modifier.padding(10.dp).width(100.dp).height(40.dp), shape = MaterialTheme.shapes.medium, onClick = { scope.launch { snackbarHostState.showSnackbar("我是提示消息") } } ){ Text( text = "点击", fontSize = 16.sp, color = Color.White ) } } }
|
全局消息提示
在 Jetpack Compose 中,通常不建议直接在 ViewModel 中处理 UI 相关操作(如弹出消息提示),因为这违反了单一职责原则和关注点分离。
ViewModel 应该只负责管理数据和业务逻辑,而 UI 相关的操作应该由 Composable 组件处理。
正确的做法是:
- 在 ViewModel 中定义一个状态来表示是否需要显示消息
- 在 Composable 中观察这个状态
- 当状态变化时,在 Composable 中显示消息提示
以下是一个实现示例:
事件类
首先,创建一个密封类来表示 UI 事件:
1 2 3
| sealed class UiEvent { data class ShowSnackbar(val message: String) : UiEvent() }
|
发送事件
然后,在 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( viewModel: BaseViewModel ) { val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { viewModel.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 2 3 4
| snackbarHostState.showSnackbar( message = event.message, duration = SnackbarDuration.Short )
|
自定义间隔
1 2 3 4 5 6 7 8 9 10 11
| scope.launch { snackbarHostState.showSnackbar( message = event.message, duration = SnackbarDuration.Indefinite ) }
delay(1000)
snackbarHostState.currentSnackbarData?.dismiss()
|
组件引用
直接使用
1
| MsgReceiveComp(loginViewModel)
|
这种方式遵循了 Jetpack 的最佳实践,保持了 ViewModel 和 UI 层的分离,同时能够从 ViewModel 触发消息提示。
如果你需要使用 Toast 而不是 Snackbar,方法类似,只需在 Composable 中收到事件时调用 Toast.makeText() 即可。
全局使用
上面说的是组件内用,但是每个组件都这样整,优点繁琐,并且位置也不一定是整个屏幕,所以这里配置全局使用。
注意
MsgReceiveComp 全局只能有一处调用,否则会出问题。
在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("未选择教材")
|
对话框
1 2 3 4 5 6 7 8 9
| Dialog(onDismissRequest = { }) { Surface( modifier = Modifier.size(300.dp), shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.background ) { Text("文字") } }
|