I'm building a Clean Architecture MVVM app with Jetpack Compose and currently busy with the log in screen. I'll add relevant code snippets below but just to summarize the issue, I have a Firebase Auth sign in function in my repository class that I have converted to a suspend function with suspendCoroutine. I then pass this repository inside the viewModel where I launch a coroutine on the IO thread and invoke the repository's signIn function. I then created a data class encompassing the ui state, created a MutableStateFlow wrapping this data class inside the viewModel and then expose this StateFlow to the Compose UI. The logic for the sign in is as follows.
Inside my compose ui, whenever uiState.response == false (bad signIn result), I show a toast. This toast should be shown after my 2nd emission in the event that I signIn with incorrect credentials but it never gets displayed unless I add a delay of +- 400m.s between 2nd and 3rd emission leading me to believe that its almost as if the flow changes 'too quickly' for Compose to react to.
Code snippets/screenshots:
UserRepository:
suspend fun signIn(
username: String,
password: String
): Resource<Boolean> {
return suspendCoroutine { continuation ->
firebaseAuth.signInWithEmailAndPassword(username, password)
.addOnSuccessListener {
currentUser = User(username = username)
continuation.resume(Resource.Success(data = true ))
}
.addOnFailureListener { exception ->
continuation.resume(
Resource.Error(
data = false,
message = exception.message ?: "Error getting message"
)
)
}
}
}
ViewModel (Sorry for all the Logs):
private val _uiState = MutableStateFlow(LoginScreenUiState())
val uiState = _uiState.asStateFlow()
fun signIn(
username: String,
password: String
) {
Log.d("login", "Starting viewModel login")
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { it.copy(isLoading = true) }
Log.d("login", "Loading set to ${uiState.value.isLoading}")
val response = userRepository.signIn(username = username, password = password)
Log.d("login", "Firebase response acquired")
_uiState.update {
it.copy(
isLoading = false,
response = response,
)
}
Log.d("login",
"State updated to Loading = ${uiState.value.isLoading} \n " +
"with Response details : isError = ${uiState.value.response is Resource.Error} | with data = ${uiState.value.response.data} | and message = ${uiState.value.response.message}"
)
// delay(400) - Initially added this delay to allow Compose to "notice" the emission right after Firebase response acquired, want to find out why there had to be response in the first place
_uiState.update { it.copy(response = Resource.Error(data = null, message = "")) }
Log.d("login", "State reset to default state")
}
}
Compose UI:
Button(
onClick = {
Log.d("login", "button clicked")
signIn(username, password)
keyboardController?.hide()
focusManager.clearFocus(true)
},
modifier = Modifier
.padding(top = 10.dp)
.fillMaxWidth(0.7f),
enabled = !uiState.isLoading
) {
Text(text = "Login")
}
if (uiState.isLoading) {
CircularProgressIndicator()
}
when (uiState.response) {
is Resource.Success -> {
navigateToHome()
}
is Resource.Error -> {
if (uiState.response.data == false) {
Log.d("login", "showing error message in compose -> ${uiState.response.data}")
Toast.makeText(context, "${uiState.response.message}", Toast.LENGTH_SHORT).show()
}
}
}
StateFlow collection:
val uiState by viewModel.uiState.collectAsState()
Wrapper class for response:
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T?): Resource<T>(data)
class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
}
uiState data class:
data class LoginScreenUiState(
val isLoading: Boolean = false,
val response: Resource<Boolean> = Resource.Error(data = null, message = ""),
)
Logcat output for incorrect credentials:
Logcat screenshot
Despite this, no toast was seen where as it should be seen after Firebase returns
Resource.Error(data = false, message = "some error message")
Notes:
Things I tried:
_uiState.update{ }, I used emit() but this yielded the same result.You should create function in your ViewModel, say toastDisplayed(), that would reset the state to default. Your UI will get the error update, show the toast, and call toastDisplayed(), which will clear the error. This approach is described in Android App architecture docs here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With