Hello, I am trying to implement a UX where somethi...
# mvikotlin
l
Hello, I am trying to implement a UX where something in the
store
is triggering an alert (once) that the UI is suppose to react up on. Similar to maybe a classic android toast/snackbar message or maybe a 'no internet' message that shows up for a few seconds before fading away. Currently I have something that looks like: Shared module
Copy code
// Store Interface
internal interface ScreenAStore : Store<Intent, State, Label> {
    sealed class Intent {
        data class xxx(val xxx: String) : Intent()
    }

    data class State(
        val xxx: String = ""
    )

    sealed class Label {
        object ShowAlert : Label()
    }
}
Copy code
// Store Factory
internal class ScreenAStoreFactory(
    private val storeFactory: StoreFactory,
    private val appRepository: AppRepository
) {
    fun provide(): ScreenAStore =
        object : ScreenAStore, Store<Intent, State, Label> by storeFactory.create(
            name = "ScreenAStore",
            initialState = State(),
            bootstrapper = SimpleBootstrapper(Unit),
            executorFactory = ::ExecutorImpl,
            reducer = ReducerImpl
        ) {}

    private inner class ExecutorImpl : CoroutineExecutor<Intent, Unit, State, Msg, Label>() {
        ...
        // In the executor 
        private fun setAlert() {
            publish(Label.ShowAlert)
        }
        ...
    }
Copy code
// Component
class ScreenAComponent(
    ...
) : ScreenA, ComponentContext by componentContext {
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main)

    override fun onBackPressed(): Boolean {
        scope.cancel()
        return true
    }

    private val store =
        instanceKeeper.getStore {
            ScreenAStoreFactory(
                storeFactory = storeFactory,
                appRepository = appRepository
            ).provide()
        }


    init {
        backPressedHandler.register(::onBackPressed)
        scope.launch {
            store.labels.collect { label ->
                when (label) {
                    is Label.ShowAlert -> {
                        Napier.d("Show message similar to a Toast in Compose UI")
                    }
                }
            }
        }
    }

    override val model: Value<Model> = store.asValue().map(stateToModel)
}
Android app
Copy code
@Composable
fun ScreenAUi(component: ScreenAComponent) {
    Scaffold(
        modifier = Modifier
            .fillMaxSize(),
        topBar = {
            ScreenAHeader(component)
        }
    ) {
        ScreenABody(component, it)
    }
}

@Composable
private fun ScreenABody(component: ScreenAComponent, innerPadding: PaddingValues) {
    val model by component.model.subscribeAsState()

    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(innerPadding)
    ) {
        // Display some stuff
    }
}
What's the recommended way to consume this one time
label
event?
a
Hey. You can expose a
Flow
or an
Observable
from your component and collect/subscribe it in your Composable function. Would this work for you?
Also I noticed that you don't properly close the
CoroutineScope
in your component. You should better write
lifecycle.doOnDestroy { scope.cancel() }
, no need to cancel in onBackPressed in this case. You can use the following approach: https://kotlinlang.slack.com/archives/C01403U1ZGW/p1631726201021700?thread_ts=1631725037.021600&amp;cid=C01403U1ZGW
👍 1
l
Ah I will give the exposing a Flow a try and collect it in the composable.
K 1
Will also update canceling the scope
Taking in your response and an earlier response from you here Inspired by https://kotlinlang.slack.com/archives/C01403U1ZGW/p1638973977042900?thread_ts=1638891839.042000&amp;cid=C01403U1ZGW I think I got it working this way:
Copy code
// Store; define possible labels

sealed class Label {
    data class Notice(val data: String) : Label()
    object None : Label()
}
Copy code
// In the Executor of the StoreFactory, trigger the labels like this when needed:

scope.launch {
    publish(Label.Notice(data))
    delay(5000)
    publish(Label.None)
}
Copy code
// Component

interface Some {
    val events: Flow<Event>
    sealed interface Event {
        data class Notice(val data: String) : Event
        object None : Event
    }
}

class ScreenAComponent : Some {
    val store = ...

    private val labelToComponentEvent: (Label) -> Event = {
        when (it) {
            is Label.Notice -> Event.Notice(date = it.date)
            is Label.None -> Event.None
        }
    }

    override val events: Flow<Event> = store.labels.map(labelToComponentEvent)
}
The benefit of proxying the event like this is that we don't need a scope in the component and thus I was able to remove the scope stuff entirely.
Copy code
// Android

// We can use a box to pile a components...
@Composable
fun ScreenAUi(component: SearchDateSelection) {

    Box( modifier = Modifier.fillMaxSize()) {
        Scaffold(
            modifier = Modifier.fillMaxSize(),
            topBar = {
                ScreenAHeader(component)
            }
        ) {
            ScreenABody(component, it)
        }
        FlowComponent(component.events)
    }
}

// Collect the flow as a state to determine if we should show our custom toast/overlay/message with some in/out fading
@Composable
fun FlowComponent(flow: Flow<Event>) {
    val events by flow.collectAsState(initial = Event.None)
    val (showNotice, message) = when (events) {
        is Event.Notice -> {
            Pair(true, "Hello World!") // Could've used the data from the Notice event.
        }
        else -> {
            Pair(false, "")
        }
    }

    AnimatedVisibility(visible = showNotice,
        enter = fadeIn(),
        exit = fadeOut()) {
        Text(text = message)
    }
}
☝️ seems to work for my use case, maybe it helps others too.
a
Yeah, this should also work. 👍
l
For my current needs the labels will just be used to display things like "oh you forgot to fill in thing x in the form" or "oops no internet". Its still a bit hacky to have a reset label being published from the store. I think it's probably possible to use a Compose Animation and have it fade out after a few seconds but still working my way through the learning curve of Compose ui 😅
a
Actually in this particular case I would just have a flag in the state. And raise/dismiss it from the Executor.
Or just use Labels plus Toast for simplicity. Or Labels plus Shankar
If you go with a flag in the state, then you don't need Labels and events at all. Just map the state to model as usual.
👍 1
l
I actually need a custom toast view (which seems to be deprecate). I think I could just use state like you suggested and keep it simple
👍 1
thanks for your help and time 🙏
a
You're welcome
n
I was facing a similar issue. As google highly emphasises,
Unidirectional data flow
which would render these kind of usecases where the Store notifies the UI of something "incorrect". Everything should be handled with state, so you should basically do something like
Copy code
onSuccess = {
   setState(state.copy(showSuccessPopup = true))
   delay(X)
   setState(state.copy(showSuccessPopup = false))
}
But this just looks hacky? Why should the Store (or source of truth) handle the UX side of things? This really seems like a good exception to the Unidirectional flow where you fire up single lived events for these kind of "notifications". Or am I missing something?
a
There are always pros and cons. Have a flag in the state adds more noise to the store, but makes this logic testable (if needed) and makes the UI stateless. However (assuming we need custom UI for alerts) I would try to create a reusable Composable widget with a similar to Snackbar API. So there would a remembered object with a method
show
.