Skip to main content

Kotlin Dependency Injection

Introduction

Dependency Injection (DI) is a design pattern that helps you build more maintainable and testable Android applications. In essence, it's a technique where an object receives its dependencies from external sources rather than creating them itself. This pattern is fundamental in modern Android development using Kotlin.

In this tutorial, we'll explore:

  • What dependency injection is and why it matters
  • Different types of dependency injection in Kotlin
  • Popular dependency injection frameworks for Android
  • How to implement dependency injection in a real Kotlin Android application

What is Dependency Injection?

Imagine you're building a car. A car needs many components: an engine, wheels, a steering wheel, etc. Rather than having the car build these components itself, you would "inject" them into the car during assembly. This is essentially what dependency injection does for your code.

Without Dependency Injection

kotlin
class Car {
// Car creates its own engine
private val engine = Engine()

fun start() {
engine.start()
}
}

class Engine {
fun start() {
println("Engine started")
}
}

// Usage
fun main() {
val car = Car()
car.start()
}

With Dependency Injection

kotlin
class Car(private val engine: Engine) {
// Engine is provided from outside (injected)

fun start() {
engine.start()
}
}

class Engine {
fun start() {
println("Engine started")
}
}

// Usage
fun main() {
val engine = Engine()
val car = Car(engine)
car.start()
}

Output:

Engine started

Benefits of Dependency Injection

  1. Testability: You can easily mock dependencies for testing.
  2. Reusability: Components are more reusable as they're decoupled.
  3. Maintainability: Cleaner code that's easier to understand and maintain.
  4. Scalability: Makes it easier to manage large applications.
  5. Parallel Development: Teams can work on different components simultaneously.

Types of Dependency Injection in Kotlin

1. Constructor Injection

Dependencies are provided through a class constructor:

kotlin
class UserRepository(
private val apiService: ApiService,
private val database: UserDatabase
)

2. Property Injection

Dependencies are injected directly into properties:

kotlin
class UserViewModel {
lateinit var userRepository: UserRepository

fun loadUser(userId: String) {
userRepository.getUser(userId)
}
}

// Usage
val viewModel = UserViewModel()
viewModel.userRepository = UserRepository(ApiService(), UserDatabase())

3. Method Injection

Dependencies are provided via methods:

kotlin
class UserAnalytics {

fun trackUserBehavior(user: User, logger: Logger) {
logger.log("User ${user.id} performed an action")
}
}

Dagger 2

Dagger is a compile-time dependency injection framework that uses annotation processing.

Basic setup:

kotlin
// Define a module
@Module
class NetworkModule {
@Provides
fun provideApiService(): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
.create(ApiService::class.java)
}
}

// Define a component
@Component(modules = [NetworkModule::class])
interface AppComponent {
fun inject(activity: MainActivity)
}

// In your Application class
class MyApplication : Application() {
val appComponent: AppComponent = DaggerAppComponent.create()
}

// In your Activity
class MainActivity : AppCompatActivity() {
@Inject
lateinit var apiService: ApiService

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(application as MyApplication).appComponent.inject(this)
}
}

Hilt

Hilt is built on top of Dagger and specifically designed for Android apps:

kotlin
// Application setup
@HiltAndroidApp
class MyApplication : Application()

// Module definition
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApiService(): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
.create(ApiService::class.java)
}
}

// Inject into an Activity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var apiService: ApiService

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// apiService is automatically injected
}
}

Koin

Koin is a lightweight dependency injection framework for Kotlin:

kotlin
// Define your module
val appModule = module {
single { Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
.create(ApiService::class.java)
}

factory { UserRepository(get()) }
}

// Start Koin in your Application
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(appModule)
}
}
}

// Inject in your Activity
class MainActivity : AppCompatActivity() {
private val apiService: ApiService by inject()
private val userRepository: UserRepository by inject()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use injected dependencies
}
}

Practical Example: Building a Weather App

Let's build a simple weather app using dependency injection with Koin, as it's one of the easiest to set up for beginners.

Step 1: Set up Gradle dependencies

kotlin
// In your app's build.gradle
dependencies {
// Koin for Android
implementation "io.insert-koin:koin-android:3.2.0"

// Retrofit for network requests
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
}

Step 2: Create the API service

kotlin
interface WeatherApiService {
@GET("weather")
suspend fun getWeather(@Query("city") city: String): WeatherResponse
}

data class WeatherResponse(
val temperature: Double,
val description: String,
val city: String
)

Step 3: Create the Repository

kotlin
interface WeatherRepository {
suspend fun getWeather(city: String): WeatherResponse
}

class WeatherRepositoryImpl(private val apiService: WeatherApiService) : WeatherRepository {
override suspend fun getWeather(city: String): WeatherResponse {
return apiService.getWeather(city)
}
}

Step 4: Create the ViewModel

kotlin
class WeatherViewModel(private val weatherRepository: WeatherRepository) : ViewModel() {

private val _weatherState = MutableLiveData<WeatherUiState>()
val weatherState: LiveData<WeatherUiState> = _weatherState

fun fetchWeather(city: String) {
viewModelScope.launch {
_weatherState.value = WeatherUiState.Loading
try {
val response = weatherRepository.getWeather(city)
_weatherState.value = WeatherUiState.Success(response)
} catch (e: Exception) {
_weatherState.value = WeatherUiState.Error("Failed to fetch weather: ${e.message}")
}
}
}
}

sealed class WeatherUiState {
object Loading : WeatherUiState()
data class Success(val data: WeatherResponse) : WeatherUiState()
data class Error(val message: String) : WeatherUiState()
}

Step 5: Set up Koin

kotlin
// Define your module
val appModule = module {
// Network
single {
Retrofit.Builder()
.baseUrl("https://api.weatherexample.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherApiService::class.java)
}

// Repository
single<WeatherRepository> { WeatherRepositoryImpl(get()) }

// ViewModel
viewModel { WeatherViewModel(get()) }
}

// Application class
class WeatherApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@WeatherApp)
modules(appModule)
}
}
}

Step 6: Use in Activity/Fragment

kotlin
@AndroidEntryPoint
class WeatherFragment : Fragment() {

private val viewModel: WeatherViewModel by viewModel()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding.searchButton.setOnClickListener {
val city = binding.cityEditText.text.toString()
viewModel.fetchWeather(city)
}

viewModel.weatherState.observe(viewLifecycleOwner) { state ->
when (state) {
is WeatherUiState.Loading -> {
binding.progressBar.isVisible = true
binding.weatherInfo.isVisible = false
binding.errorMessage.isVisible = false
}
is WeatherUiState.Success -> {
binding.progressBar.isVisible = false
binding.weatherInfo.isVisible = true
binding.errorMessage.isVisible = false

binding.temperature.text = "${state.data.temperature}°C"
binding.description.text = state.data.description
binding.city.text = state.data.city
}
is WeatherUiState.Error -> {
binding.progressBar.isVisible = false
binding.weatherInfo.isVisible = false
binding.errorMessage.isVisible = true
binding.errorMessage.text = state.message
}
}
}
}
}

When to Use Which Framework?

  • Dagger: Good for large teams and large projects where compile-time safety is critical
  • Hilt: Simplifies Dagger specifically for Android - recommended for most Android projects
  • Koin: Great for small to medium projects and when learning DI concepts
  • Manual DI: For very small projects or when introducing the concept

Summary

Dependency injection is a powerful design pattern that helps you create more maintainable, testable, and scalable Android applications with Kotlin. By providing dependencies from outside rather than creating them within a class, you decouple your components and make them more reusable and easier to test.

We've covered:

  • The basic concept of dependency injection
  • Different types of DI (constructor, property, method)
  • Popular frameworks (Dagger, Hilt, Koin)
  • A practical example building a weather app with Koin

As you develop your Android applications, consider implementing dependency injection from the start — it may seem like extra work initially, but will save you significant time in the long run, especially as your application grows in complexity.

Additional Resources

Exercises

  1. Basic Exercise: Convert an existing class that creates its own dependencies to use constructor injection.
  2. Intermediate Exercise: Implement a simple note-taking app using Koin for dependency injection.
  3. Advanced Exercise: Compare implementations of the same app using both Hilt and Koin. Note the differences in code structure and complexity.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)