App Failures By Koin: Story 2— Unreliable Dynamic Loading

Viktoriia.io
Dec 14, 2020 · 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

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

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 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 KoinComponentclass.

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

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

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

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

// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
unloadKoinModules(koinModules())
loadKoinModules(koinModules())
}
override fun onDestroy() {
unloadKoinModules(koinModules())
super.onDestroy()
}
// Entry point
internal fun openMain(context: Context, flags: Int? = null) {
// No loading here
val intent = MainActivity.callingIntent(context)
flags?.let { intent.setFlags(it) }
context.startActivity(intent)
}
// Modules
internal val repositoriesModule = module {
single { DynamicRepositoryImpl() } bind DynamicRepository::class
...
}

internal fun viewModelsModule() = module {
viewModel { MainViewModel(get()) }
viewModel { OtherViewModel() }
}

Finally, we reached the goal, and all modules required for the Dynamic dependencies are loaded and unloaded properly. We also got deeper into the dynamic loading of modules algorithm, examined override option as well as unloading before loading pair. The solution received at the end is the most working one in the current situation, it’s not perfect, but quite reliable and easier to support. It would be better to have some sort of manageable lifecycle to handle dynamic loading, but it’s like a future improvement or solution for the new project when writing it from scratch.
Anyway, thanks for reading! Hope you enjoyed and found some answers to the question you could potentially have when using Koin in your app. If you have any suggestions on how to do it better — you’re welcome😊

References

The Startup

Get smarter at building your thing. Join The Startup’s +725K followers.

Viktoriia.io

Written by

Software Developer, Android https://www.linkedin.com/mwlite/in/viktoriia-ioltukhivska-41a3434a

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +725K followers.

Viktoriia.io

Written by

Software Developer, Android https://www.linkedin.com/mwlite/in/viktoriia-ioltukhivska-41a3434a

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +725K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store