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
)
}
}
}

ViewModel 中消息提示

在 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
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
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 com.xhkjedu.zxs_android.model.UiEvent
import com.xhkjedu.zxs_android.vm.BaseViewModel
import kotlinx.coroutines.launch

@Composable
fun MsgReceiveComp(
viewModel: BaseViewModel
) {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

// 观察 UI 事件
LaunchedEffect(Unit) {
viewModel.uiEvents.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
scope.launch {
snackbarHostState.showSnackbar(
message = event.message
)
}
}
}
}
}

Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// 用于显示 Snackbar 的宿主
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

自定义样式

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
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
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 com.xhkjedu.zxs_android.model.UiEvent
import com.xhkjedu.zxs_android.vm.BaseViewModel
import kotlinx.coroutines.launch

@Composable
fun MsgReceiveComp(
viewModel: BaseViewModel
) {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

// 观察 UI 事件
LaunchedEffect(Unit) {
viewModel.uiEvents.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
scope.launch {
snackbarHostState.showSnackbar(
message = event.message
)
}
}
}
}
}

Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val mShape = RoundedCornerShape(8.dp)
// 用于显示 Snackbar 的宿主
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
) { snackbarData ->
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.height(46.dp)
.background(color = Color(0xaaffffff), shape = mShape)
.padding(8.dp)
) {
Text(
text = snackbarData.visuals.message,
fontSize = 18.sp,
modifier = Modifier.align(Alignment.CenterStart)
)
}

}
}
}

组件引用

直接使用

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