Simplify the Word - Dependency Injection
- Dependency Injection (DI) is a software design pattern used in object-oriented and modular programming to achieve the principle of Inversion of Control (IoC).
- It is a technique that allows to separate the construction and handling of the dependencies of an object from the object itself.
Goals of using Dependency Injection
-
Decoupling: Helps in reducing the coupling between different components of a system. Components are no longer responsible for creating their own dependencies, causing them more reusable and easier to maintain.
-
Testability: Easier to write unit tests for the code. One can inject mock or test-specific dependencies when testing a component, allowing to isolate and control the behavior of the component.
-
Flexibility: Enables to change the behavior of a component by simply changing the injected dependencies, rather than modifying the component’s code making the codebase more adaptable to changes and extensions.
Dependency Injection in Android
Some popular Dependency Injection frameworks for Android include Dagger, Hilt, and Koin.
These frameworks help to manage the creation and injection of dependencies into the Android components like Activities, Fragments, Services, and ViewModels.
Overview of how DI works in Android
-
Module Configuration ~ One can create modules that define how to create and provide instances of the dependencies. These modules can include the configuration of various objects, such as network clients, database instances, or repositories.
-
Component Setup: One can define a component that specifies the classes or scopes where the dependencies should be injected. Components act as bridges between the modules and the Android components that require these dependencies.
-
Injection Points: In the Android components (e.g., Activities, Fragments, or ViewModels), one can annotate fields, constructors, or methods with DI annotations (e.g., @Inject) to indicate where dependencies should be injected.
-
Dependency Resolution: When the Android component is created (e.g., when an Activity is launched), the DI framework uses the configuration from the modules and the component to resolve and inject the required dependencies into the component.
Hilt for Dependency Injection
- Hilt was developed by Google and is based on Dagger.
- Hilt simplifies the setup and configuration process for Dagger.
- It provides components and annotations that make it easier to configure Dagger.
Why use Hilt for Dependency
- Hilt reduces the boilerplate code of using manual dependency injection in the project.
- Manual dependency injection requires constructing every class and its dependencies by hand but Hilt automatically generates and provides components for integrating Android framework classes with Dagger.
What is Qualifier in Hilt Dagger
-
Purpose ~
Qualifiers are used to distinguish between different instances of the same type when one has multiple bindings for the same type. They help to specify which specific instance of a dependency should be injected at a particular injection point.
-
UseCase ~
Qualifiers are often used when one has multiple implementations of the same interface or when one needs to differentiate between similar dependencies.
-
Example ~
Lets say we have two implementations of a MLogger interface, one for logging to the console and another for logging to a file. We can use qualifiers to indicate which logger implementation should be injected.
@Module
@InstallIn(SingletonComponent.class)
public class MLoggingModule {
@Provides
@ConsoleLoggerQualifier
public MLogger provideConsoleLogger() {
return new ConsoleLogger();
}
@Provides
@FileLoggerQualifier
public MLogger provideFileLogger() {
return new FileLogger();
}
}
What is Annotation in Hilt Dagger
-
Purpose ~
Annotations in Hilt are used for various purposes, including marking Android components for dependency injection (@AndroidEntryPoint), marking application classes for Hilt initialization (@HiltAndroidApp), and marking Dagger modules for dependency binding and configuration.
-
UseCase ~
Annotations serve as instructions to Hilt and Dagger to perform specific actions or generate code in a certain way. They provide metadata and configuration information.
-
Example ~
@HiltAndroidApp is an annotation used to mark the custom Application class, indicating that Hilt should initialize itself for the Android application. It serves as an instruction to Hilt’s code generation process.
@HiltAndroidApp
public class MainApplication extends Application {
// ...
}
Important annotations used in Hilt
-
@HiltAndroidApp ~
Used to mark the custom Application class, indicating that Hilt should initialize itself for the Android application. It’s typically the entry point for setting up Hilt in the app.
@HiltAndroidApp
public class MainApplication extends Application {
// ...
}
-
@AndroidEntryPoint ~
When we annotate an Android component(like Activity, Fragment) with @AndroidEntryPoint, Hilt will handle the injection of dependencies into that component.
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
// ...
}
-
@Module ~
We use @Module to define Dagger modules where we provide bindings for the dependencies. These modules are then installed in Dagger components using the @InstallIn annotation.
@Module
@InstallIn(ActivityComponent.class)
public class MainModule {
// ...
}
-
@InstallIn ~
This annotation specifies which Dagger component a module should be installed in. For example, one can use @InstallIn(ActivityComponent.class) to indicate that a module should be available for injection in activities.
-
@Provides ~
Within a Dagger module, we use @Provides to define methods that provide instances of dependencies. These methods are responsible for creating and returning the required objects.
@Module
@InstallIn(ActivityComponent.class)
public class MainModule {
@Provides
public ApiService provideApiService() {
return new ApiService();
}
}
-
@Inject ~
In Android components (e.g., activities, fragments, view models), we use @Inject to indicate that a field, constructor, or method requires dependency injection. Hilt will inject the requested dependencies into these marked elements.
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
@Inject
ApiService apiService;
// ...
}
-
@ViewModelInject ~
For injecting dependencies into Android ViewModel classes, one can use @ViewModelInject. This annotation allows Hilt to inject dependencies into the ViewModel’s constructor.
@HiltViewModel
public class MainViewModel @ViewModelInject constructor(
private final MainRepository repository
) : ViewModel() {
// ...
}
-
@Binds ~
This annotation tells Hilt which implementation to use when it needs to provide an instance of an interface.
Extra Facts about Hilt
- Component Scopes - All bindings in Hilt are unscoped. This means that each time any app requests the binding, Hilt creates a new instance of the needed type.
Component lifetimes - Hilt automatically creates and destroys instances of generated component classes following the lifecycle of the corresponding Android classes.
Hilt automatically generates and provides the following -
- Components for integrating Android framework classes with Dagger that one would otherwise need to create by hand.
- Scope annotations to use with the components that Hilt generates automatically.
- Predefined bindings to represent Android classes such as Application or Activity.
- Predefined qualifiers to represent @ApplicationContext and @ActivityContext.
Hilt currently supports the following Android classes:
- Application (by using @HiltAndroidApp)
- ViewModel (by using @HiltViewModel)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
Code for adding Hilt in Android
Step 1 : In project/build.gradle file, please add
plugins {
id("com.google.dagger.hilt.android") version "2.44" apply false
}
Step 2 : In app/build.gradle file, please add
plugins {
kotlin("kapt")
id("com.google.dagger.hilt.android")
}
android {
...
}
dependencies {
implementation("com.google.dagger:hilt-android:2.44")
kapt("com.google.dagger:hilt-android-compiler:2.44")
}
kapt {
correctErrorTypes = true
}
Step 3 : Hilt uses Java 8 features. To enable Java 8 in your project, add the following to the app/build.gradle file:
android {
...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
Step 4 : @HiltAndroidApp triggers Hilt’s code generation, including a base class for the application that serves as the application-level dependency container.
@HiltAndroidApp
class MainApplication : Application() {
}
Step 5 : In AndroidManifest.xml file add the below code under
android:name=".MainApplication"
Applying the above Lets do some coding of fetching some api data and showcase
Step 6 : Creating a data class - FactModel
data class FactModel(val fact: String)
Step 7 : Creating an interface
interface FactsApi {
@GET(FACTS_ENDPOINT)
suspend fun getFacts(): Response<List<FactModel>>
}
Step 8:
class FactService @Inject constructor(private val factsApi: FactsApi){
suspend fun getFacts(): List<FactModel>{
return withContext(Dispatchers.IO){
val facts = factsApi.getFacts()
facts.body()?: emptyList()
}
}
}
Step 9 :
class FactsRepository @Inject constructor(private val factService: FactService){
suspend fun getFacts(): List<FactItem>{
return factService.getFacts().map {
it.toFactItem()
}
}
}
Step 10 :
class GetFactsUseCases @Inject constructor(private val factsRepository: FactsRepository){
suspend operator fun invoke(): List<FactItem>{
return factsRepository.getFacts().shuffled()
}
}
Step 11 :
@HiltViewModel
class FactViewModel @Inject constructor(private val factsUseCases: GetFactsUseCases):ViewModel(){
private val _facts = MutableStateFlow(emptyList<FactItem>())
val facts : StateFlow<List<FactItem>> get() = _facts
init {
getFacts()
}
private fun getFacts() {
viewModelScope.launch{
try{
val fact = factsUseCases()
_facts.value = fact
}catch (_: Exception){}
}
}
}
Step 12 : Here we use compose to get the fact data from viewmodel, and in fragment/ activity class
@AndroidEntryPoint
class SplashScreenFragment : Fragment() {
private val splashViewModel: SplashViewModel by viewModels()
val fact = splashViewModel.facts.collectAsState()
}
Drawbacks of Hilt
-
Limited to Android: Hilt is designed specifically for Android, which means it may not be suitable to share code and dependencies across multiple platforms.
-
Runtime Overhead: While Hilt aims to minimize runtime overhead, there can still be some overhead associated with the generated code and the Hilt runtime which may cause an overhead in performance-sensitive applications.
-
Prescribed Architecture: Hilt encourages the use of certain architectural patterns, such as ViewModel and repository patterns, which might not align with some project’s specific requirements or existing architecture.
-
Annotation Processing Time: Hilt relies on annotation processing to generate code for dependency injection. This can increase build times, especially in larger projects.
Subject to note that - It’s important to note that the disadvantages of Hilt should be considered in the context of the specific project requirements and team expertise. Hilt can be an excellent choice for many Android projects, especially those where simplicity and Android-specific features are valued.
Summary
By adding Hilt dependencies, we implement dependency injection in Android, Which make the code more modular, testable, and maintainable, while also promoting better separation of concerns within the application.