Сбор потоков во ViewModel

Я пишу приложение для Android с базой данных Compose и Room. У меня есть работающее решение, но я не уверен, использую ли я лучшие практики. Я собираю два потока в инициализации ViewModel, чтобы создать состояние пользовательского интерфейса, которое используется в компонуемой ViewModel:

class MyViewModel(
    savedStateHandle: SavedStateHandle,
    context: Context
) : ViewModel() {

    // Some code omitted

    var uiState by mutableStateOf(MyUiState())

    init {
        viewModelScope.launch {
            combine(
                myRepo.getMovie(movieId).filterNotNull(),
                myRepo.getActors(movieId)
            ) { movie, actors ->
                uiState.copy(
                    movie = movie,
                    actorList = actors
                )
            }.collect { newState ->
                uiState = newState
            }
        }
    }
}

Из того, что я исследовал, вызов Collect() в init может быть сомнительным, но я изо всех сил пытаюсь найти какую-либо документацию, говорящую, что не делайте этого. И я не знаю другого способа автоматического обновления uiState при каждом добавлении или удалении актера без вызова метода Collect(). Будем очень признательны за любые советы по лучшему решению обновления uiState.

Чтобы ответить на закрытые голоса: хотя вопрос требует лучших практик (которые могут быть субъективными), я вижу четкий консенсус, возникающий в отношении того, как следует обрабатывать потоки в чистой архитектуре, особенно с помощью Compose для пользовательского интерфейса. Поскольку официальная документация (пока) не настолько конкретна, и большая часть знаний распространяется разработчиками по различным сообщениям в блогах и примерам проектов, подобные вопросы вполне понятны и должны быть по теме.

Leviathan 05.04.2024 21:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
978
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Общая идея состоит в том, чтобы просто преобразовать потоки в модели представления, сбор должен выполняться только в пользовательском интерфейсе. Таким образом, пользовательский интерфейс может подписываться и отписываться от потоков по мере необходимости, что экономит ресурсы.

В вашем примере uiState должен быть поток, а именно StateFlow:

val uiState: StateFlow<MyUiState> = transformedFlow(movieId)
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = MyUiState(),
    )

Kotlin StateFlow концептуально похож на Compose State тем, что представляет одно значение, изменения которого можно наблюдать. В ваших составных объектах вы можете получить uiState следующим образом:

val uiState: MyUiState by viewModel.uiState.collectAsStateWithLifecycle()

Для этого вам нужно добавить зависимость androidx.lifecycle:lifecycle-runtime-compose к вашим файлам Gradle. Дополнительную информацию см. https://medium.com/androiddevelopers/sumption-flows-safely-in-jetpack-compose-cde014d0d5a3.

transformedFlow будет объединенным потоком из вашего репозитория:

private fun transformedFlow(movieId: Int) = combine(
    myRepo.getMovie(movieId).filterNotNull(),
    myRepo.getActors(movieId),
) { movie, actors ->
    MyUiState(
        movie = movie,
        actorList = actors,
    )
}

В ответ на комментарий: все, от чего зависит ваш uiState, должно быть предоставлено как поток и объединено с другими потоками. Таким образом, всякий раз, когда что-либо меняется, полученный StateFlow всегда будет актуальным.

В качестве примера того, как это можно сделать, предположим, что ваш MovieId может меняться со временем. Как и сейчас, transformedFlow(movieId) вызывается только один раз при инициализации модели представления. Изменения в movieId не приведут к повторному выполнению getMovie или getActors. Теперь рассмотрите возможность добавления этого в вашу модель представления:

private val selectedMovieId: MutableStateFlow<Int?> = MutableStateFlow(null)

fun selectMovie(movieId: Int) {
    selectedMovieId.value = movieId
}

Теперь идентификатор фильма сам по себе является потоком, и его можно изменить, вызвав selectMovie. Тогда ваш uiState может основываться на этом потоке следующим образом:

@OptIn(ExperimentalCoroutinesApi::class)
val uiState: StateFlow<MyUiState> = selectedMovieId
    .flatMapLatest(::uiStateFlowOf)
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = MyUiState(),
    )

private fun uiStateFlowOf(movieId: Int?): Flow<MyUiState> =
    if (movieId == null)
        flowOf(MyUiState())
    else
        combine(
            myRepo.getMovie(movieId).filterNotNull(),
            myRepo.getActors(movieId),
        ) { movie, actors ->
            MyUiState(
                movie = movie,
                actorList = actors,
            )
        }

Если есть другие источники данных, их потоки необходимо объединить с другими потоками. Если они зависят от идентификатора фильма, просто добавьте их к существующему combine, если они чем-то другим, полностью объедините их с результирующим потоком. stateIn всегда идет последним, объединение необходимо выполнить заранее. Обязательно вынесите логику преобразования в отдельные функции, иначе будет очень сложно понять, что происходит. В конце концов, вся логика трансформации коренится в объявлении uiState и носит функциональный, а не императивный характер. Там можно было бы разместить всё в одном монолитном заявлении. Не делайте этого, вместо этого выделите независимую логику в отдельные функции.

Если ваши источники данных не предоставляют потоки, их, вероятно, можно легко обернуть потоками: см. https://developer.android.com/kotlin/flow, особенно раздел о callbackFlow. Если вы все еще где-то используете LiveData, есть удобный asFlow() метод для преобразования их в потоки.

Я понимаю, как бы это работало, если бы свойствами MyUiState были только Movie и ActorList. Но у меня есть и другие свойства, не связанные с потоками. Поэтому мне нужно иметь возможность их изменять, но StateFlow не является изменяемым. Я попытался сделать uiState MutableStateFlow, но тогда я не могу инициализировать uiState так, как вы это сделали выше.

tronman 05.04.2024 20:05

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

Leviathan 05.04.2024 21:05

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