Jetpack Compose中消息提示Toast、 Snackbar、组件封装

前言

除了布局组件外,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

  • Snackbar

  • 自定义组件

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
// 启动一个协程来显示 Toast
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

// 显示 Toast 的函数
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 组件处理。

正确的做法是:

  1. 在 ViewModel 中定义一个状态来表示是否需要显示消息
  2. 在 Composable 中观察这个状态
  3. 当状态变化时,在 Composable 中显示消息提示

以下是一个实现示例:

事件类

首先,创建一个密封类来表示 UI 事件:

1
2
3
sealed class UiEvent {
data class ShowSnackbar(val message: String) : UiEvent()
}

发送事件

然后,在 ViewModel 中使用 ChannelStateFlow 来发送事件:

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() {
// 用于发送 UI 事件
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()

// 观察 UI 事件
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 {
// 2. 自定义时长:延迟 1600 毫秒后关闭
delay(1600) // 此处可替换为任意毫秒值(如 5000 表示5秒)

// 3. 关闭当前显示的 Snackbar(避免内存泄漏)
snackbarHostState.currentSnackbarData?.dismiss()
}
}

else -> {}
}
}
}



Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val mShape = RoundedCornerShape(8.dp)
// 用于显示 Snackbar 的宿主
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
)
}
// 2. 自定义时长:延迟 1000 毫秒(1秒)后关闭
delay(1000) // 此处可替换为任意毫秒值(如 5000 表示5秒)

// 3. 关闭当前显示的 Snackbar(避免内存泄漏)
snackbarHostState.currentSnackbarData?.dismiss()

组件引用

直接使用

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("未选择教材")

对话框

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("文字")
}
}