在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

相关推荐
FunnySaltyFish12 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker18 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker2 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
Haha_bj3 天前
Flutter——状态管理 Provider 详解
flutter·app
A0微声z4 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton4 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream4 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam4 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker5 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc5 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite