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.
@HiltAndroidApp
: This annotation is used on your customApplication
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.@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.@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 areActivityComponent
,FragmentComponent
,ServiceComponent
,ApplicationComponent
, etc.@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.@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.@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'sby 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
Feature | Hilt | Koin |
Dependency Injection (DI) | Built-in DI solution by Google, part of Jetpack | Standalone DI framework for Kotlin and Android |
Annotation-based DI | Yes | No |
Constructor Injection | Yes | Yes |
Field Injection | Yes | Yes |
Method Injection | Yes | Yes |
Scoping | Supports predefined scopes (e.g., Singleton, ActivityScoped, etc.) | Flexible scoping with modules and definitions |
Compile-Time Checking | Yes | No (runtime checking) |
Integration with Android Components | Fully integrated with Android framework components (Activity, Fragment, Service, etc.) | Decoupled from Android framework |
Setup Complexity | Moderate to high | Low to moderate |
Configuration Flexibility | Limited to predefined scopes and bindings | Highly flexible configuration using modules and definitions |
Learning Curve | Steeper learning curve due to its integration with Dagger and Android components | Easier learning curve with a simpler syntax and approach |
Performance | Performance optimized with compile-time checks and optimizations | Slightly lower performance due to runtime checks and reflection |
Community Support | Strong community support due to integration with Android ecosystem | Active community support from Kotlin developers |
Official Support | Officially supported by Google as part of Jetpack | Independent open-source project maintained by the community |
Use Cases | Suitable for larger projects and team collaboration where compile-time safety and full Android integration are crucial | Suitable 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! ๐
Subscribe to my newsletter
Read articles from Mayursinh Parmar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mayursinh Parmar
Mayursinh Parmar
๐ฑMobile App Developer | Android & Flutter ๐๐ก Passionate about creating intuitive and engaging apps ๐ญโจ Letโs shape the future of mobile technology!