Clean code: Android modules and Dependency Injection (feat: Koin)

A story of clean separations and cordial agreements

Jacob Allenwood
Level Up Coding

--

A modular app design is vital for developer sanity, app scalability, and code quality.

Design is the art of separation, grouping, abstraction, and hiding. — Uncle Bob Martin

How can I enjoy an Android app made of proper separations and abstractions (using modules) when each module may need to know when the Application class is created?

(A few use cases for setting up modules are: initializing an analytics library, observing a LocaleLiveData to sync recommendations when the app language changes, or unpacking bundled data and inserting into a local database.)

Great question! To explain, let’s hear from our good friend, Application:

Quick note: though we’ll be using Koin as our DI library here, the following design patterns and process also applies to Dagger 2 (and most, if not all, other DI libraries).

Hi, I’m Application!

I live in a module named app. I‘m composed of lots of different modules because I like to be clean! I let each module do its own thing and I don’t worry about their implementation details — honestly, I’m more into the big-picture…

But modules need me... especially when I’m first created. Luckily, we’ve come to an agreement on how to communicate with each other! The base module calls it: the ModuleSetup interface. All I need to do is ask our friend Koin for all instances of ModuleSetup, and call the setup method on each one! I let the individual modules do the gritty work of telling Koin how to provide these implementations. Piece of cake 🍰.

We stay out of each other’s hair, but still have a very cordial relationship 🙂.

Fin

For those of you who prefer lists rather than stories from inanimate objects; follow these three easy steps to set up your Android app’s modules:

  1. Create a shared ModuleSetup interface with a setup(app: Application) method
  2. Provide ModuleSetup implementation(s) from each module and bind them to the interface
  3. In Application onCreate, use Koin’s getAll<ModuleSetup> to get all implementations and call setup on each with the Application context
Photo by pixpoetry on Unsplash

For the following explanations, it will be helpful to know the implied app structure:

-- app
-- modules
-- base
-- ...
-- ...
-- super-cool

The modules know nothing about app. The base module will contain code available to all other modules, including the app module. This is where we will place our ModuleSetup interface.

Create a ModuleSetup interface

Oftentimes we have code in our app that needs to be initialized. This means that we need the ability to provide callbacks for when the Application class is created. Additionally, initialization code usually requires a Context to run.

To meet these two requirements, whilst writing clean code, we need to create a contract for a class that contains a method which takes the Application context as a parameter. This will allow Koin’s getAll<T> method to return all implementations of our contract so Application can interact with them accordingly. This contract needs to be available to both the app module and all other modules, so we’ll put it in our base module.

Our contract will look like so:

package appname.setupinterface ModuleSetup {
fun setup(app: Application)
}

Now, any module that needs to be set up on Application creation can implement this interface and provide it with Koin.

Implement, provide, and bind your ModuleSetup implementation(s)

So, base has created an interface for us. Let’s implement it in our super-cool module and run some amazing initialization code.

package appname.modules.super.cool.setupimport appname.setup.ModuleSetup
import ...
class ModuleSetupImpl constructor(private val repo: SuperCoolRepository, private val locale: LocaleLiveData) : ModuleSetup { @MainThread
override fun setup(app: Application) {

// warning: do not run blocking code on the main thread
locale.observeForever {
// runs task in background
repo.syncRecommendations(it.languageTag)
}
}
}

We can use Koin to inject dependencies into our implementation, if needed; notice the SuperCoolRepository and LocaleLiveData parameters.

In this setup example, we inject a LiveData which contains our app’s locale and a Repository which will build us some content recommendations based on the locale. This setup allows us to keep our content in sync with the user’s language no matter when or where it changes.

Next, we provide the implementation in our Koin module.

package appname.modules.super.cool.di.modulesimport ...val superCoolModule = module {    ...
single { SuperCoolRepository() }
single { ModuleSetupImpl(get(), get()) } bind ModuleSetup::class
...
}

The key here is Koin’s bind operator. This allows us to bind our implementation to the ModuleSetup interface. Now, we can move on to our final step.

Ask Koin for ModuleSetup classes and profit!

We’ve already wired everything together! The last thing to do is to ask Koin for all the ModuleSetup classes and call thesetup method on each one of them.

class ModularApplication : Application(), KoinComponent {

override fun onCreate() {
super.onCreate()

startKoin
{
androidLogger()
androidContext(this@ModularApplication)
modules(
appModule,
baseModule,
…,
superCoolModule
)
}

// Set up our modules
getKoin().getAll<ModuleSetup>().forEach {
it.setup(this)
}
}

}

A few things to point out here:

  1. We start Koin as usual and load our modules, including superCoolModule
  2. We implement the KoinComponent interface in order to access Koin
  3. We getKoin() as provided by the KoinComponent and call the getAll<ModuleSetup> method (on the main thread) in order to loop through and set up each module

That’s it!

One important note before we depart: all the code within the setup method will get run on the main thread when the Application is created. This means we must avoid blocking code in our setup methods at all costs, unless we first offload the work to a background thread. The more blocking code we have to run on the main thread, the longer it will take for our Application to start up. In more extreme cases, running blocking code (disk I/O, networking, etc.), will cause the app to crash.

We’ve now implemented a way to separate our code into modules while maintaining the ability to run vital code on Application creation, all in three easy steps:

  1. Create our setup contract (interface)
  2. Implement and provide all instances of the contract
  3. Retrieve contract implementations and interact with them in Application onCreate

Now, enjoy your clean and modular Android app!

Photo by Lauren Mancke on Unsplash

--

--

Android @YouVersion. Probably drinking coffee ☕. Passionate about the Bible.