-1

I need your help in the following problem.

When I navigate to a SignUpScreen and try to make a signup and force an error (by try to signup with already used email/password combination) I would like to show a snackbar. Unfortunately it looks like the recompose didn't take place because I didn't see the log messages and also not the snackbar after the state changed. I see the snackbar with a text of null at the initial compose which is fine for now because it makes sure the snackbar can be shown and the LaunchedEffect is triggered.

Here is the code that is involved: State, Viewmodel, Screen, and Log messages.

data class AuthUiState(
    val alreadySignUp: Boolean = false,
    val isAnonymous: Boolean = false,
    val user: FirebaseUser? = null,
    val authState: AuthState = AuthState.SignedOut,
    val isLoading: Boolean = false,
    val error: Throwable? = null,
    val idToken: String? = null,
    val errorMessage: String? = null,
) {
    val isAuthenticated: Boolean
        get() = authState == AuthState.SignedIn || authState == AuthState.Authenticated
}

enum class AuthState {
    SignedOut, // Not authenticated in Firebase.
    SignedIn, // Authenticated in Firebase using one of service providers, and not anonymous.
    Authenticated, // Anonymously authenticated in Firebase.
    Loading
}

And the Viewmodel:

@HiltViewModel
class AuthViewModel @Inject constructor(
    private val signInEmailPasswordUseCase: SignInEmailPasswordUseCase,
    private val signInAnonymously: SignInAnonymousUseCase,
    private val registerNewAccountUseCase: RegisterNewAccountUseCase,
    private val signOutUseCase: SignOutUseCase,
    private val signInWithGoogleUseCase: SignInWithGoogleUseCase,
    firebaseStringViewModel: FirebaseStringViewModel,
    private val userDataRepository: UserDataRepository,
) : ViewModel() {
    
    private val _authUiState = MutableStateFlow(AuthUiState())
    val authUiState: StateFlow<AuthUiState> = _authUiState.asStateFlow()

    fun onEvent(event: AuthUiEvent) {
        when (event) {
            is AuthUiEvent.RegisterNewAccount -> registerNewAccount(event.email, event.password)
        }
    }

    private fun registerNewAccount(email: String, password: String) {
        viewModelScope.launch() {
            _authUiState.update {
                Log.d("AuthViewModel", "registerNewAccount: authUiState hashcode: ${it.hashCode()}")
                it.copy(
                    authState = AuthState.Loading,
                    isLoading = true,
                )
            }

            val result = registerNewAccountUseCase.invoke(email, password)
            if (result.succeeded) {
                Log.d("AuthViewModel", "registerNewAccount success: $result")
                val user = result.getSuccessOrNull()?.user
                _authUiState.update {
                    it.copy(
                        isLoading = false,
                        alreadySignUp = true,
                        error = null,
                        errorMessage = null,
                        user = user,
                        isAnonymous = user?.isAnonymous ?: false,
                        authState = AuthState.SignedIn,
                    )
                }
            } else {
                _authUiState.update {
                    Log.d("AuthViewModel", "registerNewAccount error: authUiState hashcode: ${it.hashCode()}")
                    it.copy(
                        isLoading = false,
                        error = result.getExceptionOrNull(),
                        errorMessage = result.getExceptionOrNull()?.message,
                        user = null,
                        isAnonymous = false,
                        authState = AuthState.SignedOut,
                    )
                }
                Log.d("AuthViewModel", "_authUiState.value.errorMessage: ${_authUiState.value.errorMessage}")
            }
        }
    }

    fun clearError() {
        _authUiState.update {
            it.copy(
                error = null,
                errorMessage = null,
            )
        }
    }
}

And the Screen:

@Composable
internal fun SignUpRoute(
    onEvent: (AuthUiEvent) -> Unit,
    onLoginClick: () -> Unit,
    onSignUpSuccess: (email: String) -> Unit,
    onShowSnackbar: suspend (String, String?) -> Boolean,
    currentLanguageCode: String,
    firebaseStringUiState: FirebaseStringUiState,
    authViewModel: AuthViewModel = hiltViewModel(),
) {
    Log.d("SignUpRoute", "SignUpRoute triggered")

    SignupScreen(
        onEvent = onEvent,
        onLoginClick = onLoginClick,
        onSignUpSuccess = onSignUpSuccess,
        onClearError = { authViewModel.clearError() },
        currentLanguageCode = currentLanguageCode,
        onShowSnackbar = onShowSnackbar,
        authViewModel = authViewModel,
        firebaseStringUiState = firebaseStringUiState,
    )
}

@Composable
fun SignupScreen(
    onEvent: (AuthUiEvent) -> Unit,
    onLoginClick: () -> Unit,
    onSignUpSuccess: (email: String) -> Unit,
    onClearError: () -> Unit,
    currentLanguageCode: String,
    onShowSnackbar: suspend (String, String?) -> Boolean,
    authViewModel: AuthViewModel = hiltViewModel(),
    firebaseStringUiState: FirebaseStringUiState,
    modifier: Modifier = Modifier
) {

    val authUiState by authViewModel.authUiState.collectAsStateWithLifecycle()
    Log.d(
        "SignupScreen",
        "collectAsStateWithLifecycle: authUiState hashcode: ${authUiState.hashCode()}, errorMessage: ${authUiState.errorMessage}"
    )

    LaunchedEffect(key1 = authUiState.errorMessage) {
        Log.d(
            "SignupScreen",
            "LaunchedEffect: authUiState hashcode: ${authUiState.hashCode()}, errorMessage: ${authUiState.errorMessage}"
        )
        onShowSnackbar(authUiState.errorMessage.toString(), null)
    }

    //TextFields
    var email by remember { mutableStateOf(TextFieldValue("[email protected]")) }
    var password by remember { mutableStateOf(TextFieldValue("123456")) }
    var confirmPassword by remember { mutableStateOf(TextFieldValue("123456")) }
    var hasError by remember { mutableStateOf(false) }

    var passwordVisualTransformation by remember {
        mutableStateOf<VisualTransformation>( PasswordVisualTransformation() )
    }
    var confirmPasswordVisualTransformation by remember {
        mutableStateOf<VisualTransformation>( PasswordVisualTransformation() )
    }
    val passwordInteractionState = remember { MutableInteractionSource() }
    val confirmPasswordInteractionState = remember { MutableInteractionSource() }
    val emailInteractionState = remember { MutableInteractionSource() }

    val createAccount = firebaseStringUiState.stringValues["create_account"] ?: "Create Account"
    val confirmPasswordLabel = firebaseStringUiState.stringValues["password_confirm"] ?: "Confirm Password"
    val emailAddress = firebaseStringUiState.stringValues["email_address"] ?: "Email address"
    val passwordLabel = firebaseStringUiState.stringValues["password"] ?: "Password"
    val registerLabel = firebaseStringUiState.stringValues["register"] ?: "Register"

    if (authUiState.alreadySignUp) {
        onSignUpSuccess(email.text)
    }

    if (firebaseStringUiState.isLoading) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.fillMaxSize()
        ) {
            AromenLoadingWheel(contentDesc = "",)
        }
    } else {
        Scaffold(modifier = modifier) { paddingValues ->
            LazyColumn(
                verticalArrangement = Arrangement.Center,
                modifier = Modifier
                    .padding(paddingValues)
                    .fillMaxSize()
                    .padding(horizontal = 16.dp)
            ) {
                // Headline
                item { Spacer(modifier = Modifier.height(20.dp)) }
                item {
                    Text(
                        text = createAccount,
                        style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.ExtraBold),
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(top = 8.dp),
                        textAlign = TextAlign.Center
                    )
                }
                // Email address
                item {
                    OutlinedTextField(
                        value = email,
                        leadingIcon = {
                            Icon(
                                imageVector = AromenIcons.Email,
                                contentDescription = null,
                                tint = MaterialTheme.colorScheme.primary
                            )
                        },
                        maxLines = 1,
                        isError = hasError,
                        modifier = Modifier.fillMaxWidth(),
                        keyboardOptions = KeyboardOptions(
                            keyboardType = KeyboardType.Text,
                            imeAction = ImeAction.Next
                        ),
                        colors = OutlinedTextFieldDefaults.colors(
                            focusedBorderColor = MaterialTheme.colorScheme.primary,
                            unfocusedBorderColor = MaterialTheme.colorScheme.primary
                        ),
                        label = { Text(text = emailAddress) },
                        onValueChange = { email = it },
                        interactionSource = emailInteractionState,
                    )
                }
                // Password
                item {
                    OutlinedTextField(
                        value = password,
                        leadingIcon = {
                            Icon(
                                imageVector = AromenIcons.Lock,
                                contentDescription = null,
                                tint = MaterialTheme.colorScheme.primary
                            )
                        },
                        trailingIcon = {
                            Icon(
                                imageVector = AromenIcons.BaselineRemoveRedEye,
                                contentDescription = null,
                                tint = MaterialTheme.colorScheme.primary,
                                modifier = Modifier.clickable(onClick = {
                                    passwordVisualTransformation =
                                        if (passwordVisualTransformation != VisualTransformation.None) {
                                            VisualTransformation.None
                                        } else {
                                            PasswordVisualTransformation()
                                        }
                                })
                            )
                        },
                        colors = OutlinedTextFieldDefaults.colors(unfocusedBorderColor = MaterialTheme.colorScheme.primary),
                        maxLines = 1,
                        isError = hasError,
                        modifier = Modifier.fillMaxWidth(),
                        keyboardOptions = KeyboardOptions(
                            keyboardType = KeyboardType.Password,
                            imeAction = ImeAction.Done
                        ),
                        label = { Text(text = passwordLabel) },
                        onValueChange = { password = it },
                        interactionSource = passwordInteractionState,
                        visualTransformation = passwordVisualTransformation,
                    )
                }
                // Confirm Password
                item {
                    OutlinedTextField(
                        value = confirmPassword,
                        leadingIcon = {
                            Icon(
                                imageVector = Icons.Default.Lock,
                                contentDescription = null,
                                tint = MaterialTheme.colorScheme.primary
                            )
                        },
                        trailingIcon = {
                            Icon(
                                imageVector = AromenIcons.BaselineRemoveRedEye,
                                contentDescription = null,
                                tint = MaterialTheme.colorScheme.primary,
                                modifier = Modifier.clickable(onClick = {
                                    confirmPasswordVisualTransformation =
                                        if (confirmPasswordVisualTransformation != VisualTransformation.None) {
                                            VisualTransformation.None
                                        } else {
                                            PasswordVisualTransformation()
                                        }
                                }
                                )
                            )

                        },
                        colors = OutlinedTextFieldDefaults.colors(unfocusedBorderColor = MaterialTheme.colorScheme.primary),
                        maxLines = 1,
                        isError = hasError,
                        modifier = Modifier.fillMaxWidth(),
                        keyboardOptions = KeyboardOptions(
                            keyboardType = KeyboardType.Password,
                            imeAction = ImeAction.Done
                        ),
                        label = { Text(text = confirmPasswordLabel) },
                        onValueChange = { confirmPassword = it },
                        interactionSource = confirmPasswordInteractionState,
                        visualTransformation = confirmPasswordVisualTransformation,
                    )
                }
                // Register Button
                item {
                    Button(
                        onClick = {
                            if (invalidInput(email.text, password.text, confirmPassword.text)) {
                                hasError = true
                            } else {
                                hasError = false
                                onEvent(AuthUiEvent.RegisterNewAccount(email.text, password.text))
                                onClearError
                            }
                        },
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(vertical = 16.dp)
                            .height(50.dp)
                            .clip(CircleShape)
                    ) {
                        if (authUiState.isLoading) {
                            AromenLoadingWheel(contentDesc = "")
                        } else {
                            Text(text = registerLabel)
                        }
                    }
                }

                item { Spacer(modifier = Modifier.height(50.dp)) }
            }
        }
    }
}


fun invalidInput(email: String, password: String, confirmPassword: String): Boolean {
    if (email.isBlank() || password.isBlank() || confirmPassword.isBlank()) return true
    if (confirmPassword != password) return true
    return false
}

The Logmessages:

SignUpRoute         D  SignUpRoute triggered
SignupScreen        D  collectAsStateWithLifecycle: authUiState hashcode: 425474467, errorMessage: null
SignupScreen        D  LaunchedEffect: authUiState hashcode: 425474467, errorMessage: null
AuthViewModel       D  registerNewAccount: authUiState hashcode: 425474467
FirebaseAuth        I  Creating user with [email protected] with empty reCAPTCHA token
AuthRepositoryImpl  E  Firebase RegisterNewAccount Error: The email address is already in use by another account.
AuthViewModel       D  registerNewAccount error: authUiState hashcode: -280934312
AuthViewModel       D  _authUiState.value.errorMessage: The email address is already in use by another account.

From the log messages you can see the the signup process is initiated correctly and also produce the forced error. You could also see that the error message is correctly written to the authUiState and that it also generates a new object (because the hashcode is changed)

But I miss the recomposition of the SignupScreen after the changes of the authUiState.

5
  • Did you try diagnosing this stability issue by generating a compose compiler report? Also, if you are not using the Throwable other than showing the error message in the UI layer, please try removing it from the state and use the string error message only and not complicate the state object with stability issues.
    – Jay
    Commented Apr 20 at 12:34
  • 1
    Please edit the question to reduce your code to a minimal reproducible example that we can actually execute to see for ourselves.
    – tyg
    Commented Apr 20 at 15:02
  • @tyg: I think it already totally simple. The part of login can simulated by just writing the error inside the view model. For simulation you also maybe can skip the the event logic and the texts for the textfields could be hardcoded. Commented Apr 20 at 20:05
  • @Jay: I wasn't generating any compose compiler report. I will read about it and see if it will helps me Commented Apr 20 at 20:07
  • 1
    The code you provided is neither minimal (there is still a lot of unnecessary code), nor reproducible because it cannot be executed. You can try this for yourself by copying the code from the question to a new project and try to run it. Please edit to provide a minimal reproducible example.
    – tyg
    Commented Apr 20 at 22:36

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.