发出 UI 状态时,同时进行收集不会更新 UI。

huangapple go评论46阅读模式
英文:

Emitting ui state while collecting does not update ui

问题

以下是代码部分的翻译:

This init block is in my ViewModel:
这个初始化块位于我的ViewModel中:

init {
    viewModelScope.launch {
        userRepository.login()
        userRepository.user.collect {
            _uiState.value = UiState.Success(it)
        }
    }
}

This is very similar to what's actually written on the app, but even this simple example doesn't work. After userRepository.login(), user which is a SharedFlow emits a new user state. This latest value DOES get collected within this collect function shown above, but when emitting a new uiState containing the result, the view does not get such update.
这与实际在应用程序中编写的内容非常相似,但即使这个简单的示例也不起作用。在userRepository.login()之后,user,一个SharedFlow,会发出一个新的用户状态。在上面显示的collect函数中,确实会收集到这个最新值,但在发出包含结果的新uiState时,视图没有获得此类更新。

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Doing this for some reason does not work. I suspect the issue is related to the lifecycle of the viewmodel, because when I treat the viewmodel as a singleton, this doesn't happen. It happens only when the viewmodel gets destroyed and then created a 2nd (or more) time(s).
由于某种原因,这样做不起作用。我怀疑问题与ViewModel的生命周期有关,因为当我将ViewModel视为单例时,就不会发生这种情况。这只发生在ViewModel被销毁然后第二次(或更多次)创建时。

What I'm trying to achieve is that the screen containing the view model is aware of the user state. Meaning that when I navigate to the screen, I want it to collect the latest user state, and then decide which content to show.
我想要实现的是包含ViewModel的屏幕能够了解用户状态。这意味着当我导航到该屏幕时,我希望它能够收集最新的用户状态,然后决定要显示哪些内容。

I also realize this is not the best pattern, most likely. I'm currently looking into a solution that holds the User as part of the app state and collecting per screen (given that it basically changes all or many screens and functionalities) so if you have any resources on an example on such implementation I'd be thankful. But I can't get my head around why this current implementation doesn't work so any light shed on the situation is much appreciated.
我也意识到这可能不是最佳的模式。我目前正在研究一种将用户作为应用程序状态的一部分并根据每个屏幕进行收集的解决方案(鉴于它基本上会改变所有或许多屏幕和功能),所以如果您有关于这种实现的示例的资源,我将不胜感激。但我无法理解为什么当前的实现不起作用,所以对这种情况的任何启示都将不胜感激。

EDIT
This is what I have in mind for the repository
这是我对存储库的设想:

private val _user = MutableSharedFlow<User>()
override val user: Flow<User> = _user

override suspend fun login() {
    delay(2000)
    _user.emit(LoggedUser.aLoggedUser())
}

override suspend fun logout() {
    delay(2000)
    _user.emit(GuestUser)
}
英文:

This init block is in my ViewModel:

init {
        viewModelScope.launch {
            userRepository.login()
            userRepository.user.collect {
                _uiState.value = UiState.Success(it)
            }
        }
    }

This is very similar to what's actually written on the app, but even this simple example doesn't work. After userRepository.login(), user which is a SharedFlow emits a new user state. This latest value DOES get collected within this collect function shown above, but when emitting a new uiState containing the result, the view does not get such update.

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Doing this for some reason, does not work. I suspect the issue is related to the lifecycle of the viewmodel, because when I treat the viewmodel as a singleton, this doesn't happen. It happens only when the viewmodel gets destroyed and then created a 2nd (or more) time(s).

What I'm trying to achieve is that the screen containing the view model is aware of the user state. Meaning that when I navigate to the screen, I want it to collect the latest user state, and then decide which content to show.

I also realize this is not the best pattern, most likely. I'm currently looking into a solution that holds the User as part of the app state and collecting per screen (given that it basically changes all or many screens and functionalities) so if you have any resources on an example on such implementation I'd be thankful. But I can't get my head around why this current implementation doesn't work so any light shed on the situation is much appreciated.

EDIT
This is what I have in mind for the repository

    private val _user = MutableSharedFlow&lt;User&gt;()
    override val user: Flow&lt;User&gt; = _user

    override suspend fun login() {
        delay(2000)
        _user.emit(LoggedUser.aLoggedUser())
    }

    override suspend fun logout() {
        delay(2000)
        _user.emit(GuestUser)
    }

答案1

得分: 1

以下是翻译的部分:

ViewModel类:

sealed interface UserUiState {
    object NotLoggedIn : UserUiState
    object Error : UserUiState
    data class LoggedIn(val user: User) : UserUiState
}

class MyViewModel @Inject constructor(
    userRepository: UserRepository
) : ViewModel() {

    val userUiState = userRepository.login()
        .map { user ->
            if (user != null)
                UserUiState.LoggedIn(user)
            else
                UserUiState.Error
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = UserUiState.NotLoggedIn
        )
}

Repository类:

class UserRepository {
    fun login(): Flow<User?> = flow {
        val user = TODO("Your code to get user")
        if (isSuccess) {
            emit(user)
        } else {
            emit(null)
        }
    }
}

您的屏幕Composable:

@Composable
fun Screen() {
    val userUiState by viewModel.userUiState.collectAsStateWithLifecycle()
    when (userUiState) {
        is UserUiState.LoggedIn -> { TODO("Success code") }
        UserUiState.NotLoggedIn -> { TODO("Waiting for login code") }
        UserUiState.Error -> { TODO("Error display code") }
    }
}

它的工作原理: Repository中的login()返回授权用户流,可在ViewModel中使用。我使用UserUiState密封类来处理可能的用户状态。然后,我在map {}中将User值转换为UserUiState,以在UI层中显示它。然后,UserUiState的流需要转换为StateFlow,以从Composable函数中获取它,所以我使用了stateIn。当然,这将解决您的问题。

如果我理解错了或者代码不符合您的期望,请在评论中告诉我。

注意: 不建议在数据层中使用SharedFlow和StateFlow。

编辑: 如果您正在处理网络请求,可以这样发出流:

val user = flow {
    while (true) {
        // 网络调用以获取用户
        delay(2000)
    }
}

如果您使用Room,可以在您的DAO中执行以下操作:

@Query("获取实际用户查询")
fun getUser(): Flow<User>

这是更好的方法,也是Android开发者YouTube频道推荐的方法。

英文:

For your case better to use this pattern:

ViewModel class:

sealed interface UserUiState {
    object NotLoggedIn : UserUiState
    object Error : UserUiState
    data class LoggedIn(val user: User) : UserUiState
}

class MyViewModel @Inject constructor(
    userRepository: UserRepository
) : ViewModel() {

    val userUiState = userRepository.login()
        .map { user -&gt;
            if (user != null)
                UserUiState.LoggedIn(user)
            else
                UserUiState.Error
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = UserUiState.NotLoggedIn
        )
}

Repository class:

class UserRepository {
    fun login(): Flow&lt;User?&gt; = flow {
        val user = TODO(&quot;Your code to get user&quot;)
        if (isSuccess) {
            emit(user)
        } else {
            emit(null)
        }
    }
}

Your screen Composable:

@Composable
fun Screen() {
    val userUiState by viewModel.userUiState.collectAsStateWithLifecycle()
    when (userUiState) {
        is UserUiState.LoggedIn -&gt; { TODO(&quot;Success code&quot;) }
        UserUiState.NotLoggedIn -&gt; { TODO(&quot;Waiting for login code&quot;) }
        UserUiState.Error -&gt; { TODO(&quot;Error display code&quot;) }
    }
}

How it works: login() in repository returns autorized user flow which can be used in ViewModel. I use UserUiState sealed class to handle possible user states. And then I convert User value in map {} to UserUiState to display it in the UI Layer. Then Flow of UserUiState needs to be converted to StateFlow to obtain it from the Composable function, so I made stateIn.
And of course, this will solve your problem

Tell me in the comments if I got something wrong or if the code does not meet your expectations

Note: SharedFlow and StateFlow are not used in the Data Layer like you do.

EDIT:
You can emiting flow like this if you are working with network:

val user = flow of {
    while (true) {
        // network call to get user
        delay(2000)
    }
}

If you use Room you can do this in your dao.

@Query(TODO(&quot;get actual user query&quot;))
fun getUser(): Flow&lt;User&gt;

It is a better way and it recommended by android developers YouTube channel

huangapple
  • 本文由 发表于 2023年2月14日 09:08:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75442594.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定