简体   繁体   English

对于 Compose UI,StateFlow 更新太快

[英]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.登录逻辑如下。

  1. Compose button invokes viewModel login function when clicked (onClick)撰写按钮在单击时调用 viewModel 登录 function (onClick)
  2. ViewModel launches coroutine and sets uiState.isLoading -> true (1st flow emission) ViewModel 启动协程并设置 uiState.isLoading -> true(第一次流发射)
  3. Invoke userRepository.signIn()调用 userRepository.signIn()
  4. Update StateFlow with isLoading -> false, response -> userRepository signIn's return (2nd emission)使用 isLoading -> false,response -> userRepository signIn 的返回更新 StateFlow(第二次发射)
  5. Reset uiState to response -> null (3rd emission)将 uiState 重置为响应 -> null(第三次发射)

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 凭据不正确:

Logcat screenshot日志截图

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:笔记:

  • The reason why I had to reset to default state at the end was because if the screen recomposed (if I started editing my textfield to fix credentials) then the toast would be shown again.最后我不得不重置为默认值 state 的原因是,如果屏幕重新组合(如果我开始编辑我的文本字段以修复凭据),那么 toast 将再次显示。
  • I'm aware that this can be solved with 2 alternatives, one being having a flag inside Compose to ensure that the toast is only shown once after button is clicked and the other method being that I can pass callback parameter to viewModel sign in and invoke it inside compose when the success case happens.我知道这可以通过两种选择来解决,一种是在 Compose 中有一个标志,以确保在单击按钮后只显示一次吐司,另一种方法是我可以将回调参数传递给 viewModel 登录并调用当成功案例发生时,它会在里面组合。 I am not opposed to the first method if there's no better way but for the second one, I prefer to use the observer pattern.如果没有更好的方法,我不反对第一种方法,但是对于第二种方法,我更喜欢使用观察者模式。

Things I tried:我尝试过的事情:

  1. Tried to use SharedFlow and instead of invoking _uiState.update{ } , I used emit() but this yielded the same result.尝试使用 SharedFlow 而不是调用_uiState.update{ } ,我使用了emit()但这产生了相同的结果。
  2. As mentioned above, I tried using callbacks passed into the signIn function and invoking on API returns and this worked but I would prefer to use the observer pattern.如上所述,我尝试使用传递给 signIn function 的回调并调用 API 返回,这有效,但我更愿意使用观察者模式。
  3. Beyond this, I've gone through a lot of docs/articles, and I couldn't find this issue on StackOverflow.除此之外,我浏览了很多文档/文章,但在 StackOverflow 上找不到这个问题。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM