在Compose Desktop实现简单消息通知

对于手机用户来说,Toast是一个司空见惯的东西,它在一个小窗口中显示简短的消息反馈。

然而这是一个手机端独有的东西,在桌面端我们怎么显示类似的提示呢?

与Toast提示关联的概念

Toast本身是用来展示一个简短的消息提示,然而可以用来展示消息提示的组件却有很多。他们与Toast有哪些联系和区别?

MessageBox

MessageBox是电脑端最常见的消息对话框,它通常是打断的 ,甚至模态的。通常在向用户通知或询问重要信息时才会使用,例如,当用户尝试在没有保存文件时尝试直接退出应用询问用户是否保存。

vbs 复制代码
result = MsgBox("你想知道生命的意义吗,你想真正地活着吗?", vbYesNo, "人生选择")

If result = vbYes Then
    CreateObject("WScript.Shell").Run "https://www.qidian.com/book/109222/"
Else
    CreateObject("WScript.Shell").Run "https://www.qidian.com/book/3347854/"
End If

这种标准消息提示实现简单,因为很多编程语言中都内置了,在Win32 API中也直接提供,所以在桌面场景使用相当广泛。

Compose中也提供类似的对话框组件,美观且与界面风格统一,但是用起来就没有那么简便了

Notification

通知(Notification)是展示在用户界面之外的消息,用来给用户提供实时消息提醒,如来自其他用户的消息、新闻、广告和优惠信息等。

在桌面操作系统上也有类似的通知概念。

相较于Toast,通知脱离了当前应用,可以向用户传达一些实时消息或重要状态,但显然更重一些。

气泡提示

在Windows XP时代,在托盘区显示一个icon看上去很高逼格,而在icon上弹个通知就更厉害了。因为Windows早年这个通知是个气泡的形式,因此又称为气泡提示。

短暂出现,自动消失,这是与我们要的Toast概念最接近的东西了,某种意义上来说它是Windows通知中心的前身。然而感觉它更适合后台运行的应用刷存在感,用户本来在窗口里操作,在托盘区弹出提示反而会转移注意力

Tooltip

Tooltip是桌面应用常见的交互元素,当用户鼠标在按钮或其他可交互组件上悬停时,向用户提示当前操作的相关信息。

状态栏

状态栏是父窗口底部的水平窗口,应用程序在这里可以显示各种状态信息。

状态栏能直接向用户提供可能关心的状态信息,如文档的字数,当前的操作处理进度等。它能给用户提供很多的信息增量,且信息实时更新的。对于短暂的提示就不太合适了。

封装一个Toast工具

桌面端的交互设计很多,但我感觉没有一个可以替代Toast的使用场景,我还是需要一个轻量、自动消失、不打断用户的提示方式。然而Compose显然没有提供这样的标准API,那么我们只好尝试封装一个。

实现界面外观

实现Toast的界面外观是不复杂的,就是搞一段文本显示到界面的下方嘛。

kotlin 复制代码
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.BottomCenter
        ) {
            val messages = remember { mutableStateListOf<ToastMessage>() }
            val scope = rememberCoroutineScope()
            App(onShowToast = { message ->
                scope.launch {
                    messages.add(message)
                    delay(message.duration)
                    messages.remove(message)
                }
            })

            messages.toList().forEachIndexed { index, message ->
                Surface(
                    shape = MaterialTheme.shapes.medium,
                    color = Color.Black.copy(alpha = 0.7f),
                    tonalElevation = 16.dp,
                    shadowElevation = 16.dp,
                    modifier = Modifier
                        .padding(bottom = 24.dp + 48.dp * index)
                ) {
                    Text(
                        text = message.text,
                        color = Color.White,
                        style = MaterialTheme.typography.bodySmall,
                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
                    )
                }
            }
        }

界面展示没有问题,但是这段代码实用度为0。因为真实的业务场景中我们不可能在App()方法中直接放置我们的业务代码,肯定嵌套了无数层,要在点击按钮的位置把事件一路透传到根层级,想想都觉得头皮发麻。

CompositionLocal封装

为了支持无需将颜色作为显式参数依赖项传递给大多数可组合项,Compose 提供了 CompositionLocal,可让您创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式。

Compose中提供了CompositionLocal,以便略过中间层,直接将参数传递到使用处。Compose中也大量使用这种方式,传递类似颜色、样式之类的数据。

首先定义个操作接口

kotlin 复制代码
interface IToastController {
    fun show(text: String, duration: Long = 3000)
    fun getMessages(): List<ToastMessage>
}

定义CompositionLocal

kotlin 复制代码
val LocalToastController = staticCompositionLocalOf<IToastController> {
    error("No ToastController Provided. Make sure to wrap your app with ToastHost.")
}

封装一个ToastHost用来提供LocalToastController

kotlin 复制代码
@Composable
fun ToastHost(content: @Composable () -> Unit) {
    val coroutineScope = rememberCoroutineScope()
    val controller = remember(coroutineScope) {
        DefaultToastController(coroutineScope)
    }

    CompositionLocalProvider(LocalToastController provides controller) {
        content()

        val messages by remember { derivedStateOf { controller.getMessages() }}

        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.BottomCenter
        ) {
            messages.forEachIndexed { index, message ->
                Surface(
                    shape = MaterialTheme.shapes.medium,
                    color = Color.Black.copy(alpha = 0.7f),
                    tonalElevation = 16.dp,
                    shadowElevation = 16.dp,
                    modifier = Modifier
                        .padding(bottom = 24.dp + 48.dp * index)
                ) {
                    Text(
                        text = message.text,
                        color = Color.White,
                        style = MaterialTheme.typography.bodySmall,
                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
                    )
                }
            }
        }
    }
}

接下来只要把这个ToastHost包装到App()方法的外面,就可以在任意层级弹出Toast了。

kotlin 复制代码
// main.kt
ToastHost {
    App()
}

// 使用样例
@Composable
fun DemoUsage(modifier: Modifier) {
    val toastController = LocalToastController.current
    Button(onClick = {
        toastController.show("Hello World!")
    }) {
        Text("show toast")
    }
}

完整代码我贴到了Github

相关推荐
alexhilton6 小时前
学会说不!让你彻底学会Kotlin Flow的取消机制
android·kotlin·android jetpack
Monkey-旭2 天前
Android Bitmap 完全指南:从基础到高级优化
android·java·人工智能·计算机视觉·kotlin·位图·bitmap
随笔记2 天前
uniapp蓝牙连接设备并发送接收信息
javascript·uni-app·app
不爱说话郭德纲2 天前
别再花冤枉钱!手把手教你免费生成iOS证书(.p12) + 打包IPA(超详细)
前端·ios·app
iOS阿玮2 天前
凭一己之力干穿一个品牌,互联网+时代口碑比以前更重要了!
uni-app·app·apple
Monkey-旭3 天前
深入理解 Kotlin Flow:异步数据流处理的艺术
android·开发语言·kotlin·响应式编程·flow
程序员江同学4 天前
Kotlin 技术月报 | 2025 年 7 月
android·kotlin
_frank2224 天前
kotlin使用mybatis plus lambdaQuery报错
开发语言·kotlin·mybatis
Bryce李小白4 天前
Kotlin实现Retrofit风格的网络请求封装
网络·kotlin·retrofit