Home > Back-end >  How to resolve infinite loop causing by state change in android compose with firebase authentication
How to resolve infinite loop causing by state change in android compose with firebase authentication

Time:02-01

I have an application with sign in via email and password with firebase. I'm using jetpack compose with MVVM and clean architecture.

When getting from firebase that login was done I'm getting true in the view model and then listening to this state change in the composable.

The problem is I'm always get in the when statement of the sign in state and it cause an infinite loop that always navigating to the next composable.

Copying here the specific line from the view:

when (val response = viewModel.signInState.value) {
        is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
        is Response.Success -> if (response.data) {
            navController?.navigate(Screen.HomeScreen.route)
        }
        is Response.Error -> LaunchedEffect(Unit) {
            scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
        }
    }

My View:

@Composable
fun LoginScreen(
    navController: NavController?,
    viewModel: LoginViewModel = hiltViewModel()
) {
    val scaffoldState = rememberScaffoldState()
    Scaffold(scaffoldState = scaffoldState) {
        Column (
            modifier = Modifier
                .fillMaxHeight()
                .background(
                    Color.White
                ),
            verticalArrangement = Arrangement.Top,
            horizontalAlignment = Alignment.CenterHorizontally,
        ){
            var email = viewModel.email
            var pass = viewModel.password
            var passwordVisibility = viewModel.passwordVisibility
            TitleView(title = "SIGN IN", topImage = Icons.Filled.Person, modifier = Modifier)
            Spacer(modifier = Modifier.padding(20.dp))
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Top
            ) {
                OutlinedTextField(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 20.dp),
                    value = email.value,
                    onValueChange = viewModel::setEmail,
                    label = { Text( "Email")},
                    placeholder = { Text("Password") }
                )
                OutlinedTextField(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 20.dp),
                    value = pass.value,
                    onValueChange = viewModel::setPassword,
                    label = { Text( "Password")},
                    placeholder = { Text("Password") },
                    visualTransformation = if (passwordVisibility.value) VisualTransformation.None else PasswordVisualTransformation(),
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
                    trailingIcon = {
                        val image = if (passwordVisibility.value)
                            Icons.Filled.Visibility
                        else Icons.Filled.VisibilityOff

                        IconButton(onClick = {
                            viewModel.setPasswordVisibility(!passwordVisibility.value)
                        }) {
                            Icon(imageVector  = image, "")
                        }
                    }
                )
                Spacer(modifier = Modifier.padding(5.dp))
                Button(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 20.dp)
                        .border(
                            width = 2.dp,
                            brush = SolidColor(Color.Yellow),
                            shape = RoundedCornerShape(size = 10.dp)
                        ),
                    onClick = {
                        viewModel.signInWithEmailAndPassword()
                    },
                    colors = ButtonDefaults.buttonColors(
                        backgroundColor = Color.White
                    )
                ) {
                    Text(
                        text = "SIGN IN",
                        textAlign = TextAlign.Center,
                        fontSize = 20.sp,
                        style = TextStyle(
                            fontWeight = FontWeight.Thin
                        )
                    )
                }
                Spacer(modifier = Modifier.padding(5.dp))
                Button(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 20.dp)
                        .border(
                            width = 2.dp,
                            brush = SolidColor(Color.Yellow),
                            shape = RoundedCornerShape(size = 10.dp)
                        ),
                    onClick = {
                        viewModel.forgotPassword()
                    },
                    colors = ButtonDefaults.buttonColors(
                        backgroundColor = Color.White
                    )
                ) {
                    Text(
                        text = "FORGOT PASSWORD",
                        textAlign = TextAlign.Center,
                        fontSize = 20.sp,
                        style = TextStyle(
                            fontWeight = FontWeight.Thin
                        )
                    )
                }
            }
        }
    }

    when (val response = viewModel.signInState.value) {
        is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
        is Response.Success -> if (response.data) {
            navController?.navigate(Screen.HomeScreen.route)
        }
        is Response.Error -> LaunchedEffect(Unit) {
            scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
        }
    }

    when (val response = viewModel.forgotState.value) {
        is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
        is Response.Success -> if (response.data) {
            LaunchedEffect(Unit){
                scaffoldState.snackbarHostState.showSnackbar("Please click the link in the verification email sent to you", "", SnackbarDuration.Short)
            }
        }
        is Response.Error -> LaunchedEffect(Unit){
            scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
        }
    }
}

My View model:

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val useCases: UseCases
) : ViewModel() {
    private val _signInState = mutableStateOf<Response<Boolean>>(Response.Success(false))
    val signInState: State<Response<Boolean>> = _signInState

    private val _forgotState = mutableStateOf<Response<Boolean>>(Response.Success(false))
    val forgotState: State<Response<Boolean>> = _forgotState

    private val _email = mutableStateOf("")
    val email = _email
    fun setEmail(email: String) {
        _email.value = email
    }

    private val _password = mutableStateOf("")
    val password = _password
    fun setPassword(password: String) {
        _password.value = password
    }

    private val _passwordVisibility = mutableStateOf(false)
    val passwordVisibility = _passwordVisibility
    fun setPasswordVisibility(passwordVisibility: Boolean) {
        _passwordVisibility.value = passwordVisibility
    }

    fun signInWithEmailAndPassword() {
        viewModelScope.launch {
            useCases.signInWithEmailAndPassword(_email.value, _password.value).collect { response ->
                _signInState.value = response
            }
        }
    }

    fun forgotPassword() {
        viewModelScope.launch {
            useCases.forgotPassword(_email.value).collect { response ->
                _forgotState.value = response
            }
        }
    }
}

My Sign in function:

override suspend fun signInWithEmailAndPassword(
        email: String,
        password: String
    ) = flow {
        try {
            emit(Response.Loading)
            val result = auth.signInWithEmailAndPassword(email, password).await()
            if (result.user != null)
                emit(Response.Success(true))
            else
                emit(Response.Success(false))
        } catch (e: Exception) {
            emit(Response.Error(e.message ?: ERROR_MESSAGE))
        }
    }

CodePudding user response:

Navigation is also a side effect, after each frame of the animation you will again navigate to your destination. Change your block to:

when (val response = viewModel.signInState.value) {
    is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
    is Response.Success -> if (response.data) LaunchedEffect(response.data){ navController?.navigate(Screen.HomeScreen.route) }
    is Response.Error -> LaunchedEffect(Unit) {
        scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
    }
}
  •  Tags:  
  • Related