SingleFlowEvent в MVI и Compose
есть вот такой пример кода
// Viewmodel
sealed interface Event {
object A : Event
object B : Event
}
private val _event = Channel<Event>(Channel.UNLIMITED)
val event = _event.receiveAsFlow()
sealed interface Intent {
class A : Intent
class B : Intent
}
fun sendIntent(intent: Intent) {
when (intent) {
is Intent.A -> viewModelScope.launch { _event.send(Event.A) }
is Intent.B -> viewModelScope.launch { _event.send(Event.B) }
}
}
// Compose UI
@Composable
fun UI(){
val vm = koinViewModel<TestVM.VM>()
val event = remember { vm.event }
Element1(event)
LaunchedEffect(Unit){
event.filterIsInstance<Event.A>().collect{
// ловим А
}
}
}
@Composable
fun Element1(
event : Flow<Event>
){
LaunchedEffect(Unit){
event.filterIsInstance<Event.B>().collect{
// ловим B
}
}
}
есть суть закллючается в том, что у нас есть 2 вида Event'а.
Event.A
никак не зависит от UI. обработка его события - это переход на другой экран, показ Toast'а и другие подобные события. он спокойно может размещаться в корневой функции UI()
а вот Event.B
- это ивент, который зависит от UI. например: подсветить какой-то текст красным на 2 секуду, а затем вернуть обычный цвет. или проскролить список к определенной позиции.
и он должен быть там, где может повлиять на цвета текста или обратиться к ListState
. должен быть внутри определенных функций.
и проблема такого кода заключается в том, что если у нас есть слушатель ивентов, который фильтрует их через filterIsInstance
, то он отлавливает их, независимо от того filterIsInstance
т.е. ивент считает сколлекченным уже на стадии фильтрации. а то, что он не прошел фильтр - не учитывается.
я вижу 1 выход, но он мне не нравится:
отказаться от sealed Event
и единого Channel<Event>
и для каждого ивента создавать свой собственный Channel<DataType>
подскажите, что делать в такой ситуации.
Ответы (2 шт):
Как вариант проверять есть ли события после фильтра и не вызывать collect
на пустом flow. Пример кода:
@Composable
fun UI(){
val vm = koinViewModel<TestVM.VM>()
val event = remember { vm.event }
Element1(event)
LaunchedEffect(Unit) {
event.filterIsInstance<Event.A>().apply {
if (count() > 0) {
collect {
// ловим А
}
}
}
}
}
@Composable
fun Element1(
event : Flow<Event>
){
LaunchedEffect(Unit){
event.filterIsInstance<Event.B>().apply {
if (count() > 0) {
collect {
// ловим B
}
}
}
}
}
вот такое решение, нашел для своей задачи:
private val _event = Channel<Event>(Channel.UNLIMITED)
val event = _event
.receiveAsFlow()
.shareIn(scope, SharingStarted.WhileSubscribed(), 0)
именно таким образом мы получаем:
- только новые значения. при перевороте экрана старые(текущее) не коллектиться
- если мы поверх одного фрагмента, откроем другой, а в первый прилетят ивенты, то когда мы вернемся на первый фрагмент, то сможем сколлектить их все