Hey all, I've got a bit of a code design problem, ...
# compose
t
Hey all, I've got a bit of a code design problem, to do with holding observable state in a ViewModel, to be consumed by Composable functions..
For this particular screen, I have a list of 'exercises'. Each exercise is associated with an 'exercise progress'. I want to expose the exercise progress for each exercise in an observable fashion, so I'm thinking I should do something like:
Copy code
val exerciseProgressMap: LinkedHashMap<Exercise, MutableStateFlow<ExerciseProgress>> = linkedMapOf()
The map is initially empty, because the exercises aren't initially available to the ViewModel
You can't observe the progress of an exercise that doesn't exist. So any interested observers have to wait until this map is populated, before subscribing to the StateFlow<ExerciseProgress>
This seems problematic, as I won't be able to trigger a recompose automatically when the map is populated
I've considered holding the progress state like so:
Copy code
val exerciseProgressMap: MutableStateFlow<LinkedHashMap<Exercise, ExerciseProgress>> = MutableStateFlow(linkedMapOf)
But now I need to emit a whole new map of Exercise and ExerciseProgress, each time the progress changes
d
Without seeing how this reactive data is being consumed; it sounds as though you want to do both:
MutableStateFlow<LinkedHashMap<Exercise, MutableStateFlow<ExerciseProgress>>>
The outer flow emits when the Exercises change. Inner flows emit when an individual Exercises progress changes.
t
This seems problematic - I can imagine a case where the outer flow emits, and now any observers of the previous inner flow are observing stale data
d
Typical way to consume this type of construct would be through a function that takes an
Exercise
of interest; and `flatMapLatest`s on
exerciseProgressMap
to maintain awareness of that particular progress. E.g:
Copy code
fun exerciseProgress(exercise: Exercise) : StateFlow<ExerciseProgress?> = exerciseProgressMapFlow.flatMapLatest { progressByExercise -> progressByExercise[exercise] ?: flowOf(null) }
By accessing exercise progress via this function; if an exercise gets dropped from the map, then the value of progress will get
nulled
for observers too; i.e. preventing them seeing stale data.
Of course, you could imagine something a bit more explicit than
nulls
based on sealed classes; but this illustrates the concept.
t
I don't understand the advantage of (or difference between)
MutableStateFlow<LinkedHashMap<Exercise, MutableStateFlow<ExerciseProgress>>>
vs  
MutableStateFlow<LinkedHashMap<Exercise, ExerciseProgress>>
d
There may not be one depending on your use case; but if you have downstream logic or UI bindings that only care about the progress of one particular exercise; then having nested streams in this way could represent a significant optimisation.
Your second line could mean many places in the App responding to a change in progress for a single exercise.
t
I see, thanks
d
By flat-mapping out a
Flow
of single exercise change; you limit the impact on other areas of the App in a minimal and consistent way.
👍 1
Whether this is the best overall solution depends on how your data is being prepared upstream.
t
This entire approach feels a little over-engineered (mine, not yours). But I'm not sure if there's an alternative. I think that using MutableStateFlow to observe changes to a single object is fine, but a map of objects to MutableStateFlow feels very unwieldy.
d
It does 'feel' a bit that way, yes, doesn't necesarily mean it's wrong depending on the inputs you're dealing with.
t
I'll have to keep working with it and see how it sits. Thanks fellow Melbournian!
👍 1
d
Hah, didn't realise.. ding ding! 🚋
That's it, sometimes you need to massage things around a bit, good learning though, enjoy 🙂
a
you could also try
Copy code
val exerciseProgressMap = mutableStateMapOf<Exercise, ExerciseProgress>()
👍 2
which is observable via compose's snapshot system; just make changes to it as needed and any composable functions that read it will recompose when changes happen
reads don't need to be direct either, if a composable function calls any other function that reads that map, the composable function will still know to recompose when the underlying map changes
t
This seems like a simpler equivalent to
Copy code
val exerciseProgressMap: MutableStateFlow<Map<Exercise, ExerciseProgress>>
Right?
a
kind of. More of the indirection is handled for you, it just behaves like a normal mutable map
t
Thanks, I'll try it out
👍 1
z
Another option would be using the kotlinx immutable collections library’s persistent map type with a state flow. It would look like you’d be creating a new map for every emission, but the data structures are designed to shared most of their storage so it’s much more efficient than copying a hash map every time.
👍 1
a
that's exactly how
mutableStateMapOf
is implemented 😄
🙈 1
👍 1