If one was to model an unidirectional data-flow, w...
# coroutines
l
If one was to model an unidirectional data-flow, which primitive would you use to represent the events that the UI can create back to the Presenter/ViewModel? I’m currently using a
SendChannel
backed by a
RENDEZVOUS
implementation, but wondering if
MutableSharedFlow
(with
replay=0
) would be better (i assume due to less overhead?!)
r
I’ve been using
MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
and
.tryEmit()
to replace PublishSubjects for this use case.
l
Interesting, I didn’t think too much of
extraBufferCapacity
but the values you’re initializing the Flow makes total sense
r
It’s because you need to use
tryEmit()
when not in a suspend function. The event gets buffered until the suspended coroutine resumes.
a
Can the events happen concurrent with one another? How many producers are there? How many consumers?
r
The consumers are transient so there could be 0 or more of them. This would be used for hot producers for example, mouse moved events. The producer will produce regardless of the consumers. Unless I misunderstood the question.
a
I ask because multiple consumers of UI events can carry design implications that ripple throughout a system. While it can permit passive observation for things like magic autoconfiguration of analytics, it also means abandoning single source of truth/responsibility. It can mean that parts of your system are observing the downstream side effects of app data model changes on UI widgets when observing those app data model changes might be more appropriate. Or it can mean that multiple consumers are configured with tightly coupled assumptions about what other consumers are monitoring/will do.
it's often appropriate for UI events to be single consumer, but those single consumers of UI events apply changes to app state that can be observed by multiple consumers.
r
Right I guess it depends on if the event producer is hot or cold.
a
there's a correlation, but not necessarily a causation. You could think of button clicks or mouse movement as being hot because the physical action taken by the user happens regardless of whether software is listening to it, but the software machinery to generate events for a listener might be implemented as something cold that only does that work if something is listening. It's more about the conceptual responsibilities of producer/consumer and how you model source of truth in this case.
Hot vs. cold streams is an implementation detail of that modeling
r
There are at least two ways to model state. Using FRP and unidirectional data flow, you can model your UI state without have to have a single source of truth for the entire state of the UI. You can separate out state for each independent element on screen like a recycler view into a single observable. Or you could have transient elements like dialogs that can be shown when subscribed to and generate an event when they’re dismissed. If the element in the UI is transient then it’s events would get emitted via a cold observable. If the UI element is there for the entire lifecycle of the Activity, then it would hot.
Here is an example. Please look at the createUx() method here where the flow of getting ArCore installed and permissions is described in a flow https://github.com/zirman/arcore-filament-example-app/blob/master/app/src/main/java/com/example/app/aractivity/ArActivity.kt
Saying that hot vs. cold is an implementation detail implies that which one it is doesn’t matter. It does matter if the observable has effects.
l
Interesting discussion! my line of thinking was having a single presenter that would
collect
the primitive (either my current implementation -- a channel -- or a more optimised entity e.g.
MutableSharedFlow
), and expose this primitive to 0-to-many "viewtrees" (not exactly, but imagine I had a
ViewModel
by
activityViewModels()
and had different fragments sending UI events (e.g. button clicks) to the centralised processor/presenter).
r
Are these buttons transient? Why are there multiple listeners on the button clicks?
1
a
@Rob I think we're coming to similar/related conclusions by way of different paths 🙂
👍 1
@leandro it might be from not seeing the details from this thread, but multiple producer multiple consumer setups for UI events tend to pull you into a gravity well that ends in something analogous to an event bus
scope becomes difficult to reason about, individual events are both everyone and no one's problem, you end up chasing weird ordering bugs and race conditions around which observers are present when an event intended for a specific observer happens
l
I agree, apologies English isn’t my mother language, I didn’t mean multiple producer multiple consumer, but rather a single presenter that’s injected to mainly one screen, but could be passed to a master and a detail fragments. So something like:
Copy code
class Presenter {
  val events: ReceiveChannel<Event>
  
  suspend fun start() {
    events.consumeEach {
	  //
	}
  }
}
where each fragment would be like:
Copy code
class MyFragment : Fragment {
  @Inject presenter: Presenter

  fun onCreatedView() {
	view.findViewById(R.id.plus).setOnClickLister { 
      presenter.events.offer(Event.OnPlusClick)
	}
	
	view.findViewById(R.id.minus).setOnClickLister { 
      presenter.events.offer(Event.OnMinusClick)
	}	
  }
}
But oftentimes it’s a 1:1
r
This looks more or less like an event bus which has the issue of broadcasting events to every object, regardless of if it requires processing. I wold split up each button to emit to a separate MutableSharedFlow on the Fragment. And the Presenter would then consume those Flows instead of injecting the Channel into the Fragment.
You could automatically show/hide the fragment while it’s Flow is being consumed, but that is more advanced than I’m willing to go into here.