Can someone help clarify the best practices on usi...
# compose
c
Can someone help clarify the best practices on using hot flows within Compose? Almost everything I’ve found seems to specify recommendations for cold flows explicitly. This article describes the use of
flowWithLifecycle
, which looks like it was introduced in a newer version of lifecycle than I have installed. Is
flowWithLifecycle
the best way or can/should one still use
shareIn
or
stateIn
with a hot flow to control the collection behavior in a lifecycle-aware way?
a
If you have a hot flow then by definition the work backing it is always still happening regardless. Compose UI itself is already lifecycle-aware and won't perform recomposition if the UI isn't visible.
what kind of hot flow are you working with?
c
I’m using SharedFlow to make incoming sensor data available throughout the app
a
are you using a cold flow upstream of that then?
c
i’m not
a
then what scopes the observation of sensor data that feeds it?
c
The activity opens a connection to an FTDI device that’s streaming data and managed in a separate thread, it lives as long as the app
a
ok, well, first and foremost that sounds like a bad idea for system health, you should probably scope that connection itself in such a way to be lifecycle aware. 🙂
c
Do you think that’s necessary if this is the only app running on the tablet?
I’m not coming from a typical Android background so the best practices around lifecycles have been the trickiest thing for me to really digest so far 🙃
a
no worries 🙂
when you say, "this is the only app running on the tablet" do you mean this is a screen always on, always plugged into wall power kiosk device or something?
c
that’s right
a
then you could get away with it but once you add in all of the robustness handling for the device connection itself, doing this part "right" is so trivial that you might as well.
anyway, usual recommendation:
c
That sounds great! Could you point me towards the right resource for that?
a
any of the docs around the connection APIs you're using, plus the
flow {}
,
channelFlow {}
and
callbackFlow {}
APIs from kotlinx.coroutines; none of them are compose-specific
Consuming hot flows (or any other hot observable) doesn't have much documentation because there isn't really anything special you need to do. Lifecycle doesn't matter for hot flows since you've thrown lifecycle out the window further upstream by choosing a hot flow to begin with. 🙃
c
Great, that makes sense. So when people are concerned about flows and lifecycles, they’re specifically focused on the fact that the act of calling
collect
makes it come to life, which, like you said… I’m already nope-ing out of.
a
It starts to matter when you've used
.stateIn
with
WhileSubscribed
and the like, since then the flow collection is scoped to the union of all current subscribers, at which point you want the subscribers to be lifecycle-aware.
c
ahh
This is helpful, it’s how I was interpreting it but I was concerned that I was missing some critical piece due to not knowing enough about lifecycles
👍 1
a
a bit of caution, it's usually cheaper to think through all of this producer/consumer subscription scoping "right"/precisely from the get go and test it accordingly, since if your requirements change in the future to need it, (and they often do,) you won't have a mountain of work and surprises spread out across a large codebase
c
and that goes back to what you were saying before, scoping the hardware sensor connection handling appropriately?
a
yeah, that too
chances are you're going to have to handle cases of random disconnects and reconnections just to be robust enough to operate in the wild anyway, and the same considerations you'll need to employ there will be the same things needed to stop/restart a cold data source
c
yup, i’ve got quite a bit of that sort of thing in place
a
prior to coroutines and Flow asking someone to think through all of these considerations that may or may not strictly pay off later was a big up front thing to ask, but coroutines and flow make it so straightforward that the cost/benefit analysis changes quite a bit and it makes the code more maintainable even for simpler cases.
c
as evidenced by new Android dev (me) building our kiosk app with serial USB connections + bluetooth connection + compose UI in < 12 months 😂
a
😄
c
though i worry about the Android-specific things that i’m missing by not having the full picture of the Android ecosystem
so you said that scoping the connection the right way should be pretty trivial once i’ve got the rest of the connection handling in place… can you suggest anything to read so i can set that up?
a
c
right now, i don’t go much further than ensuring the connections are released when the activity ends
ah i see
so you’d suggest this at the source instead of a SharedFlow?
a
yes, I think you'll end up with something more robust and easy to test
what's the nature of the data coming at you over the wire? is it incremental events where you need to receive a full and complete stream, or is it full state representations with each emit?
c
i see. this will then necessitate some use of
shareIn
down in the UI since the providers will be cold, right?
the most important one is a constant stream of data at approx 300hz
oh but that doesn’t answer your question…
they’re incremental events so we need the complete stream but given the high rate, we prioritize the most recent messages. each message is a full state representation of the data captured by the firmware attached to the sensors
but they’re incremental in the sense that they tell a story of what happened here in the real world
a
what I'm getting at is, if you have events that are getting cooked into state by some reducer-like step that then gets consumed by the rest of the app, don't share the event stream, share the resulting state.
shareIn
is only useful when it's ok for any given consumer to miss events because it arrived late, or unsubscribed and resubscribed for some reason.
shareIn
represents a diffusion of responsibility for consumers and people tend to tie themselves in knots trying to fix the problems they created by adding that diffusion of responsibility.
Stay single producer/single consumer as often as possible and only share out to multiple consumers after the complicated parts have been handled/processed.
c
Right, that makes sense. We’re already doing some massaging of the data close to where it’s received, the consumers get immediately-usable pieces of information instead of the truly raw stream.
👍 1
But just so I understand, moving from
SharedFlow
to
channelFlow
would switch us from a hot flow to a cold flow, so that changes the lifecycle equation for all consumers, right?
a
it means there's a motive for consumers to be less sloppy about lifecycle, but if you change nothing then you're no worse off than you are today with a hot source; that's what it degenerates to in the worst case
c
I think this is starting to click
🎉 1
And being “less sloppy” — that would involve the consumer being aware of its own lifecycle and stopping/starting collection appropriately?
Rather than just shrugging it off and letting data pour out all the time?
a
yeah, and that means things like
.flowWithLifecycle
or
repeatOnLifecycle
blocks for collectors
and then my usual spiel about not using
.collectAsState()
for event streams, only for actual state 🙂
c
ok, this really is starting to make sense
🙌 1
and “for actual state” — that includes (or maybe that is) state in a composable function that can then be watched by a side effect of some sort?
a
by "actual state" I mean declarations of fact, things that you can act on in an idempotent manner
c
right
ok
a
absolute positions, not deltas
c
right right
that does describe how i’ve been using it, doesn’t sound like i’ve been botching that 😂
👍 1
Oh so then one last question: when would you recommend one use
SharedFlow
or
StateFlow
if not in a case like this?
a
via
shareIn
or
stateIn
, at points of last-mile info distribution of data originating from cold flows, especially when combined with
WhileSubscribed
.
MutableSharedFlow
is useful for FYI types of events where it truly doesn't matter if no one was listening and things get dropped.
MutableStateFlow
I use almost never when compose's
mutableStateOf
is available instead, but some data access patterns can make it the better choice. (For example, compose's Recomposer exposes its current operational state as a StateFlow and it wouldn't benefit in the slightest from using snapshot state instead; there would be a number of drawbacks in terms of immediacy of change dispatch among other things.)
c
very interesting
I really appreciate all your help and information here
So to summarize my big takeaways: • I should prefer
channelFlow
over a hot
SharedFlow
for incoming sensor messages because it forces consumers to play an active role (or at least acknowledge) in their lifecycle handling. Consumers should use
flowWithLifecycle
or
repeatOnLifecycle
. • If for some reason my composable function is consuming a hot flow, it’s not necessary to add any additional lifecycle handling since I’m skipping around that by using a hot flow in the first place. The composable function will stop consuming the flow when it goes out of scope.
👍🏽 1
a
yep
c
Great!
Thank you for the help!
👍 1