Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

My view recompose itself muitple times after changing state

i am working on compose project. I have simple login page. After i click login button, loginState is set in viewmodel. The problem is when i set loginState after service call, my composable recomposed itself multiple times. Thus, navcontroller navigates multiple times. I don't understand the issue. Thanks for helping.

My composable :

@Composable
fun LoginScreen(
    navController: NavController,
    viewModel: LoginViewModel = hiltViewModel()
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly
    ) {
        val email by viewModel.email
        val password by viewModel.password
        val enabled by viewModel.enabled

        if (viewModel.loginState.value) {
            navController.navigate(Screen.HomeScreen.route) {
              popUpTo(Screen.LoginScreen.route) {
                 inclusive = true
              }
            }
        }

        LoginHeader()
        LoginForm(
            email = email,
            password = password,
            onEmailChange = { viewModel.onEmailChange(it) },
            onPasswordChange = { viewModel.onPasswordChange(it) }
        )
        LoginFooter(
            enabled,
            onLoginClick = {
                viewModel.login()
            },
            onRegisterClick = {
                navController.navigate(Screen.RegisterScreen.route)
            }
        )
    }

ViewModel Class:

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val loginRepository: LoginRepository,
) : BaseViewModel() {

    val email = mutableStateOf(EMPTY)
    val password = mutableStateOf(EMPTY)
    val enabled = mutableStateOf(false)
    val loginState = mutableStateOf(false)

    fun onEmailChange(email: String) {
        this.email.value = email
        checkIfInputsValid()
    }

    fun onPasswordChange(password: String) {
        this.password.value = password
        checkIfInputsValid()
    }

    private fun checkIfInputsValid() {
        enabled.value =
            Validator.isEmailValid(email.value) && Validator.isPasswordValid(password.value)
    }

    fun login() = viewModelScope.launch {
        val response = loginRepository.login(LoginRequest(email.value, password.value))
        loginRepository.saveSession(response)
        loginState.value = response.success ?: false
    }
}
like image 463
Berkay Kireçci Avatar asked Oct 20 '25 19:10

Berkay Kireçci


1 Answers

You should not cause side effects or change the state directly from the composable builder, because this will be performed on each recomposition.

Instead you can use side effects. In your case, LaunchedEffect can be used.

if (viewModel.loginState.value) {
    LaunchedEffect(Unit) {
        navController.navigate(Screen.HomeScreen.route) {
            popUpTo(Screen.LoginScreen.route) {
                inclusive = true
            }
        }
    }
}

But I think that much better solution is not to listen for change of loginState, but to make login a suspend function, wait it to finish and then perform navigation. You can get a coroutine scope which will be bind to your composable with rememberCoroutineScope. It can look like this:

suspend fun login() : Boolean {
    val response = loginRepository.login(LoginRequest(email.value, password.value))
    loginRepository.saveSession(response)
    return response.success ?: false
}

Also check out Google engineer thoughts about why you shouldn't pass NavController as a parameter in this answer (As per the Testing guide for Navigation Compose ...)


So your view after updates will look like:

@Composable
fun LoginScreen(
    viewModel: LoginViewModel = hiltViewModel(),
    onLoggedIn: () -> Unit,
    onRegister: () -> Unit,
) {
    // ...
    val scope = rememberCoroutineScope()
    LoginFooter(
        enabled,
        onLoginClick = {
            scope.launch {
                if (viewModel.login()) {
                    onLoggedIn()
                }
            }
        },
        onRegisterClick = onRegister
    )
    // ...
}

And your navigation route:

composable(route = "login") {
    LoginScreen(
        onLoggedIn = {
            navController.navigate(Screen.HomeScreen.route) {
                popUpTo(Screen.LoginScreen.route) {
                    inclusive = true
                }
            }
        },
        onRegister = {
            navController.navigate(Screen.RegisterScreen.route)
        }
    )
}
like image 193
Philip Dukhov Avatar answered Oct 23 '25 11:10

Philip Dukhov



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!