avatarViktoriia.io

Summary

The article discusses the pitfalls of using singletons in Koin DI (Dependency Injection) framework, particularly when dealing with dynamic data, and emphasizes the importance of choosing the correct provider functions to avoid app failures.

Abstract

The author shares a cautionary tale from their experience with Koin DI, illustrating how a seemingly harmless optimization to reduce boilerplate code led to unexpected app behavior. The issue arose when dynamic app configuration data, meant to be updated periodically, was incorrectly declared as singletons in the Koin module. This caused inconsistencies in the UI, as parts of the app did not receive updates. The article explains the incorrect approach of using 'single' declarations for non-static, changeable data and contrasts it with the correct use of 'factory' declarations. The correct implementation ensures that the latest data is always accessed, maintaining consistency across the app. The author encourages developers to be cautious when choosing provider functions in Koin DSL to prevent such issues and save time on debugging.

Opinions

  • The author believes that developers should be cautious and think twice before deciding something is a singleton.
  • There is an emphasis on the importance of understanding the implications of using 'single' versus 'factory' in Koin DI to manage the lifecycle of dependencies properly.
  • The author suggests that while Koin's DSL makes module declarations concise, it can also mislead developers into making mistakes with dependency scopes.
  • The article implies that the problem might not be immediately obvious in a large codebase, potentially leading to significant time spent on debugging.
  • The author expresses that one word in the Koin DSL (e.g., 'factory' instead of 'single') can drastically change the behavior of an app and prevent issues.
  • The author provides the insight that the wrong choice of provider function can lead to weird and inconsistent app behavior, which can be difficult to diagnose.

App Failures By Koin: Story 1 — Be Careful With Singletons

This story will show you the importance of choosing the right provider functions in your Koin module declarations.

Photo by Stanislav Ivanitskiy on Unsplash

Nothing new to most of you, like a rule of thumb: “think twice before you decide smth is a singleton”. And I’m pretty sure you’re already doing this like we were doing. Hence now you will most probably go to skip this part :).. but wait a min, check the code first.

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

Story 2 — Unreliable Dynamic Loading

Story 3 — Issue With sharedViewModel()

Story 4 — Does It Worth Sharing a ViewModel?

… to be continued…

Koin DSL

Let’s remind ourselves what is Koin first:

A pragmatic lightweight dependency injection framework for Kotlin developers. Written in pure Kotlin, using functional resolution only: no proxy, no code generation, no reflection. Koin is a DSL, a light container and a pragmatic API

DSL (domain-specific language) makes Koin declarations concise and handy so that with a couple of lines you can define your module. Usually your module declaration is smth like (example from the official doc):

// ComponentB <- ComponentA
class ComponentA()
class ComponentB(val componentA : ComponentA)

val moduleA = module {
    // Singleton ComponentA
    single { ComponentA() }
}

val moduleB = module {
    // Singleton ComponentB with linked instance ComponentA
    single { ComponentB(get()) }
}

For those of you who are not yet familiar with Koin injection and DSL functions, please reference to the official documentation. Now, when we all are on the same page, let’s move to the real app example and failure explanation.

What Went Wrong?

So imagine you have some singleton, some service in your app, let’s call it SharedDataContainer. This class holds a bus of periodically updated information, let it be a LiveData, and all subscribers receive new values of AppConfiguration once subscribed.

interface SharedDataContainer {
    val appData: LiveData<AppConfiguration>
    fun appData() =
        appData.value ?: error("SharedDataContainer is not initalised")
}
class SharedDataContainerImpl : SharedDataContainer { ... }
single<SharedDataContainer> { SharedDataContainerImpl() }

All fine so far, right? Application was growing, AppData is grouping more and more valuable information, received from server, some dynamic app parameters, theme, etc:

data class AppData(
    val userSettings: UserSettings,
    val defaultLocation: Location,
    val theme: Theme,
...
)
data class Theme(
    val attributes: Attributes,
    val styles: Styles
)
data class Attributes(
    val colors: Colors,
    val fonts: Fonts,
...
)

A question now — what would you do to use “colors” in one of your views? Okay, you may inject your SharedDataContainer and access it via properties:

val sharedDataContainer: SharedDataContainer by inject()
...
val color = sharedDataContainer.appData().theme.attributes.colors.accentColor

But you’re a bit lazy to do this every time you need to use colors values, or fonts, and after some time you think — wait, I can optimise this, I can extend DI module declaration to write less words in injection:

single<SharedDataContainer> { SharedDataContainerImpl() }
single<AppData> { get<SharedDataContainer>().appData() }
single<Theme> { get<AppData>().theme }
single<Attributes> { get<Theme>().attributes }
single<Colors> { get<Attributes>().colors }

And now life is perfect :) Yeap, injection works like a charm, great job ;) Just compare, isn’t it better?

val colors: Colors by inject()
...
val color = colors.accentColor

Congratulations 🥳 , just now we not only improved the code & saved our fingers from typing more words, but we also fall down…🥺

Photo by T.H. Chia on Unsplash

Understanding & Fixing

Have you already caught the problem? Might be yes, because the story has a tip, and you already know where to pay attention to 👀.. But if you have tens of modules declarations, thousands of lines of code, and once someone tells you “part of your views don’t receive server updates for the UI” — how much time and effort would you spend to recognize the problem? How much time of debugging it will take to catch the diverged theme attributes in short and long injection ways? I believe it won’t be that fast as reading this post 😏

So what’s just happened ? You just saw the wrong way of declaring non static, changeable data via ‘shortcuts’.

Photo by Free To Use Sounds on Unsplash

Let’s dispel doubts and check the right version of being concise in injection and at the same time guarantee the correct work of the app.

single<SharedDataContainer> { SharedDataContainerImpl() }
factory<AppData> { get<SharedDataContainer>().appData() }
factory<Theme> { get<AppData>().theme }
factory<Attributes> { get<Theme>().attributes }
factory<Colors> { get<Attributes>().colors }

One word can change the situation dramatically, like “factory” in this case. Ultimately, we have the expected app behavior and consistency in all injection alternatives. Declaration via “factory” guarantees that the latest data is taken each time it’s referenced in injection or assigned to a variable, whereas “single” makes a copy of data inside the koin component when it’s first referenced. As a result, from the UI perspective application behavior was so weird & inconsistent, that it took some hours to understand what’s the root of evil…

To sum up the story, Koin DSL module declarations by examples usually look very obvious when you read documentation and guidelines. However, when you are writing your code it’s quite easy to make a mistake just because you automatically chose the wrong providing function. Hope this article will save you some hours of debugging and you’ll be careful with singletons ;) Thanks for reading, and let’s see in the next stories.

References

Koin
Android
Android App Development
Dependency Injection
Kotlin
Recommended from ReadMedium