Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by Scott Graham on Unsplash

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… 😅

@HiltViewModel
class ForYouViewModel @Inject constructor(
private val getTopicsUseCase: GetTopicsUseCase,
private val getNewsUseCase: GetNewsUseCase
) : BaseViewModel<ForYouState, ForYouEvent, ForYouEffect>(
initialState = ForYouState.initial(),
reducer = ForYouScreenReducer()
) {
init {
// Get data via UseCases
}
fun onTopicClick(topicId: String) {
sendEffect(
effect = ForYouEffect.NavigateToTopic(
topicId = topicId
)
)
}
}

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:

  • 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

abstract class BaseViewModel<State : Reducer.ViewState, Event : Reducer.ViewEvent, Effect : Reducer.ViewEffect>(
initialState: State,
private val reducer: Reducer<State, Event, Effect>
) : ViewModel() {
private val _state: MutableStateFlow<State> = MutableStateFlow(initialState)
val state: StateFlow<State>
get() = _state.asStateFlow()
// Remaining ViewModel variables and functions
}

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
val state: StateFlow<State> by lazy {
_state.onStart {
// Load initial data here
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = initialState
)
}

The use of by lazy { } here is to ensure the content of the onStart is only called when getting the data and not every time we access the state 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.

val state: StateFlow<State> by lazy {
_state.onStart {
viewModelScope.launch {
// Load initial data here
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = initialState
)
}

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!

abstract class BaseViewModel<State : Reducer.ViewState, Event : Reducer.ViewEvent, Effect : Reducer.ViewEffect>(
val initialState: State,
private val reducer: Reducer<State, Event, Effect>
) : ViewModel() {
private val _state: MutableStateFlow<State> = MutableStateFlow(initialState)
val state: StateFlow<State> by lazy {
_state.onStart {
viewModelScope.launch {
initialDataLoad()
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = initialState
)
}
// Other variables
open suspend fun initialDataLoad() {}
// Other functions
}

We could use abstract here instead of open but this would force all ViewModel’s to define a data loading function even when they don’t load data so open 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:

@HiltViewModel
class HomeViewModel @Inject constructor(
private val loadInitialDataUseCase: Lazy<LoadInitialDataUseCase>,
) : BaseViewModel<HomeViewState, HomeViewEvent, HomeViewEffect>(
initialState = HomeViewState.initial(),
reducer = HomeReducer()
) {
override suspend fun initialDataLoad() {
loadInitialDataUseCase.get().invoke(Unit).collect { result ->
when (result) {
is Result.Success -> sendEvent(
event = HomeViewEvent.UpdataInitialData(
data = result.data
)
)
else -> Unit
}
}
}
}
  1. Override the initialDataLoad function
  2. Call the initial data loading UseCase
  3. Collect the data from it
  4. Update our MVI state with the sendEvent function
  5. And voilà!
  6. We’re good to go!
  7. … 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.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

No results found.

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:

@Test
fun `ViewModels should not have UseCases in init block`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("ViewModel")
.withoutName("BaseViewModel")
.assertTrue { viewModel ->
viewModel.initBlocks.all { initBlock ->
!initBlock.hasTextContaining("UseCase")
}
}
}

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!)

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu