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 шт):

Автор решения: Eugene Krivenja

Как вариант проверять есть ли события после фильтра и не вызывать 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
                }
            }
        }
    }
}
→ Ссылка
Автор решения: Wlad

вот такое решение, нашел для своей задачи:

 private val _event = Channel<Event>(Channel.UNLIMITED)

 val event = _event
   .receiveAsFlow()
   .shareIn(scope, SharingStarted.WhileSubscribed(), 0)

именно таким образом мы получаем:

  • только новые значения. при перевороте экрана старые(текущее) не коллектиться
  • если мы поверх одного фрагмента, откроем другой, а в первый прилетят ивенты, то когда мы вернемся на первый фрагмент, то сможем сколлектить их все
→ Ссылка