[英]StateFlow updates too quickly for Compose UI
I'm building a Clean Architecture MVVM app with Jetpack Compose and currently busy with the log in screen.我正在使用 Jetpack Compose 构建一个 Clean Architecture MVVM 应用程序,目前正忙于登录屏幕。 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.
我将在下面添加相关的代码片段,但只是为了总结这个问题,我在我的存储库 class 中有一个 Firebase 身份验证登录 function,我已经使用 suspendCoroutine 将其转换为暂停 function。 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.
然后我在 viewModel 中传递这个存储库,在 IO 线程上启动协程并调用存储库的登录 function。然后我创建了一个包含 ui state 的数据 class,创建了一个 MutableStateFlow 包装这个数据 class 然后在 StateFlow 中公开这个数据到撰写用户界面。 The logic for the sign in is as follows.
登录逻辑如下。
Inside my compose ui, whenever uiState.response == false (bad signIn result), I show a toast.在我的 compose ui 中,每当 uiState.response == false(错误的登录结果)时,我都会举杯祝酒。 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.
如果我使用不正确的凭据登录,则应在我第二次发射后显示此祝酒词,但除非我在第二次和第三次发射之间添加 +- 400m.s 的延迟,否则它永远不会显示,这让我相信它几乎就像流程一样变化“太快”以至于 Compose 无法做出反应。
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): ViewModel(抱歉所有日志):
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:包装器 class 用于响应:
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: uiState数据class:
data class LoginScreenUiState(
val isLoading: Boolean = false,
val response: Resource<Boolean> = Resource.Error(data = null, message = ""),
)
Logcat output for incorrect credentials: Logcat output 凭据不正确:
Despite this, no toast was seen where as it should be seen after Firebase returns尽管如此,在Firebase返回后,应该看到的地方没有看到toast
Resource.Error(data = false, message = "some error message")
Notes:笔记:
Things I tried:我尝试过的事情:
_uiState.update{ }
, I used emit()
but this yielded the same result._uiState.update{ }
,我使用了emit()
但这产生了相同的结果。You should create function in your ViewModel, say toastDisplayed()
, that would reset the state to default.您应该在 ViewModel 中创建 function,比如
toastDisplayed()
,这会将 state 重置为默认值。 Your UI will get the error update, show the toast, and call toastDisplayed()
, which will clear the error.您的 UI 将获得错误更新、显示 toast 并调用
toastDisplayed()
,这将清除错误。 This approach is described in Android App architecture docs here .此方法在此处的 Android 应用架构文档中进行了描述。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.