Search code examples
androidandroid-jetpack-composekotlin-coroutineskotlin-flownavcontroller

Navigate to next screen when the api call is finished


I want to go to another screen only when the api call is finished. I'm using the below code and it works fine in splash screen (i can see the api call happening in log cat) but when i navigate and try to collect the flow in that screen nothing is showing (like the bottomsheets based on result) what is the problem here ? is there a way to achieve this? i would appreciate any help and solution.

my viewmodel:

@HiltViewModel
class MainViewModel @Inject constructor(
    private val mainRepo: MainRepository,
    @ApplicationContext private val context:Context
) : ViewModel() {

    val statusFlow = MutableStateFlow<NetworkResult<Status>>(NetworkResult.Loading())
    var status by mutableStateOf(Status())



    fun getStatus(){
        viewModelScope.launch {
            statusFlow.emit(mainRepo.getStatus())
        }
    }
}

... rest of the viewmodel

splash screen:

@Composable
fun SplashScreen(navController: NavController,mainViewModel: MainViewModel = hiltViewModel()) {

    Splash()
    LaunchedEffect(true) {
        getAllApiCalls(mainViewModel)
        mainViewModel.statusFlow.collectLatest {
            when(it){
                is NetworkResult.Success -> {
                    navController.navigate(Screens.MainScreen.route) {
                        popUpTo(Screens.SplashScreen.route) {
                            inclusive = true
                        }
                    }
                }
                is NetworkResult.Loading -> {...}
                is NetworkResult.Error -> {...}
            }
        }

    }
}

private fun getAllApiCalls(mainViewModel: MainViewModel) {
    mainViewModel.getStatus()
}

my main screen:

@Composable
fun MainScreen(
    navController: NavController,
    mainViewModel: MainViewModel = hiltViewModel(),
) {

    Main(mainViewModel = mainViewModel)

}


@SuppressLint("CoroutineCreationDuringComposition")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Main(mainViewModel: MainViewModel) {

    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    val sheetState = rememberBottomSheetScaffoldState(
        bottomSheetState = SheetState(
            initialValue = SheetValue.Hidden,
            skipPartiallyExpanded = false,
        )
    )
    val drawerState = rememberDrawerState(
        initialValue = DrawerValue.Closed
    )


    val statusResult by mainViewModel.statusFlow.collectAsState()
    Log.e("TAG",statusResult.toString())
    when (statusResult) {
        is NetworkResult.Success -> {
            mainViewModel.status = statusResult.data ?: Status()
            Log.e("TAG"," hi ${mainViewModel.status.url}")
            scope.launch {

                if (mainViewModel.status.minVersion > VersionHelper.getVersionCode(context)!!) {
                    if (!sheetState.bottomSheetState.isVisible) {
                        drawerState.close()
                        mainViewModel.bottomSheetContents.value = BottomSheetContents.UPDATE
                        sheetState.bottomSheetState.expand()
                    }
                    return@launch
                }

                if (!mainViewModel.status.functional) {
                    if (!sheetState.bottomSheetState.isVisible) {
                        drawerState.close()
                        mainViewModel.bottomSheetContents.value =
                            BottomSheetContents.UNDER_CONSTRUCTION
                        sheetState.bottomSheetState.expand()
                    }
                    return@launch
                }

                if (!mainViewModel.status.reject) {
                    if (!sheetState.bottomSheetState.isVisible) {
                        drawerState.close()
                        mainViewModel.bottomSheetContents.value = BottomSheetContents.DISABLED_USER
                        sheetState.bottomSheetState.expand()
                    }
                    return@launch
                }
            }

        }

        is NetworkResult.Error -> {
            Log.e("TAG", "error api statusResult : ${statusResult.message}")
            LaunchedEffect(Unit) {
                if (!sheetState.bottomSheetState.isVisible) {
                    drawerState.close()
                    mainViewModel.bottomSheetContents.value =
                        BottomSheetContents.UNDER_CONSTRUCTION
                    sheetState.bottomSheetState.expand()
                }
            }
        }

        is NetworkResult.Loading -> {}
    }

... rest of the code

If i call the getAllApiCalls() function in the mainscreen the problem goes away but in this way i call the apis 2 times and that would defeat the whole purpose of calling in in the SplashScreen()


Solution

  • The problem here lies in how hiltViewModel scopes a ViewModel. By default, it will be scoped to the current NavGraphEntry. That means if your NavHost navigates to a new destination, the new destination will create its own ViewModel instance.

    In your case, the SplashScreen holds the ViewModel instance with the updated statusFlow, but then your MainScreen receives a completely new ViewModel instance with initial state.

    You can however obtain the ViewModel from a certain NavGraphEntry and pass the ViewModel into each Composable in your NavHost, as described in this stackoverflow answer:

    @Composable
    fun MyApp() {
        NavHost(navController, startDestination = startRoute) {
            // ...
            composable("exampleWithRoute") { backStackEntry ->
                val parentEntry = remember {
                  navController.getBackStackEntry(Screens.SplashScreen.route)  // get ViewModel instance from SplashScreen
                }
                val mainViewModel = hiltViewModel<MainViewModel>(
                  parentEntry
                )
                MainScreen(
                    navController,
                    mainViewModel
                )
            }
        }
    }
    

    To make that work, you however will have to remove the

    popUpTo(Screens.SplashScreen.route) {
        nclusive = true
    }
    

    otherwise your ViewModel is very likely going to be lost.

    Alternatively, you could consider to call hiltViewModel in the very root Composable of your app and then pass the instance to the NavHost and then further to each Composable.

    Please let me know if the approaches described here worked in your case!