Search code examples
androidkotlinandroid-jetpack-composeandroid-jetpackjetpack-compose-navigation

Home and Auth navigation flows in Jetpack Compose


I have a problem with navigation in my Compose app. So starting from the beginning I want to have two navigation graphs: one for authentication related things and second for main functionalities, which should be accessible only after login. So typical case.

I want to use Splash Screen API from Android 12, so in my main activity I could do something like this:

class AppMainActivity : ComponentActivity() {

    private val viewModel by viewModels<SplashViewModel>()
    private var userAuthState: UserAuthState = UserAuthState.UNKNOWN

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

        viewModel.onEvent(SplashEvent.CheckAuthentication)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.isAuthenticated.collect {
                    userAuthState = it
                }
            }
        }

        setContent {
            CarsLocalizerTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    val scaffoldState = rememberScaffoldState()
                    val navController = rememberNavController()

                    Scaffold(
                        modifier = Modifier.fillMaxSize(),
                        scaffoldState = scaffoldState
                    ) {
                        val splash = installSplashScreen()
                        splash.setKeepOnScreenCondition {
                            userAuthState != UserAuthState.UNKNOWN
                        }

                        when (userAuthState) {
                            UserAuthState.UNAUTHENTICATED -> {
                                AuthNavigation(
                                    navController = navController,
                                    scaffoldState = scaffoldState
                                )
                            }
                            UserAuthState.AUTHENTICATED -> {
                                HomeScreen(
                                    viewModel = hiltViewModel(),
                                    onLogout = { navController.popBackStack() }
                                )
                            }
                            UserAuthState.UNKNOWN -> {}
                        }
                    }
                }
            }
        }
    }
}

Here I am collecting StateFlow from view model, which describes if user is authenticated already or no. If authenticated successfully then go to HomeScreen, which has HomeNavigation inside. If not authenticated, go to Authentication nav graph. Problem is that this approach would not work, since activity is created only once, so if I user will login, this when

when (userAuthState) {
    UserAuthState.UNAUTHENTICATED -> {
        AuthNavigation(
            navController = navController,
            scaffoldState = scaffoldState
        )
    }
    UserAuthState.AUTHENTICATED -> {
        HomeScreen(
            viewModel = hiltViewModel(),
            onLogout = { navController.popBackStack() }
        )
    }
    UserAuthState.UNKNOWN -> {}
}

Won’t be called again.

I was trying to find some solution for my problem but, can’t find anything helpful. Maybe somebody had such issue before, or saw something useful? Will be very glad for any help.

Rest of my code: SplashViewModel

@HiltViewModel
class SplashViewModel @Inject constructor(
    private val authenticateUseCase: AuthenticateUseCase
): ViewModel() {

    private val _isAuthenticated = MutableStateFlow<UserAuthState>(value = UserAuthState.UNKNOWN)
    val isAuthenticated: StateFlow<UserAuthState> = _isAuthenticated.asStateFlow()

    fun onEvent(event: SplashEvent) {
        when (event) {
            is SplashEvent.CheckAuthentication -> {
                viewModelScope.launch {
                    val result = authenticateUseCase()
                    when (result) {
                        true -> {
                            _isAuthenticated.emit(UserAuthState.AUTHENTICATED)
                        }
                        false -> {
                            _isAuthenticated.emit(UserAuthState.UNAUTHENTICATED)
                        }
                    }
                }
            }
        }
    }

}

AuthNavigation

@Composable
fun AuthNavigation(
    navController: NavHostController,
    scaffoldState: ScaffoldState
) {
    NavHost(
        navController = navController,
        startDestination = Screen.Login.route,
        modifier = Modifier.fillMaxSize()
    ) {
        composable(Screen.Login.route) {
            LoginScreen(
                onNavigate = { navController.navigate(it) } ,
                onLogin = {
                    navController.popBackStack()
                },
                scaffoldState = scaffoldState,
                viewModel = hiltViewModel()
            )
        }
        composable(Screen.Register.route) {
            RegisterScreen(
                onPopBackstack = { navController.popBackStack() },
                scaffoldState = scaffoldState,
                viewModel = hiltViewModel()
            )
        }
        composable(Screen.Onboarding.route) {
            OnboardingScreen(
                onCompleted = { navController.popBackStack() },
                viewModel = hiltViewModel()
            )
        }
    }
}

HomeScreen

@Composable
fun HomeScreen(
    viewModel: HomeViewModel,
    onLogout: () -> Unit
) {
    val navController = rememberNavController()
    val scaffoldState = rememberScaffoldState()

    var appBarTitle by remember {
        mutableStateOf("")
    }

    LaunchedEffect(key1 = true) {
        viewModel.userName.collectLatest {
            appBarTitle = "Hello $it"
        }
    }

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            if (appBarTitle.isEmpty()) {
                AppBarWithName("Hello")
            } else {
                AppBarWithName(appBarTitle)
            }
        },
        bottomBar = { BottomNavigationBar(navController) },
        content = { padding ->
            Box(modifier = Modifier.padding(padding)) {
                HomeNavigation(
                    navController = navController,
                    scaffoldState = scaffoldState,
                    onLogout = { onLogout() }
                )
            }
        }
    )
}

HomeNavigation

@Composable
fun HomeNavigation(
    navController: NavHostController,
    scaffoldState: ScaffoldState,
    onLogout: () -> Unit
) {
    NavHost(
        navController = navController,
        startDestination = Screen.Map.route
    ) {
        composable(Screen.Map.route) {
            MapScreen(viewModel = hiltViewModel())
        }
        composable(Screen.ManageCars.route) {
            ManageCarsScreen(
                viewModel = hiltViewModel(),
                scaffoldState = scaffoldState,
                onAddCar = {
                    navController.navigate(Screen.AddCar.route)
                }
            )
        }
        composable(Screen.AddCar.route) {
            AddCarScreen(
                viewModel = hiltViewModel(),
                onPopBackstack = {
                    navController.popBackStack()
                }
            )
        }
        composable(Screen.Logout.route) {
            LogoutDialogScreen(
                viewModel = hiltViewModel(),
                onLogout = {
                    navController.popBackStack()
                    onLogout()
                },
                onCancel = {
                    navController.popBackStack()
                }
            )
        }
    }
}

Solution

  • You need to store the UserAuthState as MutableState, this way, when the value is updated, setContent will automatically re-compose:

    private var userAuthState = mutableStateOf(UserAuthState.UNKNOWN)
    
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.isAuthenticated.collect { userAuthState.value = it }
        }
    }
    
    setContent { 
        when (userAuthState.value) {
            etc....
        } 
    }
    

    From the docs:

    mutableStateOf creates an observable MutableState, which is an observable type integrated with the compose runtime.

    Any changes to value schedules recomposition of any composable functions that read value.