Topic: 10 Understanding Dependency Injection

This is our tenth topic from learn android from basic to advance series

Topic: 10 Understanding Dependency Injection

Hello devs, Today, we'll discuss Dependency Injection. Many tasks that we typically write manually in code can be automated with the help of dependency injection. We simply need to instruct the DI framework about the dependencies we require and how they should be instantiated.

Dependency Injection

Dependency injection is design pattern used to managed the dependency between different component in an application. The Process of passing dependencies into a class rather than the class creating them itself. This can be done using constructor injection, method injection. The framework we use for the DI is Dagger, Hilt, and Koin.

Why Dependency Injection in Android?

Android applications often consist of numerous components that rely on each other, such as Activities, Fragments, Services, and ViewModels. Without proper management, these components can become tightly coupled, making the codebase difficult to maintain and test. Dependency Injection helps to decouple these components by providing a way to manage their dependencies effectively.

Best Practices

  • Keep dependencies minimal: Avoid injecting unnecessary dependencies into your components to keep them focused and maintainable.

  • Use constructor injection: Prefer constructor injection over field or method injection as it makes dependencies explicit and ensures they are satisfied at the time of object creation.

  • Modularize your application: Organize your dependencies into modules based on their functionality to keep your DI configuration manageable and maintainable.

  • Follow naming conventions: Use meaningful names for your modules, components, and dependencies to improve readability and understanding of your DI configuration.

Alright, devs let's explore two examples of Dependency Injection (DI). I'll demonstrate Hilt DI and Koin DI to provide a clear comparison.

Hilt Dependency Injection Example

Step 1: Add Dependencies

Add the necessary dependencies to your app's build.gradle file:

// Hilt
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-compiler:2.38.1"

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"

// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.1"

Step 2: Set up Hilt

Enable Hilt in your Application class:

@HiltAndroidApp
class MyApplication : Application()

Step 3: Define User Model

Create a data class to represent a user:

data class User(val id: Int, val name: String, val email: String)

Step 4: Define the Network Module

Create a Network Module to provide Retrofit.

@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .readTimeout(60, TimeUnit.SECONDS)
            .connectTimeout(60, TimeUnit.SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://your.base.url/") // Replace with your base URL
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

Step 5: Define Retrofit Service

Create a Retrofit service interface to define API endpoints:

interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<User>
}

Step 6: Create UserRepository

Define a repository interface to abstract the data source:

interface UserRepository {
    suspend fun getUsers(): List<User>
}

Step 7: Implement UserRepository

Implement the repository interface using Retrofit:

class UserRepositoryImpl @Inject constructor(private val apiService: ApiService) : UserRepository {
    override suspend fun getUsers(): List<User> {
        return apiService.getUsers()
    }
}

Step 8: Create ViewModel

Develop a ViewModel to manage data for the UI:

@HiltViewModel
class UserViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {
    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> = _users

    fun fetchUsers() {
        viewModelScope.launch {
            _users.value = userRepository.getUsers()
        }
    }
}

Step 9: Develop Activity

Create an Activity to observe the ViewModel and update the UI:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.users.observe(this, { users ->
            // Update UI with users data
        })

        viewModel.fetchUsers()
    }
}

In this example, we've created an Android app that fetches a list of users from an API using Retrofit. We've used Hilt for Dependency Injection, MVVM architecture pattern for separation of concerns, and a repository to abstract the data source. This approach makes the codebase modular, testable, and maintainable. Hilt automatically handles the injection of dependencies, Retrofit facilitates network requests, and MVVM ensures a clear separation of UI logic from business logic.

As you see in the example we use some kind of annotation so this is Hilt annotation let's explore one by one.

  1. @HiltAndroidApp: This annotation is used on your custom Application class to trigger Hilt's code generation. It generates the necessary components and modules for dependency injection. By adding this annotation, you indicate that your application is Hilt-enabled.

  2. @HiltViewModel: This annotation is used to mark a ViewModel class for injection. When you use @HiltViewModel, Hilt will automatically provide dependencies to the ViewModel's constructor. It's particularly useful for injecting repository instances or other dependencies into ViewModels.

  3. @InstallIn: This annotation is used to specify the component where the provided dependencies should be available. Hilt requires you to specify where the dependencies should be installed. Commonly used components are ActivityComponent, FragmentComponent, ServiceComponent, ApplicationComponent, etc.

  4. @Module and @Provides: These annotations are used together to define modules and methods that provide dependencies. Modules are classes where you define methods annotated with @Provides to tell Hilt how to create instances of the provided types. Hilt will use these methods to construct dependencies.

  5. @Inject: This annotation is used to mark dependencies for which Hilt should provide instances. You can apply @Inject to constructor parameters, fields, or methods. When applied to a constructor, Hilt knows how to construct the object automatically.

  6. @AndroidEntryPoint: This annotation is used to mark Android classes for field and method injection. It enables Hilt to inject dependencies into Android components such as Activities, Fragments, Services, etc. Once marked with @AndroidEntryPoint, you can use Hilt's by viewModels() delegate to inject ViewModels or simply annotate fields with @Inject to inject dependencies.

These annotations, along with the respective classes and methods, work together to enable Hilt's dependency injection capabilities in your Android application. They help in maintaining a clear separation of concerns, promoting modularity, and easing the process of managing dependencies within your app.

Koin Dependency Injection Example

Step 1: Add Dependencies

Add the necessary dependencies to your app's build.gradle file:

// Koin
implementation "org.koin:koin-android:3.2.0"

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"

// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.1"

Step 2: Define User Model

Create a data class to represent a user:

data class User(val id: Int, val name: String, val email: String)

Step 3: Define Retrofit Service

Create a Retrofit service interface to define API endpoints:

interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<User>
}

Step 4: Create UserRepository

Define a repository interface to abstract the data source:

interface UserRepository {
    suspend fun getUsers(): List<User>
}

Step 5: Implement UserRepository

Implement the repository interface using Retrofit:

class UserRepositoryImpl(private val apiService: ApiService) : UserRepository {
    override suspend fun getUsers(): List<User> {
        return apiService.getUsers()
    }
}

Step 6: Create ViewModel

Develop a ViewModel to manage data for the UI:

class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> = _users

    fun fetchUsers() {
        viewModelScope.launch {
            _users.value = userRepository.getUsers()
        }
    }
}

Step 7: Develop Activity

Create an Activity to observe the ViewModel and update the UI:

class MainActivity : AppCompatActivity() {

    private val viewModel: UserViewModel by viewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.users.observe(this, { users ->
            // Update UI with users data
        })

        viewModel.fetchUsers()
    }
}

Step 8: Define Koin Modules

Create Koin modules to provide dependencies:

val networkModule = module {
    single { OkHttpClient.Builder().build() }
    single {
        Retrofit.Builder()
            .baseUrl("https://your.base.url/")
            .client(get())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    single<ApiService> { get<Retrofit>().create(ApiService::class.java) }
}

val repositoryModule = module {
    single<UserRepository> { UserRepositoryImpl(get()) }
}

val viewModelModule = module {
    viewModel { UserViewModel(get()) }
}

Step 9: Start Koin

Initialize Koin in your Application class:

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApplication)
            modules(networkModule, repositoryModule, viewModelModule)
        }
    }
}

In this example, we've created an Android app that fetches a list of users from an API using Retrofit. We've used Koin for Dependency Injection, MVVM architecture pattern for separation of concerns, and a repository to abstract the data source. This approach makes the codebase modular, testable, and maintainable. Koin handles dependency injection for us, Retrofit facilitates network requests, and MVVM ensures a clear separation of UI logic from business logic.

Here's a comparison table between Hilt and Koin

FeatureHiltKoin
Dependency Injection (DI)Built-in DI solution by Google, part of JetpackStandalone DI framework for Kotlin and Android
Annotation-based DIYesNo
Constructor InjectionYesYes
Field InjectionYesYes
Method InjectionYesYes
ScopingSupports predefined scopes (e.g., Singleton, ActivityScoped, etc.)Flexible scoping with modules and definitions
Compile-Time CheckingYesNo (runtime checking)
Integration with Android ComponentsFully integrated with Android framework components (Activity, Fragment, Service, etc.)Decoupled from Android framework
Setup ComplexityModerate to highLow to moderate
Configuration FlexibilityLimited to predefined scopes and bindingsHighly flexible configuration using modules and definitions
Learning CurveSteeper learning curve due to its integration with Dagger and Android componentsEasier learning curve with a simpler syntax and approach
PerformancePerformance optimized with compile-time checks and optimizationsSlightly lower performance due to runtime checks and reflection
Community SupportStrong community support due to integration with Android ecosystemActive community support from Kotlin developers
Official SupportOfficially supported by Google as part of JetpackIndependent open-source project maintained by the community
Use CasesSuitable for larger projects and team collaboration where compile-time safety and full Android integration are crucialSuitable for smaller to medium-sized projects or projects where flexibility and simplicity are prioritized

Alright, devs it's time to wrap up this blog. Remember both Hilt and Koin are excellent choices for dependency injection in Android projects, and the choice between them depends on the specific requirements and preferences of the project. Hilt offers strong integration with the Android ecosystem and compile-time safety, while Koin provides flexibility and simplicity with a lower learning curve. See you on our next topic Coroutins.


Connect with Me:

Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. πŸ“²πŸ‘¨β€πŸ’» Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟

Check out my Instagram page

Check out my LinkedIn

Β