0

When I navigate from one screen to another, the composable screen which I'm navigating away from is getting recomposed many times as a result. I'm just curious why this is happening and if i'm doing something wrong. This is the code for my NavHost which I call in my Scaffold in my MainScreen composable which is inside the setContent block of my MainActivity. So it's setContent -> MainScreen -> Scaffold -> MyAppNavHost

MainScreen

   MaterialTheme {
        val appState = rememberAppState()
        val navController = rememberNavController()
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val route = NavRoutes.allRoutes().find { navBackStackEntry?.destination?.route?.contains(it) == true }
        Scaffold(
            scaffoldState = appState.scaffoldState,
            snackbarHost = { hostState ->
                hostState.currentSnackbarData?.let {
                    SnackbarHost(
                        hostState = hostState,
                        snackbar = {
                            Snackbar(
                                snackbarData = it,
                                backgroundColor = Color.Red,
                                contentColor = Color.White
                            )
                        }
                    )
                }
            },
            backgroundColor = colorResource(id = R.color.windowBackground),
            topBar = {
                TopBar(
                    isVisible = route !in NavRoutes.topBarInvisibleRoutes(),
                    title = getTopBarTitle(route, navBackStackEntry?.arguments),
                    closeButtonEnabled = route in NavRoutes.topBarCloseIconRoutes()
                ) { navController.navigate(NavRoutes.Home.route) { popUpTo(navController.graph.id) { inclusive = true } } }
                     },
            bottomBar = { BottomNavBar(navController, viewModel.bottomBarState) }
        ) {
            MyAppNavHost(
                modifier = Modifier.padding(it),
                navController = navController,
                showSnackbar = { message, duration ->
                    appState.showSnackbar(message, duration) {}
                },
                startDestination = viewModel.getStartRoute()
            )
        }

        LaunchedEffect(navBackStackEntry) {
            handleBottomBarState(viewModel, route)
            if (route != null && route !in NavRoutes.noAuthRoutes()) {
                viewModel.rescheduleOrLogOut()
            }
        }

        if (viewModel.logoutState) {
            LogoutDialog(
                onDismiss = {
                    viewModel.logout()
                    navController.navigate(NavRoutes.Login.route) { popUpTo(navController.graph.id) { inclusive = true } }
                }
            )
        }
    }
}

NavHost

@Composable
fun MyAppNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: String = NavRoutes.Home.route,
    showSnackbar: (String, SnackbarDuration) -> Unit
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        composable(NavRoutes.OnBoarding.route) {
            OnBoardingScreen(navController = navController, showSnackbar)
        }
        composable(NavRoutes.Login.route) {
            LoginScreen(viewModel = koinNavViewModel(), navController = navController, showSnackbar = showSnackbar)
        }
        composable(NavRoutes.Home.route) {
            HomeScreen(homeViewModel = koinNavViewModel(), healthViewModel = koinNavViewModel(), navController = navController, showSnackbar = showSnackbar)
        }
        composable(
                route = "${NavRoutes.StartSurvey.route}/{surveyId}/{surveyTitle}",
            arguments = listOf(
                navArgument("surveyId") { type = NavType.StringType },
                navArgument("surveyTitle") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            val args = backStackEntry.arguments
            val surveyId = args?.getString("surveyId").orEmpty()
            val surveyTitle = args?.getString("surveyTitle").orEmpty()
            StartSurveyScreen(
                surveyId = surveyId,
                surveyTitle = surveyTitle,
                viewModel = koinNavViewModel { parametersOf(surveyId, surveyTitle) },
                navController = navController
            )
        }

I have tested this on 2 of my screens (showing LoginScreen as an example) and I'm getting the same behavior on both as mentioned in my first paragraph. "LoginScreen recomposed" appears over 10 times at once in Logcat when my app is navigating away from LoginScreen.

@Composable
fun LoginScreen(
    viewModel: LoginViewModel = koinViewModel(),
    navController: NavController,
    showSnackbar: (String, SnackbarDuration) -> Unit
) {
    val context = LocalContext.current

    Timber.tag("Recomposition").i("LoginScreen recomposed")
    val bankIdResultLauncher = bankIdResultLauncher(viewModel = viewModel)
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ScreenContent(
        isLoading = uiState.isLoading,
        userInput = viewModel.ssnInput.takeIf {
            it.length !=  SSN_LENGTH
        } ?: stringResource(id = R.string.saved_ssn, viewModel.ssnInput.substring(0, 8)),
        onInputValueChanged = { viewModel.updateSsnUserInput(it) },
        onLogInClick = { viewModel.sendBankIdRequest() },
        onKeyboardDone = {
            (context as Activity).hideKeyboard()
            viewModel.sendBankIdRequest()
        },
        onClearClick = { viewModel.clearInputText() }
    )
    HandleError(viewModel.error, showSnackbar)
    viewModel.loginRequest?.let {
        if (it is CallbackRequestSuccess) navController.navigate(NavRoutes.Home.route)
        HandleLoginRequest(
            request = it,
            context = context,
            onBankIdNotInstalled = { viewModel.listenAfterBankIdCallback() },
            onCallBackSuccess = { viewModel.callbackSuccess(null) },
            bankIdResultLauncher = bankIdResultLauncher
        )
    }
}

I used koinNavViewModel instead of koinViewModel for the viewmodels but I knew it wouldnt make a difference and it didn't. I've also checked this Jetpack Compose NavHost recomposition composable multiple times but It seems the recomposition is happening excessively in my case.

John N.
  • 1
  • 1
  • Nope, that's normal. – ianhanniballake Jul 08 '23 at 15:24
  • @ianhanniballake Thank you! Now I don't have to worry about this hurting performance :D – John N. Jul 09 '23 at 19:42
  • It should not be normal. Just created brand new Compose project from AS menu, added AnimatedNavHost with 2 distinct routes inside with simple buttons to navigate between them, and each of them have slide animation done as per the documentation. Navigating from screen A to screen B make the whole screen A to be recomposed 2 times, and popBack recomposed screen A 4 times. Even if I know it's probably caused by the animation, that's for sure not optimal. – Skyle Jul 11 '23 at 15:41
  • I thought so too but as per google documentation, a composable might recompose as often as every frame, and this is whats happening here during navigation. I don't know whats happening behind the scenes but I'm assuming the recomposition is not doing any heavy work and I certainly hope it isn't. And since the guy of the first comment is an android engineer at google, I'm trusting his answer. – John N. Jul 12 '23 at 16:48

0 Answers0