1

StateFlow state's change is keep updating even there is no push inside composable.

@Composable
fun ScreenController(
  viewModel: SyncViewModel, log: Logger,
  navigateToLogin: () -> Unit,
  navigateToSync: () -> Unit,
  navigateToAttendance: () -> Unit,
  navigateToDashBoard: () -> Unit,
 ) {

val state by viewModel.userCurrentState.collectAsState()
LaunchedEffect(Unit) {
    viewModel.getUserState()
}

state.let {
    if (!it.isDefault) {
        if (!it.isUserLoggedIn) {
            navigateToLogin()
        } else if (!it.isDataSyncedForToday) {
            navigateToSync()
        } else if (!it.isAttendanceMarked) {
            navigateToAttendance()
        } else {
            navigateToDashBoard()
        }
    }
  }
}

This compose is keep calling navigateToLogin method, even though the method to get user's current state is called only once(viewModel.getUserState()) inside the launch effect.

Below is the code of NavHost

val navController = rememberNavController()
NavHost(navController, startDestination = FULL_SCREEN) {
         composable(route = FULL_SCREEN) {
              ScreenController(
                        syncViewModel,
                        log,
                        navigateToLogin = { navController.navigate(LOGIN_SCREEN) },
                        navigateToSync = { navController.navigate(DATA_SYNC) },
                        navigateToAttendance = { navController.navigate(ATTENDANCE_SCREEN) },
                        navigateToDashBoard = { navController.navigate(BASE_DASHBOARD) }
                    )
                }

                composable(route = LOGIN_SCREEN) {
                    LoginScreen(navController, loginViewModel, log)
                }
            }

Due to this, the LoginScreen composable keeps on recomposing itself due to the state value sending the change events even if they are not modified.

So, I am trying to understand why state value is keep updating and how?

I have tried to make the state as life cycle aware for the particular composable.

val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleAwareLoginFlow = remember(viewModel.userCurrentState, lifecycleOwner) {
    viewModel.userCurrentState.flowWithLifecycle(lifecycleOwner.lifecycle)
}
val state: UserAppState by lifecycleAwareLoginFlow.collectAsState(initial = UserAppState())

But still, the state value change is happening.

Adding more code -

class SyncViewModel(
 private val syncRepository: SyncRepository,
log: Logger
) : ViewModel() {

  private val mutableUserState: MutableStateFlow<UserAppState> =
    MutableStateFlow(UserAppState())
  val userCurrentState: StateFlow<UserAppState> =  
       mutableUserState

 fun getUserState(): Job {
    return viewModelScope.launch {
        val isUserLoggedIn = syncRepository.isUserLoggedIn()
        val isSyncDone = syncRepository.isRequiredAPIsSynced()
        val isAttendanceMarked = 
        syncRepository.isAttendanceMarked()
        log.d("App State $isUserLoggedIn, $isSyncDone, 
         $isAttendanceMarked")
        mutableUserState.update {
            it.copy(
                isDefault = false,
                isUserLoggedIn = isUserLoggedIn,
                isDataSyncedForToday = isSyncDone,
                isAttendanceMarked = isAttendanceMarked,
            )
        }
    }
}
data class UserAppState(
  val isDefault: Boolean = true,
  val isUserLoggedIn: Boolean = true,
  val isDataSyncedForToday: Boolean = true,
  val isAttendanceMarked: Boolean = true,
)
Anshul Kabra
  • 129
  • 17

2 Answers2

2

As the state value should be observed only once, so I have moved the navigation logic inside the Launch effect.

state.let {
if (!it.isDefault) {
    LaunchedEffect(it) {
     if (!it.isUserLoggedIn) {
        navigateToLogin()
     } else if (!it.isDataSyncedForToday) {
        navigateToSync()
     } else if (!it.isAttendanceMarked) {
        navigateToAttendance()
     } else {
        navigateToDashBoard()
     }
   }

With this, the state change is not pushed again, this is a workaround for me and can help someone like me, please share the correct solution for the same.

Anshul Kabra
  • 129
  • 17
  • you should have a look at this https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3. – Raghunandan Mar 11 '23 at 15:23
  • @Raghunandan thank you for sharing the details. But the colllect as life cycle aware is not helping here. If I dont have navigation then the state change is working fine. Not sure when the navigation root changes the old page state got triggered. – Anshul Kabra Mar 12 '23 at 01:50
  • And if you see, I have already mentioned the state is already life cycle aware. I can’t use cold flow inside view model, due to the code is being shared in multiplatform. – Anshul Kabra Mar 12 '23 at 01:52
  • ha. got it. i was not aware of kmm – Raghunandan Mar 15 '23 at 05:40
-1

Rewrote the getUserState in the viewModel to wait for state to be updated

   fun getUserState(): Job {
    return viewModelScope.launch {
        val isUserLoggedIn = syncRepository.isUserLoggedIn()
        val isSyncDone = syncRepository.isRequiredAPIsSynced()
        val isAttendanceMarked =
            syncRepository.isAttendanceMarked()
        log.d("App State $isUserLoggedIn, $isSyncDone,
            $isAttendanceMarked")
        val job = CoroutineScope(Dispatchers.Main).launch {
            mutableUserState.update {
                it.copy(
                    isDefault = false,
                    isUserLoggedIn =  true,
                    isDataSyncedForToday = true,
                    isAttendanceMarked = true,
                )
            }
        }
        job.join()
    }
}

Then called in the same LaunchedEffect after the new state is retrieved

val state: UserAppState by viewModel.userCurrentState
                           .collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
    val job = taskViewModel.getUserState()
    job.join()
    state.let {
        if (!it.isDefault) {
            if (!it.isUserLoggedIn) {
                Log.e("nav", "login")
            } else if (!it.isDataSyncedForToday) {
                Log.e("nav", "sync")
            } else if (!it.isAttendanceMarked) {
                Log.e("nav", "attended")
            } else {
                onNavigateToFriends()
                Log.e("nav", "else")
            }
        }
    }
}
Cj_Rodriguez
  • 459
  • 2
  • 4