App Failures by Koin: Story 3 — Issue With sharedViewModel()
Tags: Fragments, Lazy, Viewmodel, Koin, Android

This story highlights sharing data between Fragments via ViewModels, usage of sharedViewModel injection by Koin.

Photo by Markus Spiske on Unsplash

The Roadmap

As a true lover of Koin DI framework, in this series of stories, I’d like to share some of the true app failures my team gained when we started using Koin. (Most of them were found in production, unfortunately…)

Story 1 — Be Careful With Singletons

Story 2 — Unreliable Dynamic Loading

Story 3 — Issue With sharedViewModel() (we are here)

Story 4 — Does It Worth Sharing a ViewModel?

… to be continued…

If you’re not yet familiar with Koin, please first read Story 1 from this series. Otherwise, let’s continue and check how to share ViewModels between host Activity and Fragments, and what issue you may face if you decide to use Koin sharedViewModel(…) for that purpose.

Injecting ViewModel via Android

To begin with, if you’re not yet using ViewModels in your code, or you want to refresh your knowledge about it, please refer to the official docs. ViewModels are highly valuable when you need to organize some logic, store some data that will survive config changes, and share some valuable information between your fragments or host activity and fragments.

Android provides great VM sharing alternatives:

by viewModels() + by activityViewModels() — to share data between host Activity and fragments

by activityViewModels() — to share data between fragments, keeping your host Activity unaware of sharing

by viewModels() + by viewModels({requireParentFragment()}) — to share data between parent and child fragments

by navGraphViewModels(R.id.fragment_destination_id) — to scope data to nav graph’s NavBackStackEntry

Injecting ViewModel via Koin

Koin also provides us with the ability to inject VMs, and also share them between host activity and fragments. Let’s reference the official Koin docs for some definitions:

One ViewModel instance can be shared between Fragments and their host Activity. To inject a shared ViewModel in a Fragment use: by sharedViewModel() - lazy delegate property to inject shared ViewModel instance into a property getSharedViewModel() - directly get the shared ViewModel instance The Activity sharing its ViewModel injects it with by viewModel() or getViewModel() . Fragments are reusing the shared ViewModel with by sharedViewModel() .

If you’re using parameterless constructors of ViewModels in your app you won’t see much benefit in using Koin injection instead of Android one. Moreover, Koin won’t help you with sharing VM per nav graph or between parent/child fragments. Thus, you may see some benefits in case you’re simply sharing VM between host activity and fragments when VM has parameters in the constructor. In Android injection, you’ll have to add your implementation of ViewModelProvider.Factory, whereas in Koin you won’t need it, as all can be done automatically, you just have to use call parametersOf { … } in a place where you declare VM injection.

Issue with sharedViewModel()

So, finally, we got to the tricky place, let’s check the problem together.

To start with, our Activity, in this case TestActivity, shares TestViewModel without parameters to some fragments. No issue with that, but after some time we decided that TestViewModel should have some uniqueId as a parameter in constructor (it can be anything what you need). Hence the code for TestActivity looks like:

class TestActivity : AppCompatActivity() {

private val uniqueId: WidgetId by lazy {

intent.getIntExtra(EXTRA_ID, -1)

}

private val testViewModel: TestViewModel by viewModel {

parametersOf(uniqueId)

} override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

unloadKoinModules(koinModules)

loadKoinModules(koinModules)



setContentView(R.layout.activity_test)

}

}

For the fragments injection of the TestViewModel will be as follows:

class TestOneFragment : Fragment(R.layout.fragment_test_one) {



private val viewModel by sharedViewModel<TestViewModel>()

...

} class TestTwoFragment : Fragment(R.layout.fragment_test_two) {



private val viewModel by sharedViewModel<TestViewModel>()

...

}

Now we run the app and…

Right, but what’s happened? We’re doing all as mentioned in the documentation, why is it crashing?

What’s under the hood

To understand the core reason for injection trouble we should check the source code for Koin & Android, so first let’s see what is getSharedViewModel from Koin (we’re using Koin 2.0.1):

/**

* Get a shared viewModel instance from underlying Activity

...

*/

fun <T : ViewModel> Fragment.getSharedViewModel(

clazz: KClass<T>,

qualifier: Qualifier? = null,

from: ViewModelStoreOwnerDefinition = { activity as ViewModelStoreOwner },

parameters: ParametersDefinition? = null

): T {

return getKoin().getViewModel(

ViewModelParameters(

clazz,

this@getSharedViewModel,

qualifier,

from,

parameters

)

)

}

As you may see inside getSharedViewModel(…) call Koin is wrapping passed parameters into ViewModelParameters. Afterwards, this instance of ViewModelParameters is passed into getViewModel(…) call, which under the hood looks like:

fun <T : ViewModel> Koin.getViewModel(parameters: ViewModelParameters<T>): T {

val vmStore: ViewModelStore = parameters.owner.getViewModelStore(parameters)

val viewModelProvider = rootScope.createViewModelProvider(vmStore, parameters)

return viewModelProvider.getInstance(parameters)

}



fun <T : ViewModel> ViewModelProvider.getInstance(parameters: ViewModelParameters<T>): T {

val javaClass = parameters.clazz.java

return if (KoinApplication.logger.isAt(Level.DEBUG)) {

logger.debug("!- ViewModelProvider getting instance")

val (instance: T, duration: Double) = measureDuration {

if (parameters.qualifier != null) {

this.get(parameters.qualifier.toString(), javaClass)

} else {

this.get(javaClass)

}

}

logger.debug("!- ViewModelProvider got instance in $duration")

return instance

} else {

if (parameters.qualifier != null) {

this.get(parameters.qualifier.toString(), javaClass)

} else {

this.get(javaClass)

}

}

}

I’ve highlighted the parts that highly important for us, actual receive of the ViewModel instance from ViewModelProvider. Let’s check the ViewModelProvider code (androidx.lifecycle 2.2.0)

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {

ViewModel viewModel = mViewModelStore.get(key);



if (modelClass.isInstance(viewModel)) {

if (mFactory instanceof OnRequeryFactory) {

((OnRequeryFactory) mFactory).onRequery(viewModel);

}

return (T) viewModel;

} else {

//noinspection StatementWithEmptyBody

if (viewModel != null) {

// TODO: log a warning.

}

}

if (mFactory instanceof KeyedFactory) {

viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);

} else {

viewModel = (mFactory).create(modelClass);

}

mViewModelStore.put(key, viewModel);

return (T) viewModel;

}

As you see, sharedViewModel(…) is nothing more than obtaining the already present in ViewModelProvider instance of the ViewModel. Therefore, if the instance was not added to mViewModelStore before calling sharedViewModel(…) — it won’t be found and will cause InstanceCreationException.

Note: At this moment you might think that instance was not created probably because of the wrong invocation, might be parameters were not passed. But, It doesn’t matter which parameters you pass into your sharedViewModel(…). So if you try to add parametersOf { getUniqueId() } — it won’t work either, as all of the parameters passed into sharedViewModel are omitted in the call to ViewModleProvider.get(). Revisit the source code above to see the calls again.

Fixing the issue

After checking the source code and analyzing the exception we know that ViewModel was not present in ViewModelProvider store by the time of the creation of fragments. How can it be? Let’s return to the Activity code again and check it carefully at the line of TestViewModel injection:

private val testViewModel: TestViewModel by viewModel {

parametersOf(uniqueId)

}

Koin source code for the delegate viewModel function:

inline fun <reified T : ViewModel> LifecycleOwner.viewModel(

qualifier: Qualifier? = null,

noinline parameters: ParametersDefinition? = null

): Lazy<T> = lazy { getViewModel<T>(qualifier, parameters) }

It’s a lazy call, right? Think for a second on this.

So by viewModel(…) is lazy, whereas by shareViewModel(…) is not lazy!

Lazy delegated property means initialization code executes once the property is used somewhere (if you’re not yet familiar with delegated properties, check this). Did anyone call testViewModel in our TestActivity?

Photo by Pavan Trikutam on Unsplash

Nope… You’d say in the IDE it’s pretty obvious that property is unused, you’re totally right! But this is a simplified example, our testViewModel property could be used as part of another lazy unused call, or could be present in one of the if-else branches. So don’t feel you wasted time on reading this story 😏

To make it all running & fix the runtime exception, it’s enough to add a logging line in the TestActivity:

Log.d(getLogTag(), "vm uniqueId: ${testViewModel.uniqueId}")

Now everything is working fine, as the testViewModel creation happened to make the log line executed. Voila🤩