如何解决由 android 中的状态更改引起的无限循环与 firebase 身份验证组合

How to resolve infinite loop causing by state change in android compose with firebase authentication

我有一个通过电子邮件和密码登录 firebase 的应用程序。 我正在使用 Jetpack Compose 与 MVVM 和干净的架构。

当从 firebase 获取登录完成时,我在视图模型中得到 true,然后在可组合项中监听此状态变化。

问题是我总是进入登录状态的 when 语句,这会导致无限循环,总是导航到下一个可组合项。

将视图中的特定行复制到此处:

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)
        }
    }

我的看法:

@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)
        }
    }
}

我的视图模型:

@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
            }
        }
    }
}

我的登录功能:

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))
        }
    }

导航也是一个side effect,在动画的每一帧之后您将再次导航到您的目的地。将您的区块更改为:

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)
    }
}