Выдача состояния пользовательского интерфейса во время сбора не обновляет пользовательский интерфейс

Этот блок инициализации находится в моем ViewModel:

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

Это очень похоже на то, что на самом деле написано в приложении, но даже этот простой пример не работает. После userRepository.login()user, который является SharedFlow, создает новое пользовательское состояние. Это последнее значение ДЕЙСТВИТЕЛЬНО собирается в этой функции сбора, показанной выше, но при создании нового uiState, содержащего результат, представление не получает такого обновления.

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Сделать это почему-то не получается. Я подозреваю, что проблема связана с жизненным циклом модели представления, потому что, когда я рассматриваю модель представления как синглтон, этого не происходит. Это происходит только тогда, когда модель представления уничтожается, а затем создается второй (или более) раз.

Я пытаюсь добиться того, чтобы экран, содержащий модель представления, знал о состоянии пользователя. Это означает, что когда я перехожу к экрану, я хочу, чтобы он собирал последнее состояние пользователя, а затем решал, какой контент показывать.

Я также понимаю, что это не лучший образец, скорее всего. В настоящее время я изучаю решение, которое удерживает пользователя как часть состояния приложения и собирает данные для каждого экрана (учитывая, что оно в основном меняет все или многие экраны и функции), поэтому, если у вас есть какие-либо ресурсы для примера такой реализации, я бы быть благодарным. Но я не могу понять, почему эта текущая реализация не работает, поэтому любой свет, пролитый на ситуацию, очень ценится.

РЕДАКТИРОВАТЬ Это то, что я имею в виду для репозитория

    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)
    }
1
0
67
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
        )
}

Класс репозитория:

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

Ваш экран

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

Как это работает: login() в репозитории возвращает авторизованный пользовательский поток, который можно использовать в ViewModel. Я использую закрытый класс UserUiState для обработки возможных состояний пользователя. Затем я конвертирую значение пользователя на карте {} в UserUiState, чтобы отобразить его на уровне пользовательского интерфейса. Затем поток UserUiState нужно преобразовать в StateFlow, чтобы получить его из функции Composable, поэтому я сделал stateIn. И, конечно же, это решит вашу проблему.

Сообщите мне в комментариях, если я что-то напутал или код не соответствует вашим ожиданиям.

Примечание. SharedFlow и StateFlow не используются на уровне данных, как вы.

Обновлено: Вы можете испускать поток следующим образом, если вы работаете с сетью:

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

Если вы используете Room, вы можете сделать это в своем дао.

@Query(TODO("get actual user query"))
fun getUser(): Flow<User>

Это лучший способ, рекомендованный разработчиками Android на канале YouTube.

Большое спасибо за Вашу помощь! Это определенно полезно, но я пытаюсь изменить это, чтобы оно соответствовало моим идеям. Я предполагаю, что я пытаюсь получить Flow<User>, который излучает текущее состояние. Это означает, что при входе в систему он создает пользователя, а при выходе из системы он создает гостевого пользователя (или null). Таким образом, например, экран в NavHost может выполнять login(), а элемент в нижней панели навигации может собирать новое состояние через репозиторий Flow<User>. Имеет ли это смысл?

Lheonair 14.02.2023 23:36

Вы можете найти то, о чем я говорю, в разделе редактирования поста.

Lheonair 14.02.2023 23:43

Хотя это выглядит очень похоже на то, что я думаю с точки зрения LiveData

Lheonair 14.02.2023 23:46

Холодный поток @Lheonair нельзя изменить. Так что да, вам нужно использовать LiveData для этого

Injent 15.02.2023 01:37

@Lheonair отредактировал мой ответ, возможно, это решение подойдет вам. Это рекомендовано каналом Android Developers на YouTube.

Injent 15.02.2023 01:56

Другие вопросы по теме