Как правильно использовать StateFlow для обновления UIState экрана
Попалась мне вот такая статья
https://proandroiddev.com/loading-initial-data-in-launchedeffect-vs-viewmodel-f1747c20ce62
в которой автор описывая преймущества и недостатки подходов инициализации данных приходит к тому, что лучше всего делать это через StateFlow
. Я решил попросбовать сделать это, при условии того, что я использую UiState
обертку как общий стейт для экрана:
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Error(val code: Int? = null, val message: String? = null) : UiState<Nothing>()
data class Content<T>(val data: T) : UiState<T>()
}
то есть моя имплементация до изменений:
class MyRepo {
fun getMyData(): Flow<List<String>> {
return flow {
delay(1000)
emit(listOf("1", "2"))
}
}
}
class MyViewModel1(repo: MyRepo): ViewModel() {
data class ScreenStateUI(
val data: List<String> = emptyList(),
val title: String = "Title"
)
private val _screenUiState: MutableStateFlow<UiState<ScreenStateUI>> = MutableStateFlow(UiState.Loading)
val screenUiState: StateFlow<UiState<ScreenStateUI>> = _screenUiState.asStateFlow()
init {
viewModelScope.launch {
repo.getMyData()
.map<List<String>, UiState<ScreenStateUI>> { UiState.Content(ScreenStateUI(data = it)) }
.collectLatest {
if (it is UiState.Content) {
_screenUiState.emit(
UiState.Content(data = it)
)
}
}
}
fun updateTitle(title: String) {
_screenUiState.update {
if (it is UiState.Content) {
it.copy(data = it.data.copy(title = title))
} else {
it
}
}
}
}
При инициализации подгружается стейт и UI может подписаться на изменения через screenUiState
, то есть если нужно использовать fun updateTitle(title: String)
то нет никаких проблем.
Теперь я поменял имплемантацию, чтоб избавиться от инициализиции в init
блоке и сделать все как только UI подписывается на ивент:
class MyRepo {
fun getMyData(): Flow<List<String>> {
return flow {
delay(1000)
emit(listOf("1", "2"))
}
}
}
class MyViewModel2(repo: MyRepo): ViewModel() {
data class ScreenStateUI(
val data: List<String> = emptyList(),
val title: String = "Title"
)
val screenUiState: StateFlow<UiState<ScreenStateUI>> by lazy {
repo.getMyData()
.map<List<String>, UiState<ScreenStateUI>> { UiState.Content(ScreenStateUI(data = it)) }
.onStart { emit(UiState.Loading) }
.catch { emit(UiState.Error(message = it.message)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UiState.Loading
)
}
fun updateTitle(title: String) {
...
}
}
то есть вроде все выглядит не плохо и действительно теперь UI запустит запросы только когда подпишется, но теперь стейт нельзя изменить, и соответственно метод fun updateTitle(title: String)
не может обновить title
.
Что я пропускаю здесь?
Ответы (2 шт):
вы не можете обновить стейт, т.к. делаете преобразование stateIn
+ выбрали стратегию SharingStarted.WhileSubscribed
ваш первый вариант, является более корректным, т.к. стейт доступен для изменения внутри ВьюМодели.
в вашем примере, у вас есть стейт и вы слушаете какой-то флоу из репозитоиря.
а что если вам придется слушать 2 флоу из 2ух разных репозиториев? пример 2 уже начнет превращаться в большую конструкцию с подводными камнями.
да и я бы сказал что идея "ВьюМодель выполняет запросы, только когда на нее подписаны" слегка противоречит основной идеи ВьюМодели "жить независимо от ЖЦ экрана"
мне до конца не ясно "что именно вы хотите изменить в своем коде (пример 1) и для чего?"
в итоге вот такое решение получилось
class MyViewModel2(repo: MyRepo) : ViewModel() {
private val titleFlow = MutableStateFlow("Title")
data class ScreenStateUI(
val data: List<String>,
val title: String,
)
init {
viewModelScope.launch {
delay(2000)
updateTitle(title = "New title")
}
}
val screenUiState: StateFlow<UiState<ScreenStateUI>> by lazy {
combine(
repo.getMyData(),
titleFlow,
) { data, title ->
Log.e("HERE", "combine: $data :: $title")
UiState.Content<ScreenStateUI>(ScreenStateUI(data = data, title = title))
}
.onStart<UiState<ScreenStateUI>> { emit(UiState.Loading) }
.catch<UiState<ScreenStateUI>> { emit(UiState.Error(message = it.message)) }
.stateIn<UiState<ScreenStateUI>>(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UiState.Loading,
)
}
fun updateTitle(title: String) {
titleFlow.value = title
}
}
Исполузуя `combine` можно сделать, то что нужно.