Loading Initial Data properly with MVI
I previously wrote an article on implementing MVI with Jetpack Compose (you can find it here). In that article, I loaded the initial data in the init
block of the ViewModel. You’re more than likely curious as to what the correct way to load initial data is and funnily enough, the init
block isn’t it… 😅
I wasn’t very satisfied with doing this but at the time, I considered it more important to publish the article and that this issue was quite minor compared to the overall contents of the article (which it was).
The first helping hand
However, two months later, an article popped in my feed which dived into, you guessed it, best practices for loading initial data. I thank Jaewoong Eum for this article as it really helped me understand the strengths and weaknesses of both major approaches.
The two approaches are:
- A
LaunchedEffect
in the screen that calls a ViewModel function - The ViewModel
init
block
I read through the article and the explanations and slowly understood the intrinsic behaviour of each approach. It helped me understand why some developers prefer one solution over the other but overall, I needed a bit more context on how the proposed solution helps but also how it should be implemented. The article gives some code to guide readers but didn’t give me enough to fully grasp how it should be done, so I set the whole thing aside for some time.
The second helping hand
Time went by and other things piled up on my never-ending pile of developer-things-to-do until one day (ooooh exciting!), a generic YouTube notification popped up which I was about to dismiss until I saw what it was…
I started to think that I was being taunted about not having finished my implementation for data loading (maybe I was, who knows 🤔). Those of you who know this man, know that his content is worth watching and if you don’t, you should probably take a look!
The video dives into more detail on the advantages and disadvantages of the two previously mentioned approaches:
The LaunchedEffect
- We control when data loading happens
- Testing can execute the function when needed
- Recomposition calls the
LaunchedEffect
again… - Defeats the purpose of ViewModels outliving configuration changes
The init block
- No reloading on configuration changes!
- No control over when the data is loaded…
- Some testing scenarios require code to be executed between ViewModel initialisation and data loading which is not possible here
Let’s write some code!
I’ll be working up from what I previously wrote in my MVI article to integrate the proper way of loading initial data.
My first issue was the way the state
variable was defined
There’s nothing inherently wrong with this code. However, in its current form, there is no way to load initial data directly into the state
. The only initial data that can be loaded is from the initialState
variable but this doesn’t correspond to what we want for one major reason:
- It’s static. i.e. Once you inherit from the
BaseViewModel
class, you have to pass in a variable to be able to compile the project.
What we want is the following:
- Given the
initialState
as a starting point, - When the
state
variable is accessed for the first time, - Then proceed with loading the initial data into it.
To be able to do this, we need two helper functions that thankfully already exist: onStart
and stateIn
.
onStart
to the rescue
This extension function was clearly one I had forgotten about and I’m very thankful to Philipp for reminding me of it, especially how to use it!
The official documentation states that it “Returns a flow that invokes the given action before this flow starts to be collected.”
Now I don’t know about you but that sounds very close to what we need 🤔 The only issue is that Flow != StateFlow so we need another puzzle piece to bring all of this together.
stateIn to save the day
This extension function is well known amongst developers that use Kotlin Flows regularly and will come as no surprise. The behaviour here is simply to take an input Flow
and, with the provided parameters, returns a StateFlow
.
All together now
The use of
by lazy { }
here is to ensure the content of theonStart
is only called when getting the data and not every time we access thestate
variable
As you can see, we now have a way to load initial data directly into our state
variable. We use viewModelScope
to link our state
to the underlying ViewModel, the initialValue
is our existing initialState
and finally, the started
parameter is set to share the state when the first collector appears and stop 5 seconds after the last one disappears. Why 5 seconds? I always assumed it was an arbitrary value but after reading Jaewoong’s article, I learnt that it actually is the ANR deadline! (Thanks Ian Lake for the explanation)
We need more code
Wait a minute…
What? What do you mean wai-aaahhhh yes… If you think about what we have right now, can you tell me how each ViewModel loads its specific initial data into the state
?
Yep, currently, it can’t. So we have this very nice initial-data-ready state
variable that pretty much just looks good but does nothing (Kind of like a plastic plant). Ok let’s fix this and be done with it!
99% of the time, my data loads are done via a UseCase (I explain what this is in this article ← To Be Published) and require a Coroutine (or a suspend function). Since we’re in a ViewModel, we’re obviously going to use viewModelScope
for this.
Woohoo, now we’re done! Right?
Nope
What do you mean “nope”?
How does each ViewModel use this?
Well, all you need to do is call yourrrrrr-’re absolutely right… they can’t use this… Ok fine! I’ll add more code to fix this because more code always fixes everything 🙄
The issue is that although we’ve added the viewModelScope
to the onStart
, we need to provide a way for specific ViewModel implementations to call their own data loading functions within it. Kotlin has this neat keyword called open
that will allow us to do just that!
We could use
abstract
here instead ofopen
but this would force all ViewModel’s to define a data loading function even when they don’t load data soopen
allows us to only override the function where we want.
Now we’re done (seriously, we are)! In this state, each ViewModel can override the initialDataLoad
and specify its specific data loading operation which will be executed as soon as the state
has an active collector.
An example integration would look like the following:
- Override the
initialDataLoad
function - Call the initial data loading UseCase
- Collect the data from it
- Update our MVI state with the
sendEvent
function - And voilà!
- We’re good to go!
- … Why is this list still going?
Ahh, better. As you can see, with our MVI implementation, we can now cleanly load initial data whilst taking advantage of the easy testability of our architecture. We also ensure performance is optimal by not loading data when not needed and, inversely, loading it when it should be.
Enforcing the behaviour
As a small bonus, you’ll have noticed that nothing prevents you from still using the init
or LaunchedEffect
methods.
To prevent the use of the init
block for data loading, you can use the Konsist library with the following test:
For the LaunchedEffect
method, unfortunately, I haven’t found a simple way to prevent its use since it implies calling a function defined in the ViewModel which could have any name. Testing against this, with such a wide range of possibilities, would be more cumbersome than beneficial in my opinion so I’ll you find a solution yourself if needed 😉.
That’s all folks!
You can find the complete code implementation here:
And you’ll find Jaewoong’s article here (be sure to give him a follow too!)