A Study: Building a Simple Dependency Injection Container in Kotlin for Android
A Study: Building a Simple Dependency Injection Container in Kotlin for Android
Dependency Injection (DI) is a core software design pattern that promotes loose coupling and enhances the testability and scalability of applications. While powerful libraries like Hilt and Koin are the standard for production Android apps, building a simple DI container from scratch is a valuable exercise. It demystifies the "magic" and solidifies the core concepts: providing dependencies to classes instead of having them create their own.
In this study, we will design and implement a basic, lifecycle-aware DI container. Our goal is to create a tool that can:
- Register dependencies (like a
UserRepositoryorAnalyticsService). - Provide instances of these dependencies on demand.
- Manage the scope of these dependencies (e.g., as singletons).
- Integrate cleanly with the Android
ViewModelarchitecture.
Step 1: Designing the Core DIContainer
The heart of our tool will be a container class responsible for holding and creating our dependencies. A simple way to store registered dependencies is in a Map, where the key is the class type (KClass) and the value is a factory lambda that knows how to create an instance of that class.
We will also manage different scopes. For this study, we'll focus on the most common one: the singleton scope, where only one instance of a dependency is ever created.
import kotlin.reflect.KClass
/**
* A simple dependency injection container.
*/
object DIContainer {
// A map to hold factory lambdas for our dependencies.
// The key is the class type, and the value is a function that creates the instance.
private val factories = mutableMapOf<KClass<*>, () -> Any>()
// A map to cache singleton instances that have already been created.
private val singletons = mutableMapOf<KClass<*>, Any>()
/**
* Registers a dependency as a singleton.
* The provided factory lambda will only be called once to create the instance.
*
* @param type The class type of the dependency to register.
* @param factory A lambda function that creates an instance of the dependency.
*/
inline fun <reified T : Any> registerSingleton(noinline factory: () -> T) {
factories[T::class] = factory
}
/**
* Resolves and provides an instance of a registered dependency.
* For singletons, it returns the cached instance or creates one if it doesn't exist.
*
* @return An instance of the requested dependency.
* @throws IllegalStateException if the dependency is not registered.
*/
inline fun <reified T : Any> resolve(): T {
// First, check if a singleton instance already exists.
val existingSingleton = singletons[T::class]
if (existingSingleton != null) {
return existingSingleton as T
}
// If not, find the factory to create it.
val factory = factories[T::class]
?: throw IllegalStateException("Cannot resolve dependency of type ${T::class.simpleName}. Not registered.")
// Create the new instance using the factory.
val newInstance = factory() as T
// Cache the new instance for future requests.
singletons[T::class] = newInstance
return newInstance
}
// A helper function to clear dependencies, useful for testing.
fun reset() {
factories.clear()
singletons.clear()
}
}
This article continues for subscribers
Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.
Become a Sponsor