You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何防止同时点击双按钮时导航至两个页面?

问题描述

我的应用包含3个页面(MainScreen、Screen1和Screen2),MainScreen上设有两个用于跳转至其他页面的按钮。

当前应用效果如下:
应用当前效果{: width="300"}

我遇到的问题是:如果同时点击这两个按钮,两个目标页面都会被加入回退栈。我希望避免这种情况,只打开一个页面。具体要求是:MainScreen必须始终保留在回退栈中,且其上方只能存在Screen1或Screen2中的一个页面。该如何实现?


现有代码

MainActivity.kt

enum class Routes {
    MainScreen, Screen1, Screen2
}

sealed class UiEvent {
    data class Navigate(val route: String): UiEvent()
}

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TestTheme {
                val navController = rememberNavController()
                NavHost(
                    navController = navController,
                    startDestination = Routes.MainScreen.name
                ) {
                    composable(route = Routes.MainScreen.name) {
                        MainScreen(onNavigate = {
                            navController.navigate(route = it.route) {
                                launchSingleTop = true
                            }
                        },)
                    }
                    composable(route = Routes.Screen1.name) {
                        Screen1()
                    }
                    composable(route = Routes.Screen2.name) {
                        Screen2()
                    }
                }
            }
        }
    }
}

@Composable
fun MainScreen(
    onNavigate: (UiEvent.Navigate) -> Unit,
    viewModel: MainViewModel = hiltViewModel()
) {
    val lifecycleOwner = LocalLifecycleOwner.current

    LaunchedEffect(key1 = true) {
        viewModel.uiEvent.collectLatest { uiEvent ->
            when (uiEvent) {
                is UiEvent.Navigate -> {
                    onNavigate(uiEvent)
                }
            }
        }
    }

    Row {
        Button(
            onClick = {
                val currentState = lifecycleOwner.lifecycle.currentState
                if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
                    viewModel.navigateTo(Routes.Screen1)
                }
            }
        ) {
            Text(text = "Screen1")
        }
        Button(
            onClick = {
                val currentState = lifecycleOwner.lifecycle.currentState
                if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
                    viewModel.navigateTo(Routes.Screen2)
                }
            }
        ) {
            Text(text = "Screen2")
        }
    }
}

@Composable
fun Screen1() { Text(text = "Screen 1") }

@Composable
fun Screen2() { Text(text = "Screen 2") }

MainViewModel.kt

@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {

    private val _uiEvent = Channel<UiEvent>()
    val uiEvent = _uiEvent.receiveAsFlow()

    fun navigateTo(route: Routes) {
        sendUiEvent(UiEvent.Navigate(route.name))
    }

    private fun sendUiEvent(event: UiEvent) {
        viewModelScope.launch {
            _uiEvent.send(event)
        }
    }
}

补充说明

我已修改代码和描述,并更新了图片。问题存在两种情况:

  • 快速先后点击两个按钮(已解决)
  • 同时点击两个按钮(未解决,如上方图片所示)

解决方案

要解决同时点击按钮导致双页面入栈的问题,可从导航栈结构控制和点击防抖两个维度处理:

1. 优化导航参数,固定回退栈结构

在navigate调用中补充popUpTo配置,确保MainScreen上方始终只有一个目标页面:

修改MainActivity中MainScreen的导航逻辑:

composable(route = Routes.MainScreen.name) {
    MainScreen(onNavigate = {
        navController.navigate(route = it.route) {
            launchSingleTop = true
            // 回退到MainScreen但保留它,同时清除其上方所有页面
            popUpTo(Routes.MainScreen.name) {
                saveState = true
                inclusive = false
            }
        }
    },)
}
  • popUpTo(Routes.MainScreen.name):导航时自动回退到MainScreen页面
  • inclusive = false:不包含MainScreen本身,确保它始终留在栈底
  • saveState = true:可选,保留MainScreen的页面状态

2. 添加点击防抖,禁止并发导航请求

在ViewModel中添加状态标记,防止同时处理多个跳转事件:

修改MainViewModel.kt:

@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {

    private val _uiEvent = Channel<UiEvent>()
    val uiEvent = _uiEvent.receiveAsFlow()
    // 标记当前是否正在处理导航
    private val _isNavigating = mutableStateOf(false)
    val isNavigating: State<Boolean> = _isNavigating

    fun navigateTo(route: Routes) {
        if (!_isNavigating.value) {
            _isNavigating.value = true
            sendUiEvent(UiEvent.Navigate(route.name))
        }
    }

    private fun sendUiEvent(event: UiEvent) {
        viewModelScope.launch {
            try {
                _uiEvent.send(event)
            } finally {
                // 导航事件发送完成后重置状态
                _isNavigating.value = false
            }
        }
    }
}

然后在MainScreen中根据isNavigating状态禁用按钮:

@Composable
fun MainScreen(
    onNavigate: (UiEvent.Navigate) -> Unit,
    viewModel: MainViewModel = hiltViewModel()
) {
    val lifecycleOwner = LocalLifecycleOwner.current
    val isNavigating by viewModel.isNavigating

    LaunchedEffect(key1 = true) {
        viewModel.uiEvent.collectLatest { uiEvent ->
            when (uiEvent) {
                is UiEvent.Navigate -> {
                    onNavigate(uiEvent)
                }
            }
        }
    }

    Row {
        Button(
            onClick = {
                val currentState = lifecycleOwner.lifecycle.currentState
                if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
                    viewModel.navigateTo(Routes.Screen1)
                }
            },
            enabled = !isNavigating
        ) {
            Text(text = "Screen1")
        }
        Button(
            onClick = {
                val currentState = lifecycleOwner.lifecycle.currentState
                if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
                    viewModel.navigateTo(Routes.Screen2)
                }
            },
            enabled = !isNavigating
        ) {
            Text(text = "Screen2")
        }
    }
}

3. 可选:增加目的地校验

导航前判断当前栈顶是否为目标页面,避免重复跳转:

composable(route = Routes.MainScreen.name) {
    MainScreen(onNavigate = {
        val currentRoute = navController.currentDestination?.route
        if (currentRoute == it.route) return@MainScreen
        
        navController.navigate(route = it.route) {
            launchSingleTop = true
            popUpTo(Routes.MainScreen.name) {
                saveState = true
                inclusive = false
            }
        }
    },)
}

内容的提问来源于stack exchange,提问作者Matteo Zattera

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 15:29:54