StateFlow对于撰写UI更新太快



我正在用Jetpack Compose构建一个Clean Architecture MVVM应用程序,目前正忙于登录屏幕。我将在下面添加相关的代码片段,但只是为了总结这个问题,在我的存储库类中有一个Firebase认证登录函数,我已经使用suspendCoroutine将其转换为挂起函数。然后将此存储库传递到viewModel中,在viewModel中启动IO线程上的协程并调用存储库的signIn函数。然后我创建了一个包含ui状态的数据类,创建了一个MutableStateFlow,将这个数据类包装在viewModel中,然后将这个StateFlow公开给Compose ui。登录的逻辑如下:

  1. 撰写按钮在点击(onClick)时调用viewModel登录函数
  2. ViewModel启动协程并设置状态。isLoading→true(第一次排放)
  3. 调用userRepository.signIn ()
  4. 使用isLoading ->False,响应->userRepository signIn的返回(第二次发射)
  5. 重置状态为响应->零(第三次排放)

在我的撰写ui中,无论何时使用状态。response == false (signIn结果不好),我显示一个toast。如果我用不正确的凭据登录,这个toast应该在我第二次发射后显示但它永远不会显示,除非我增加了+- 400m的延迟。在第2次和第3次排放之间,我认为气流变化"太快",以至于Compose无法做出反应。

代码片段/截图:

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(对不起,所有的日志):

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

组合界面:

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集合:

val uiState by viewModel.uiState.collectAsState()

响应的包装类:

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

状态数据类:

data class LoginScreenUiState(
val isLoading: Boolean = false,
val response: Resource<Boolean> = Resource.Error(data = null, message = ""),
)

错误凭据的Logcat输出:

Logcat截图

尽管如此,在Firebase返回

后应该看到的地方没有看到toast。Resource.Error(data = false, message = "some error message")

指出:

  • 我必须在最后重置为默认状态的原因是因为如果屏幕重组(如果我开始编辑我的文本字段来修复凭据),那么吐司将再次显示。
  • 我知道这可以通过2种替代方案来解决,一种是在Compose内部有一个标志,以确保toast只在单击按钮后显示一次,另一种方法是,我可以将回调参数传递给viewModel登录,并在成功案例发生时在Compose内部调用它。如果没有更好的方法,我不反对第一种方法,但对于第二种方法,我更喜欢使用观察者模式。

我尝试过的事情:

  1. 尝试使用SharedFlow,而不是调用_uiState.update{ },我使用emit(),但这产生了相同的结果。
  2. 如上所述,我尝试使用回调传递到signIn函数和调用API返回,这工作,但我更愿意使用观察者模式。除此之外,我已经浏览了很多文档/文章,我无法在StackOverflow上找到这个问题。

您应该在ViewModel中创建函数,例如toastDisplayed(),将状态重置为默认状态。您的UI将获得错误更新,显示toast,并调用toastDisplayed(),这将清除错误。此方法在Android应用架构文档中有详细描述。

最新更新