Laurence Muller
01/07/2022, 5:26 PMstore
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
// 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()
}
}
// 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)
}
...
}
// 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
@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?Arkadii Ivanov
01/07/2022, 6:29 PMFlow
or an Observable
from your component and collect/subscribe it in your Composable function. Would this work for you?Arkadii Ivanov
01/07/2022, 6:32 PMCoroutineScope
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&cid=C01403U1ZGWLaurence Muller
01/07/2022, 6:34 PMLaurence Muller
01/07/2022, 6:35 PMLaurence Muller
01/07/2022, 7:52 PM// Store; define possible labels
sealed class Label {
data class Notice(val data: String) : Label()
object None : Label()
}
// In the Executor of the StoreFactory, trigger the labels like this when needed:
scope.launch {
publish(Label.Notice(data))
delay(5000)
publish(Label.None)
}
// 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.
// 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.Arkadii Ivanov
01/07/2022, 8:01 PMLaurence Muller
01/07/2022, 8:03 PMArkadii Ivanov
01/07/2022, 8:16 PMArkadii Ivanov
01/07/2022, 8:17 PMArkadii Ivanov
01/07/2022, 8:19 PMLaurence Muller
01/07/2022, 8:27 PMLaurence Muller
01/07/2022, 8:28 PMArkadii Ivanov
01/07/2022, 8:30 PMNikola Milovic
01/08/2022, 9:47 AMUnidirectional 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
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?Arkadii Ivanov
01/08/2022, 11:53 AMshow
.