1

I have weird behavior in my compose app, it looks like UI is being recompositioned multiple times, but I don’t really know why. Problem is related to main activity and root nav graph:

@AndroidEntryPoint
class AppMainActivity : ComponentActivity() {
    private val viewModel: UserStateViewModel by viewModels()

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

        installSplashScreen().apply {
            setKeepOnScreenCondition {
                Log.d("TESTOWO", "ViewModel addr in MainActivity: ${viewModel.hashCode()}")
                viewModel.userState.value == UserAuthState.UNKNOWN
            }
        }

        setContent {
            CarsLocalizerTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    RootNavGraph(navController = rememberNavController(), viewModel = hiltViewModel())
                }
            }
        }
    }
}

RootNavGraph

@Composable
fun RootNavGraph(
    navController: NavHostController,
    viewModel: UserStateViewModel,
) {
    Log.d("TESTOWO", "ViewModel addr in RootNavGraph: ${viewModel.hashCode()}")

    NavHost(
        navController = navController,
        startDestination = if (viewModel.userState.value == UserAuthState.AUTHENTICATED) HOME_ROUTE else AUTH_ROUTE,
        route = ROOT_ROUTE
    ) {
        authNavGraph(navController = navController)
        composable(route = HOME_ROUTE) {
            HomeScreen()
        }
    }
}

UserStateViewModel

@HiltViewModel
class UserStateViewModel @Inject constructor(private val repository: UserStateRepository): ViewModel() {

    private var _userState = mutableStateOf<UserAuthState>(UserAuthState.UNKNOWN)
    val userState: State<UserAuthState> get() = _userState

    private var authStateListenerCallback: FirebaseAuth.AuthStateListener = FirebaseAuth.AuthStateListener {
        _userState.value = if (it.currentUser != null) UserAuthState.AUTHENTICATED else UserAuthState.UNAUTHENTICATED
    }

    init {
        Log.d("TESTOWO", "View model created")
        startListeningToAuthState()
    }

    override fun onCleared() {
        super.onCleared()
        stopListeningToAuthState()
    }

    private fun startListeningToAuthState() {
        repository.setupAuthStateListener(authStateListenerCallback)
    }

    private fun stopListeningToAuthState() {
        repository.removeAuthStateListener(authStateListenerCallback)
    }

AuthNavGraph

fun NavGraphBuilder.authNavGraph(
    navController: NavController
) {
    navigation(
        route = AUTH_ROUTE,
        startDestination = Screen.Login.route
    ) {
        composable(
            route = Screen.Login.route
        ) {
            LoginScreen(navController = navController, viewModel = hiltViewModel())
        }
        composable(
            route = Screen.Register.route
        ) {
            RegisterScreen(navController = navController, viewModel = hiltViewModel())
        }
        composable(
            route = Screen.Onboarding.route
        ) {
            OnboardingScreen(navController = navController, viewModel = hiltViewModel())
        }
    }
}

At app start I see the following logs:

2022-12-08 12:09:15.668 22945-22945 TESTOWO                 com.example.carslocalizer            D  View model created
2022-12-08 12:09:15.668 22945-22945 TESTOWO                 com.example.carslocalizer            D  ViewModel addr in RootNavGraph: 15070924
2022-12-08 12:09:15.695 22945-22945 TESTOWO                 com.example.carslocalizer            D  ViewModel addr in MainActivity: 15070924
2022-12-08 12:09:15.728 22945-22945 TESTOWO                 com.example.carslocalizer            D  ViewModel addr in MainActivity: 15070924
2022-12-08 12:09:15.742 22945-22945 TESTOWO                 com.example.carslocalizer            D  ViewModel addr in RootNavGraph: 15070924
2022-12-08 12:09:15.747 22945-22945 TESTOWO                 com.example.carslocalizer            D  ViewModel addr in MainActivity: 15070924

At startup I need to check if user is already authenticated. If yes, he should be navigated to home screen, if not then navigate to authentication. It works almost correct. When splash screen ends I am still able to see login view for about 1s and after that app navigates to home. I was thinking that thanks to this splash screen check

setKeepOnScreenCondition {
    Log.d("TESTOWO", "ViewModel addr in MainActivity: ${viewModel.hashCode()}")
    viewModel.userState.value == UserAuthState.UNKNOWN
}

when RootNavGraph will be reached, user auth state will be already known and correct start destination will be set. But it looks like it’s not working this way… Does anybody know why UI is recompositioned so many times and why start destination is updated late?

gawron103
  • 167
  • 6
  • 21
  • Have you tried removing the `get() ` in your userState declaration? making it look like this `val userState: State = _userState` – z.g.y Dec 08 '22 at 13:55
  • Well actually no. But this is just a getter, it won't change anything – gawron103 Dec 08 '22 at 14:01
  • 1
    Tried, but no difference – gawron103 Dec 08 '22 at 14:06
  • can you include whats inside authNavGraph? – z.g.y Dec 08 '22 at 14:34
  • This might be too much to ask, but Im curious how the navContoller is called inside those composables like `LoginScreen`, `RegisterScreen` and `OnboardingScreen`, I have few encounters of similar "infinite recomposition" issues with jetpack compose nav, like this [one](https://stackoverflow.com/questions/74174614/jetpack-compose-navigation-loads-screen-infinitely/74176509#74176509) – z.g.y Dec 09 '22 at 02:53

0 Answers0