App Failures By Koin: Story 2— Unreliable Dynamic Loading
Tags: Kotlin, Koin, Android, Android App Development, Dependency Injection

App Failures By Koin: Story 2— Unreliable Dynamic Loading Viktoriia.io Follow Dec 14 · 8 min read

This story will show you the problems of dynamic loading/unloading of Koin modules.

Image on Pinterest

In the context of application, I’d rephrase this quote into: “You are responsible for those modules you have loaded.” But not that fast, let’s check what’s under the hood.

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 (we are here)

Story 3 — Issue With sharedViewModel()

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 see what’s the specific of application we’re developing and how we’re using the dynamic loading there.

Introduction

I’d highly recommend not to skip this part, but as always — it’s up to you 😏

To begin with, not every Android application needs the dynamic loading of modules in a runtime. Many applications simply startKoin in the Application and load all the needed modules once. And as far as I understood this is the intended approach of using Koin modules set up by default. However, in the application we’re working on, this approach is applicable only partially. We’re developing White-label application, which consists of different sets of features (dependencies) for different customers.

Photo by Helena Hertz on Unsplash

So, the structure of application is like this:

Core Application (starts koin and loads known modules from Core)

Core dependencies (Main app UI parts, logic, API)

Dynamic dependencies (separate features, that use API from Core, and which Application and Core are unaware of)

With the assumption that Application and Core dependencies don’t cause many questions, I’d put some light on the ‘Dynamic dependencies’. The idea is that we don’t know the final set of dependencies during development, it can be 1 dependency as well as 100.

Note: Dynamic dependencies here are not the same as Dynamic features proposed by Android. We don’t need any dynamic installation of features into the app after its’ installation. We are concentrated on the

Once the application starts it loads all known koin modules from the Core dependencies that remain consistent for all customers. Core UI has some interactive parts that can lead to Dynamic dependencies UI, a sort of internal deep-linking. Dynamic dependency is like a small application, but running from the Core Application, that is interacting with Core using some API, has its own Activities, business logic, DI, and navigation. Consequently, all of the DI initialization is the responsibility of the Dynamic dependency itself.

Koin Dynamic Modules Loading

When you’re using Koin injection you should first create properly initialised instance of KoinApplication, and this is done vis startKoin { } function.

When we start Koin, we create a KoinApplication instance that represents the Koin container configuration instance. Once launched, it will produce a Koin instance resulting of your modules and options. This Koin instance is then hold by the GlobalContext , to be used by any KoinComponent class. The startKoin can’t be called more than once. If you need several point to load modules, use the loadKoinModules function. This function is interesting for SDK makers who want to use Koin, because they don’t need to use the starKoin() function and just use the loadKoinModules at the start of their library.

According to the mentioned above, to enable Koin in our Dynamic dependencies we also need to use loadKoinModules function somewhere at the start. In pair with loadKoinModules Koin provides developers with the unloadKoinModules function, which does the unloading of modules you specify any time you need. Finally we came up to the most tricky part — what is this “start” of dependency? How to choose the right place to manage dependency modules loading and unloading?

Choosing the Right Place to Start

Photo by Jukan Tateisi on Unsplash

Nice photo, literally showing the start of our way 😊. Dealing with this dynamic loading I’m happy only about one thing — every Dynamic dependency developed by our team had a single Entry point. Usually, this was smth like MainActivity, opened on some event, interaction with the link to this feature.

internal class MainActivity : AppCompatActivity() { ... } internal fun koinModules() = listOf(viewModelsModule(), repositoriesModule) internal val repositoriesModule = module {

single { DynamicRepositoryImpl() } bind DynamicRepository::class

...

}



internal fun viewModelsModule() = module {

viewModel { MainViewModel(get()) }

viewModel { OtherViewModel() }

}

Then, you’d think, it’s not a problem 👌! Why not just loadKoinModules where we startActivity(…) of the Dynamic dependency?

internal fun openMain(context: Context, flags: Int? = null) {

loadKoinModules(koinModules())

val intent = MainActivity.callingIntent(context)

flags?.let { intent.setFlags(it) }

context.startActivity(intent)

}

This is a start point for sure, but unfortunately, I’ll have to say that this event is not the only place to open the Entry point… Yeah, we have an internal deep-link handling mechanism, so this Entry point can be also opened from a particular function in the implementation of deep-link handler contract within the dynamic feature itself.

Again, you’d think ‘why is she overcomplicating the problem? It’s bad to have code duplication, of course, but one loading call put in 2 places is not a big deal!’. It’s not you’re right, and this was the first idea that we tried in the way of experiments… Sadly, but this simplest solution didn’t work at all. Let’s check why.

Override to Avoid Duplication

First of all, if we call openMain(…) twice or more we’ll receive the runtime application crash. Ups! And we should be ready for it, as Koin is trying to load modules at Runtime dynamically, so if anything goes wrong it will throw Runtime Exception. In the current situation, it will be an instance of DefinitionOverrideException with the detailed message. Let’s check the source code of BeanRegistry.kt class to see what’s happening:

Note: We’re using Koin 2.0.1, in the newer version BeanRegistry was replaced with InstanceRegistry and ScopeRegistry and a bit different throwing mechanism for DefinitionOverrideException (read changlelog for more details).

private fun saveDefinitionForName(definition: BeanDefinition<*>) {

definition.qualifier?.let {

if (definitionsNames[it.toString()] != null && !definition.options.override) {

throw DefinitionOverrideException("Already existing definition or try to override an existing one with qualifier '$it' with $definition but has already registered ${definitionsNames[it.toString()]}")

} else {

definitionsNames[it.toString()] = definition

if (logger.isAt(Level.INFO)) {

logger.info("bind qualifier:'${definition.qualifier}' ~ $definition")

}

}

}

}

Well, it’s obvious, that BeanDefinition should be not present on calling this function, or should have a flag override set to true. We can afford it, all definitions of modules should be updated to have an override, like this:

internal val repositoriesModule = module {

single(override = true) { DynamicRepositoryImpl() } bind DynamicRepository::class

...

}



internal fun viewModelsModule() = module {

viewModel(override = true) { MainViewModel(get()) }

viewModel(override = true) { OtherViewModel() }

}

This is not a big deal, but not very much convenient, as it’s quite easy to miss this override parameter when adding new definitions. But who cares, if this is working let’s stop at this point and just use the approach.

Photo by Cindy Tang on Unsplash

Another option would be to make the whole module marked with override, would be less wordy at least:

internal val repositoriesModule = module(override = true) {

single { DynamicRepositoryImpl() } bind DynamicRepository::class

...

}



internal fun viewModelsModule() = module(override = true) {

viewModel { MainViewModel(get()) }

viewModel { OtherViewModel() }

}

Note: To make our code testable and using mock repositories/viewmodels we’ll have to use loadKoinModules with override=true. This will be causing some confusion, as code where a sequence of calls matters usually is quite fragile.

Always Unload Before Loading

Alternatively, we can try to clear existing bean definitions before loading via unloadKoinModules function. The effect would be the same, bean definition would be loaded successfully, exception won’t be thrown, and developers should not pay extra attention to the module definitions “override” parameter presence or absence. Unfortunately, this approach is also causing some inconvenience, as we should always pair load and unload calls in the way that unload is happening right before loading:

internal fun openMain(context: Context, flags: Int? = null) {

unloadKoinModules(koinModules())

loadKoinModules(koinModules())

val intent = MainActivity.callingIntent(context)

flags?.let { intent.setFlags(it) }

context.startActivity(intent)

}

This is also quite fragile, as someone may forget to call this unloading, so I’m not sure this is a 100% better option. Despite this, I think this is a better approach than using “override” parameter in the current situation.

The Hidden Problem

Eventually, when we found a more or less working approach of loading modules dynamically and the app is not crashing. Great, we can relax a bit, to be honest, this long analysis full of experiments was quite exhausting…

Photo by Kasya Shahovskaya on Unsplash

In our application the amount of Dynamic dependencies is unlimited, so we operate with tens of them, currently can be up to 50, but later will be even more. As a result, if we only use the above example for doing dynamic loading, we’ll see that memory usage by Koin will exceed after opening all the dynamic feature activities. And that’s obvious, cause we’re only loading the data, populating the Koin bean registry map, and not clearing it. In the case of singletons, this can be a problem if they hold much data and never release it.

Therefore, to improve the situation we should do the unloading of modules once Dynamic dependency is not used/not opened. Ideally, it should be some lifecycle that Core and Dynamic dependency both know and follow. But in reality, we don’t have such, so the most logical way of doing it is the Entry point, MainActivity of a feature.

override fun onDestroy() {

unloadKoinModules(koinModules())

super.onDestroy()

}

Doing this unloading in onDestroy, we should also take care of loading modules again in onCreate, as MainActivity can be reopened from background and recreated after being destroyed without passing through the Entry point opening. So the final result will look like