有时候我们想要同步的等待Dialog返回结果,而不是通过异步回调,这在前端和Flutter中都是很容易实现的事情。在Android中并没有提供此类功能的默认实现,但通过kotlin和协程实现一个还是不难的。
官方的 SnackbarHost 组件也是可以同步等待返回结果的:
class SnackbarHostState {
private val mutex = Mutex()
var currentSnackbarData by mutableStateOf<SnackbarData?>(null)
private set
suspend fun showSnackbar(
message: String,
actionLabel: String? = null,
duration: SnackbarDuration = SnackbarDuration.Short
): SnackbarResult = mutex.withLock {
try {
return suspendCancellableCoroutine { continuation ->
currentSnackbarData = SnackbarDataImpl(message, actionLabel, duration, continuation)
}
} finally {
currentSnackbarData = null
}
}
}
所以我们可以参照 SnackbarHost 实现一个同步的Dialog。
由于 Compose Dialog 使用 Disposeable 实现了Dialog的自动隐藏,并且使用局部的协程环境启动的协程也是会自动取消,使得 Compose 中实现同步Dialog 相对简单。
Compose中的同步Dialog
在这里演示一个简单的 ConfrimDialog,包含标题、内容和一个确认按钮。可以用于用户确认弹框,比如询问是否同意协议、App更新、权限申请询问等。
首先我们需要定义一个 DialogState,用来封装Dialog中需要用到的数据(例如内容,标题等),并提显示和隐藏Dialog的方法。
class ConfirmDialogState {
var controller: ConfirmDialogController? by mutableStateOf(null)
private set
suspend fun show(title: String, content: String): Boolean {
try {
return suspendCancellableCoroutine { continuation ->
controller = ConfirmDialogController(
title = title,
content = content,
continuation = continuation,
)
}
} finally {
controller = null
}
}
}
class ConfirmDialogController(
val title: String,
val content: String,
private val continuation: CancellableContinuation<Boolean>,
) {
fun confirm() {
if (continuation.isActive) {
continuation.resume(true)
}
}
fun cancel() {
if (continuation.isActive) {
continuation.resume(false)
}
}
}
然后创建一个 ConfirmDialog 使用这个 ConfrimDialogState,通过判断 controller 是否为 null 来控制dialog的显示和隐藏。
@Composable
fun ConfirmDialog(
state: ConfirmDialogState
) {
state.controller?.let { controller ->
AlertDialog(
modifier = Modifier.fillMaxWidth(0.85f),
onDismissRequest = controller::cancel,
properties = DialogProperties(
// compose dialog bug,不设置为false的话连续弹出的弹框大小不变
usePlatformDefaultWidth = false,
),
title = { Text(text = controller.title) },
text = { Text(text = controller.content) },
confirmButton = {
Button(onClick = controller::confirm) {
Text(text = "Confirm")
}
},
dismissButton = {
Button(onClick = controller::cancel) {
Text(text = "Cancel")
}
}
)
}
}
最后,我们通过一个双确认弹框的案例来说明。用户同意两次弹框后,我们就打开外部浏览器。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
MainScreen(
openBrowser = {
val intent = Intent(Intent.ACTION_VIEW)
.setData("https://google.com".toUri())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
)
}
}
}
}
@Composable
private fun MainScreen(openBrowser: () -> Unit) {
val coroutineScope = rememberCoroutineScope()
val confirmDialogState = remember { ConfirmDialogState() }
ConfirmDialog(state = confirmDialogState)
Scaffold { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.padding(16.dp)
) {
Button(
onClick = {
coroutineScope.launch {
if (
confirmDialogState.show(
title = "Open Browser",
content = "Do you want to open browser?"
)
&&
confirmDialogState.show(
title = "Open Browser",
content = "Do you really want to open browser?"
)
) {
openBrowser()
}
}
},
modifier = Modifier.fillMaxWidth(),
) {
Text(text = "Open browser")
}
}
}
}
另外需要注意的是协程作用域的选择最好是当前组件的 rememberCoroutineScope()
,以防止窗体泄漏或内存泄漏。因为它的生命周期跟随组件,组件销毁时协程会自动取消,并让我们dialog自动隐藏。
下面是视频示例:
原生View中的同步Dialog
当然,原生Views版本的同步Dialog也很简单。
class ViewsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_views)
val openBtn = findViewById<Button>(R.id.open_btn)
openBtn.setOnClickListener {
lifecycleScope.launch {
if (showConfirmDialog(
context = this@ViewsActivity,
title = "Open Browser",
content = "Do you want to open browser?"
)
&&
showConfirmDialog(
context = this@ViewsActivity,
title = "Open Browser",
content = "Do you really want to open browser?"
)
) {
openBrowser()
} else {
finish()
}
}
}
}
private fun openBrowser() {
try {
val intent = Intent(Intent.ACTION_VIEW)
.setData("https://google.com".toUri())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
}
}
}
private suspend fun showConfirmDialog(
context: Context,
title: String,
content: String,
): Boolean {
return suspendCancellableCoroutine { continuation ->
AlertDialog.Builder(context)
.setTitle(title)
.setMessage(content)
.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
.setPositiveButton("Confirm") { _, _ ->
if (continuation.isActive) {
continuation.resume(true)
}
}
.setOnCancelListener {
if (continuation.isActive) {
continuation.resume(false)
}
}
.show()
}
}
如果没有协程和kotlin,也有使用handler版本实现的方式,可以参考这个: https://news.ebscer.com/2016/04/synchronous-alertdialogs-in-android/
Ps: 很多异步回调的API都可以使用协程转为同步调用,但某些场景需要考虑超时、异常处理等。