在基于Android Jetpack Compose的应用中,点击顶部导航按钮两次时会出现白屏,应用会卡死

移动开发 2026-07-09

我在应用中使用一个带有汉堡菜单图标的TopAppBar,以及一个导航抽屉(ModalNavigationDrawer + ModalDrawerSheet)。

问题发生在:

  • 我快速连续点击导航图标两次,或者
  • 我从主页屏幕打开抽屉,导航到设置屏幕,然后在返回主页后再次点击导航图标。

之后,应用有时会显示一个空白的白屏并且似乎卡死。日志中没有崩溃、错误信息或异常(Logcat)。

下面是演示此问题的动图

我的代码:

主页屏幕

@Composable
fun HomeScreen(
    stateHomeScreen: StateHomeScreen,
    event: (EventHomeScreen) -> Unit,
    navController: NavController,
    onMenuClick: () -> Unit,
    networkStatus: NetworkStatus,           
    snackbarHostState: SnackbarHostState 

) {
    val scope = rememberCoroutineScope()



    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background)
            .verticalScroll(state = rememberScrollState())
    ) {

        HomeHeader(
            onMenuClicked = { onMenuClick() },
            onSettingsClicked = {
                navController.navigate(Routes.SettingsScreen.route)
            },
        )

        Spacer(modifier = Modifier.height(MediumSpacerHeight))
        AppDropDownMenu(
            menuName = "Number of Questions:",
            menuList = Constants.numbersAsString,
            text = stateHomeScreen.numberOfQuizzes.toString(),
            onDropDownClick = {
                event(EventHomeScreen.SetNumberOfQuizzes(it.toInt()))
            })


        AppDropDownMenu(
            menuName = "Select Category:",
            menuList = Constants.categories,
            text = stateHomeScreen.category,
            onDropDownClick = {
                event(EventHomeScreen.SetQuizCategory(it))
            })


        AppDropDownMenu(
            menuName = "Select of Difficulty:",
            menuList = Constants.difficulty,
            text = stateHomeScreen.difficulty,
            onDropDownClick = {
                event(EventHomeScreen.SetQuizDifficulty(it))

            })


        AppDropDownMenu(
            menuName = "Select Type:", menuList = Constants.type,
            text = stateHomeScreen.type,
            onDropDownClick = {
                event(EventHomeScreen.SetQuizType(it))
            })

        AdBannerView() 
        Spacer(Modifier.height(Dimens.SmallPadding))


        ButtonBox(
            "Generate Quiz", padding = MediumPadding
        ) {
            if (networkStatus == NetworkStatus.Unavailable) {

                scope.launch {
                    snackbarHostState.showSnackbar(
                        message = "⚠️ No Internet Connection",
                        actionLabel = "Dismiss",
                        duration = SnackbarDuration.Long
                    )
                }
                return@ButtonBox
            } else {
                navController.navigate(
                    route = Routes.QuizScreen.passQuizParams(
                        stateHomeScreen.numberOfQuizzes,
                        stateHomeScreen.category,
                        difficulty = stateHomeScreen.difficulty,
                        type = stateHomeScreen.type
                    )
                )
            }
        }
        Spacer(modifier = Modifier.height(Dimens.LargePadding))


    }


}

@Composable
fun ButtonBox(text: String, padding: Dp, onButtonClicked: () -> Unit) {
    Button(
        onClick = { onButtonClicked() },
        modifier = Modifier
            .fillMaxWidth()
            .padding(padding)
    ) {
        Text(
            text, fontSize = Dimens.MediumTextSize,
            style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold)
        )
    }
}

主页头部

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeHeader(
    onMenuClicked: () -> Unit = {},   
    onSettingsClicked: () -> Unit = {}
) {
    TopAppBar(
        title = {
            Text(
                modifier = Modifier.fillMaxWidth(),
                text = "Quizzy",
                fontSize = HeroTextSize,
                fontWeight = FontWeight.Bold,
                color = MaterialTheme.colorScheme.secondary,

                textAlign = TextAlign.Center
            )
        },
        navigationIcon = {
            IconButton(
                onClick = {
                    Log.d("DRAWER_TEST", "Burger clicked!") 
                    onMenuClicked()
                },
                modifier = Modifier
                    .size(40.dp)
                    .padding(start = 8.dp),
            ) { 
                Icon(
                    imageVector = Icons.Default.Menu,
                    contentDescription = "Menu",
                    tint = MaterialTheme.colorScheme.secondary,
                )
            }
        },
        actions = {

            IconButton(onClick = onSettingsClicked) {
                Icon(
                    contentDescription = "Settings",
                    tint = colorResource(R.color.colorSecondary),
                    imageVector = Icons.Default.Settings,

                    )
            }
        },
        colors = TopAppBarDefaults.topAppBarColors(
            containerColor = colorResource(R.color.colorPrimaryVariant)
        ),

        modifier = Modifier.clip(
            RoundedCornerShape(
                bottomStart = Dimens.LargeCornerRadius,
                bottomEnd = Dimens.LargeCornerRadius
            )
        )
    )
}

主应用导航图

@Composable
fun MainAppNavGraph(
    currentUser: FirebaseUser?, onLogout: () -> Unit,
    networkStatus: NetworkStatus,           
    snackbarHostState: SnackbarHostState   
) {
    val navController = rememberNavController()
    val drawerState = rememberDrawerState(DrawerValue.Closed)
    val scope = rememberCoroutineScope()
    val context = LocalContext.current


    val currentBackStack by navController.currentBackStackEntryAsState()
    val currentRoute = currentBackStack?.destination?.route

    val drawerItems = listOf(
        DrawerItem("home", "Home", Icons.Default.Home, route = Routes.HomeScreen.route),
        DrawerItem("profile", "Profile", Icons.Default.Person, route = Routes.ProfileScreen.route),
        DrawerItem(
            "leader_board",
            "Leader Board",
            Icons.Default.Leaderboard,
            route = Routes.LeaderboardScreen.route
        ),

        DrawerItem(
            "remove_ads_for_30_mins",
            "Remove Ads?",
            iconRes = R.drawable.ad_blocker,
            route = null
        ),

        DrawerItem(
            id = "settings",
            title = "Settings",
            iconImageVector = Icons.Default.Settings,
            route = Routes.SettingsScreen.route
        )
    )


    val authViewModel = hiltViewModel<AuthViewModel>()
    val userState by authViewModel.userData.collectAsState()
    val quizViewModel = hiltViewModel<QuizViewModel>()

    var drawerGesturesEnabled by remember { mutableStateOf(true) }

    val homeScreenViewModel = hiltViewModel<HomeScreenViewModel>()
    val state by homeScreenViewModel.homeState.collectAsState()
    var isDrawerOpening by remember { mutableStateOf(false) }


    LaunchedEffect(currentUser?.uid) {
        currentUser?.uid?.let {
            authViewModel.loadUserData(it)
        }
    }

    LaunchedEffect(currentRoute) {
        drawerGesturesEnabled =
            (currentRoute != Routes.PrivacyPolicyScreen.route && currentRoute != Routes.TermsAndConditionsScreen.route)

        if (currentRoute == Routes.PrivacyPolicyScreen.route
            || currentRoute == Routes.TermsAndConditionsScreen.route
        ) {
            scope.launch { drawerState.close() }
        }
    }

    val userName = (userState as? Resource.Success<User>)?.data?.userName ?: "No User Logged"

    val userEmail =
        (userState as? Resource.Success)?.data?.email ?: "No Email"

    ModalNavigationDrawer(
        drawerState = drawerState,
        gesturesEnabled = drawerGesturesEnabled,
        drawerContent = {
            ModalDrawerSheet(
                windowInsets = WindowInsets(0, 0, 0, 0),
                drawerContainerColor = MaterialTheme.colorScheme.surface,
            ) {
                AppDrawer(
                    currentRoute = currentRoute ?: Routes.HomeScreen.route,
                    userName = userName,   
                    userEmail = userEmail,
                    drawerItems = drawerItems,
                    onItemClick = { item ->
                        item.route?.let {
                            navController.navigate(it) {
                                popUpTo(Routes.HomeScreen.route) { saveState = true }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    },
                    onLogoutClick = { 
                        scope.launch { drawerState.close() }
                        authViewModel.logout()
                        onLogout()
                    },
                    onCloseDrawer = {
                        scope.launch { drawerState.close() }
                    },
                    onRemoveAdsClick = {
                        if (AdManager.areAdsDisabled()) {
                            Toast.makeText(
                                context,
                                "Ads disabled — ${AdManager.getRemainingMinutes()} minutes remaining",
                                Toast.LENGTH_SHORT
                            ).show()
                        } else if (AdManager.isRewardedReady()) {
                            AdManager.showRewardedAd(
                                activity = context as Activity,
                                onSessionActivated = { remaining ->
                                    Toast.makeText(
                                        context,
                                        "🎉 Ads disabled for $remaining minutes!",
                                        Toast.LENGTH_LONG
                                    ).show()

                                },
                                onNotReady = {
                                    Toast.makeText(
                                        context,
                                        "Ad not ready, try again",
                                        Toast.LENGTH_SHORT
                                    ).show()
                                    AdManager.loadRewardedAd(context)
                                }
                            )
                        } else {
                            Toast.makeText(context, "Loading ad, try again", Toast.LENGTH_SHORT)
                                .show()
                            AdManager.loadRewardedAd(context)
                        }
                    }
                )
            }
        }
    ) {
        NavHost(
            navController = navController,
            startDestination = Routes.HomeScreen.route
        ) {
            composable(route = Routes.HomeScreen.route) {


                HomeScreen(
                    stateHomeScreen = state,
                    event = homeScreenViewModel::onEvent,
                    navController = navController,
                    onMenuClick = {
                Log.d("DRAWER", "onClick — isDrawerOpening=$isDrawerOpening, currentValue=${drawerState.currentValue}, isAnimating=${drawerState.isAnimationRunning}")

                if (!isDrawerOpening && !drawerState.isOpen && !drawerState.isAnimationRunning) {
                    isDrawerOpening = true
                    scope.launch {
                        try {
                            drawerState.open()
                        } catch (e: Exception) {
                            Log.e("DRAWER", "Error: ${e.message}", e)
                        } finally {
                            isDrawerOpening = false
                        }
                    }
                } else {
                    Log.d("DRAWER", "IGNORED")
                }
            },
                    networkStatus = networkStatus,
                    snackbarHostState = snackbarHostState
                )

            }


            composable(route = Routes.SettingsScreen.route) {
                SettingsScreen(
                    onBackClick = { navController.popBackStack() },
                    onPrivacyPolicyClick = { navController.navigate(Routes.PrivacyPolicyScreen.route) },
                    onTermsClick = { navController.navigate(Routes.TermsAndConditionsScreen.route) }
                )
            }


// Other Composable

    }
}

设置屏幕

@SuppressLint("LocalContextGetResourceValueCall")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
    onBackClick: () -> Unit,
    onPrivacyPolicyClick: () -> Unit,
    onTermsClick: () -> Unit,
    viewModel: SettingsViewModel = hiltViewModel<SettingsViewModel>()
) {
    val context = LocalContext.current
    val activity = context as? Activity
    val selectedMode by viewModel.selectedMode.collectAsState()


    var showThemeDialog by remember { mutableStateOf(false) }
    var showAboutDialog by remember { mutableStateOf(false) }
    var showRationaleDialog by remember { mutableStateOf(false) }
    var showSettingsDialog by remember { mutableStateOf(false) }



    val notificationLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        if (isGranted) {
            Toast.makeText(
                context,
                context.getString(R.string.notification_permission_granted),
                Toast.LENGTH_SHORT
            ).show()
            context.goToNotificationSettings()
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                val shouldShow = activity?.shouldShowRequestPermissionRationale(
                    Manifest.permission.POST_NOTIFICATIONS
                ) ?: false
                if (shouldShow) {
                    showRationaleDialog = true
                } else {
                    showSettingsDialog = true
                }
            }
        }
    }

    // ── Dialogs ──────────────────────────────────────────────────────────

    if (showThemeDialog) {
        ThemeSelectionDialog(
            currentMode = selectedMode,
            onDismiss = { showThemeDialog = false },
            onModeSelected = { mode ->

                Log.d("THEME", "saving mode = $mode")
                viewModel.updateMode(mode)
                showThemeDialog = false
            }
        )
    }

    // ── UI ───────────────────────────────────────────────────────────────

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background)

    ) {
        // AppBar
        TopAppBar(
            title = {
                Text(
                    text = stringResource(R.string.settings),
                    fontWeight = FontWeight.Bold,
                    color = MaterialTheme.colorScheme.onPrimary
                )
            },
            navigationIcon = {
                IconButton(onClick = onBackClick) {
                    Icon(
                        painter = painterResource(R.drawable.baseline_arrow_back_24),
                        contentDescription = "Back",
                        tint = MaterialTheme.colorScheme.onPrimary
                    )
                }
            },
            colors = TopAppBarDefaults.topAppBarColors(
                containerColor = MaterialTheme.colorScheme.primary
            )
        )

        Column(
            modifier = Modifier
                .fillMaxSize()
                .verticalScroll(rememberScrollState())
                .padding(horizontal = 16.dp)
        ) {
            val items = buildSettingsItems(
                onDarkModeClick = {
                    showThemeDialog = true
                },
                onNotificationClick = {
                    if (Build.VERSION.SDK_INT >= 33) {
                        notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
                    } else {
                        context.goToNotificationSettings()
                    }
                },
                onPrivacyPolicyClick = onPrivacyPolicyClick,
                onTermsClick = onTermsClick,
                onRateClick = { context.goToPlayStore() },
                onMoreAppsClick = { context.goToMoreApps() },
                // في buildSettingsItems — عدّل onRemoveAdsClick
                onRemoveAdsClick = {
                    if (AdManager.areAdsDisabled()) {
                        Toast.makeText(
                            context,
                            "Ads disabled - ${AdManager.getRemainingMinutes()} minutes remaining",
                            Toast.LENGTH_SHORT
                        ).show()
                    } else if (AdManager.isRewardedReady()) {
                        AdManager.showRewardedAd(
                            activity = context as Activity,
                            onSessionActivated = { remaining ->
                                Toast.makeText(
                                    context,
                                    "🎉 Ads disabled for $remaining minutes!",
                                    Toast.LENGTH_LONG
                                ).show()

                            },
                            onNotReady = {
                                Toast.makeText(
                                    context,
                                    "Ad not ready, try again",
                                    Toast.LENGTH_SHORT
                                ).show()
                                AdManager.loadRewardedAd(context)
                            }
                        )
                    } else {
                        Toast.makeText(context, "Loading ad, try again", Toast.LENGTH_SHORT).show()
                        AdManager.loadRewardedAd(context)
                    }
                },
                onAboutClick = { showAboutDialog = true }
            )

            items.forEachIndexed { index, item ->
                SettingsItemRow(item = item)
                if (index < items.lastIndex) {
                    HorizontalDivider(
                        color = MaterialTheme.colorScheme.outlineVariant,
                        thickness = 0.8.dp
                    )
                }
            }
        }
    }
}

完整的onCreate方法

    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            val settingsViewModel: SettingsViewModel = hiltViewModel()
            val selectedMode by settingsViewModel.selectedMode.collectAsState()
            val isSystemDark = isSystemInDarkTheme()
            val isDark = when (selectedMode) {
                AppCompatDelegate.MODE_NIGHT_YES -> true
                AppCompatDelegate.MODE_NIGHT_NO -> false
                else -> isSystemDark
            }

            val networkStatus by networkObserver.observe().collectAsState(
                initial = if (networkObserver.isConnectedNow())
                    NetworkStatus.Available else NetworkStatus.Unavailable
            )

            QuizzyTheme(darkTheme = isDark) {
                val snackbarHostState = remember { SnackbarHostState() }
                val scope = rememberCoroutineScope()


                LaunchedEffect(Unit) {
                    inAppUpdateViewModel.events.collect { event ->
                        when (event) {
                            is InAppUpdateUiEvent.StartUpdateFlow -> {
                                startUpdateFlow(event.updateType)
                            }
                            InAppUpdateUiEvent.CompleteFlexibleUpdate -> {
                                inAppUpdateViewModel.completeFlexibleUpdate()
                            }
                        }
                    }
                }


                val uiState by inAppUpdateViewModel.uiState.collectAsState()
                LaunchedEffect(uiState.isFlexibleUpdateDownloaded) {
                    if (uiState.isFlexibleUpdateDownloaded) {
                        val result = snackbarHostState.showSnackbar(
                            message = "Update downloaded",
                            actionLabel = "Restart",
                            duration = SnackbarDuration.Indefinite
                        )
                        if (result == SnackbarResult.ActionPerformed) {
                            inAppUpdateViewModel.completeFlexibleUpdate()
                        }
                    }
                }

                Scaffold(
                    snackbarHost = {
                        SnackbarHost(snackbarHostState) { data ->
                            Snackbar(
                                snackbarData = data,
//                                containerColor = MaterialTheme.colorScheme.primary,
//                                contentColor = Color.White
                            )
                        }
                    }
                ) { paddingValues ->
                    Column(
                        modifier = Modifier
                            .fillMaxSize()
                            .navigationBarsPadding()
                            .padding(paddingValues)
                    ) {

                        NetworkBanner(
                            networkStatus = networkStatus,
                            snackbarHostState = snackbarHostState
                        )

                        var currentUser by remember {
                            mutableStateOf(firebaseAuth.currentUser)
                        }

                        AnimatedContent(
                            targetState = currentUser != null,
                            transitionSpec = {
                                if (targetState) {
                                    (slideInHorizontally { it } + fadeIn()) togetherWith
                                            (slideOutHorizontally { -it } + fadeOut())
                                } else {
                                    (slideInHorizontally { -it } + fadeIn()) togetherWith
                                            (slideOutHorizontally { it } + fadeOut())
                                }
                            },
                            label = "auth_transition"
                        ) { isLoggedIn ->
                            if (isLoggedIn) {
                                MainAppNavGraph(
                                    currentUser = firebaseAuth.currentUser,
                                    onLogout = {
                                        firebaseAuth.signOut()
                                        currentUser = null
                                    },
                                    networkStatus = networkStatus,
                                    snackbarHostState = snackbarHostState
                                )
                            } else {
                                AuthNavGraph(
                                    onAuthSuccess = {
                                        currentUser = firebaseAuth.currentUser
                                    }
                                )
                            }
                        }
                    }
                }
            }
        }
    }

解决方案

当前的代码并未阻止 onBackClickSettingsScreen 的Composable中被调用两次,这随后会在 NavHost 中对 navController.popBackStack() 调用两次,导致返回栈被完全清空,从而没有任何目标页面被显示。

Jetpack Compose提供了一个便捷的工具函数 dropUnlessResumed,它仅在当前Composable的生命周期状态至少达到 State.RESUMED 时才执行传入的lambda:

IconButton(
    onClick = dropUnlessResumed { onBackClick () }
) {
    // ...
}

你需要添加以下依赖:

def lifecycle_version = "2.10.0"
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"

有了这段代码,一旦你第一次调用 popBackStack,该Composable的 Lifecycle.State 将不再是 State.RESUMED,因此第二次点击将被直接忽略。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章