Я пишу приложение для 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.
Общая идея состоит в том, чтобы просто преобразовать потоки в модели представления, сбор должен выполняться только в пользовательском интерфейсе. Таким образом, пользовательский интерфейс может подписываться и отписываться от потоков по мере необходимости, что экономит ресурсы.
В вашем примере 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 так, как вы это сделали выше.
Я отредактировал свой ответ, чтобы указать, как другие источники данных могут быть интегрированы в поток. Базовая структура останется прежней.
Чтобы ответить на закрытые голоса: хотя вопрос требует лучших практик (которые могут быть субъективными), я вижу четкий консенсус, возникающий в отношении того, как следует обрабатывать потоки в чистой архитектуре, особенно с помощью Compose для пользовательского интерфейса. Поскольку официальная документация (пока) не настолько конкретна, и большая часть знаний распространяется разработчиками по различным сообщениям в блогах и примерам проектов, подобные вопросы вполне понятны и должны быть по теме.