I have been learning dependency injection using Dagger Hilt. I built a simple bitcoin news headline app where news are fetched from news.org API using retrofit.
The whole process involves composing dependencies, having them in a container(module) and injecting them to a viewmodel where a network request to fetch news data is done. The dependency containers in dagger hilt are called modules and are annotated with @Module annotation. Basically modules provide instructions on how Hilt should provide instances of certain types.
Along with @Module annotation modules are annotated with @InstallIn annotation to define the scope of the dependency i.e where the dependency will be used.
The whole process to successfully use dagger hilt involves.
- Integrating dagger hilt dependencies
- Setting up a dagger hilt.
- Declaring module and defining dependencies
Integrating dagger hilt dependencies
In the app level build.gradle add
dependencies {
//Dagger - Hilt
implementation "com.google.dagger:hilt-android:2.40.5"
kapt "com.google.dagger:hilt-android-compiler:2.40.5"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt "androidx.hilt:hilt-compiler:1.0.0"
implementation 'com.google.code.gson:gson:2.8.9'
//networking dependecies
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.3'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
}
Note we also added retrofit dependencies to make api requests and glide dependency to load images.
Project level build.gradle
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:2.40.5
}
Setting Up dagger hilt
Setting up dagger hilt involves defining a class that extends the Application class and adding that class to the application manifest file. The significance of this step is to provide dependencies to the application throughout its lifecycle. The class must be annotated with @HiltAndroidApp. The application class is the entry point where the generated components are attached to the application.
Let us define the application class and add it to the manifest file.
App.kt
package com.example.daggertuts
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App : Application() { }
manifest.xml
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".App"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DaggerTuts">
Note android:name=".App"
Declaring module and defining dependencies
Dagger is now set up. We have to define our dependencies. We are going to store all our dependencies in a single container. We define a module named AppModule.kt. Remember a module is a container for our dependency dagger is the bridge and our activities, fragments, view models , view etc are dependencies consumers .
So the module qualifies as a producer and by that we have a consumer producer model; dagger hilt being the engine that is facilitating this mechanism.
Let us define our module. AppModule.kt
package com.example.daggertuts.di
import com.example.daggertuts.BuildConfig
import com.example.daggertuts.Constants
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
//container for application dependencies
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
// @Provides //with this function provide string
// @Singleton //only one instance
// @Named("string1")
// fun provideString(): String = "This is a string from app Module"
@Provides
@Singleton
fun provideBaseUrl() = Constants.BASE_URL
@Provides
@Singleton
fun provideOkHttpClient() = if (BuildConfig.DEBUG) {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder()
.build()
} else OkHttpClient
.Builder()
.build()
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, BASE_URL: String): Retrofit =
Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(okHttpClient)
.build()
}
We have declared two dependencies. One that provides a string and the other that provides a retrofit instance. Note how the string dependency has been used inside the module.
We are going to use the retrofit dependency to fetch the latest bitcoin news headlines. To use these dependencies inside an activity we’ll be required to declare that activity with @AndroidEntryPoint annotation.
Suppose we want to access an instance of provideBaseUrl we would first annotate MainActivity with @AndroidEntryPoint and access the instance as follows.
@Inject
lateinit var baseUrl: String
Suppose we had two instances of string declared in the appModule. We would access the string separately by.
@Inject
@Named("string1")
lateinit var myString: String
@Inject
@Named("string2")
lateinit var myString2: String
In the appModule.kt
@Provides //with this function provide string
@Singleton //only one instance
@Named("string1")
fun provideString(): String = "This is a string from app Module"
@Provides //with this function provide string
@Singleton //only one instance
@Named("string2")
fun provideString(): String = "This is a second string from app Module"
Injecting Retrofit Dependency Inside a viewmodel
To inject a dependency inside a view model we annotate the viewmodel with @HiltViewModel and construct inject the dependency as follows.
package com.example.daggertuts.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.daggertuts.Constants
import com.example.daggertuts.model.Article
import javax.inject.Inject
import com.example.daggertuts.model.Result
import com.example.daggertuts.network.ArticleService
import dagger.hilt.android.lifecycle.HiltViewModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
@HiltViewModel
class NetworkViewModel @Inject constructor(retrofit: Retrofit): ViewModel() {
val data: MutableLiveData<List<Article>> by lazy {
MutableLiveData<List<Article>>()
}
init {
val articleService = retrofit.create(ArticleService::class.java)
val call: Call<Result> = articleService.listRepos("bitcoin",Constants.API_KEY)
call.enqueue(object : Callback<Result> {
override fun onResponse(call: Call<Result>, response: Response<Result>) {
val result = response.body()
val articles = result?.articles
data.value = articles
}
override fun onFailure(call: Call<Result>, t: Throwable) {
data.value = emptyList()
}
})
}
fun getData(): LiveData<List<Article>> {
return data
}
}
Using the viewmodel in our activity.
package com.example.daggertuts.view
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView
import com.example.daggertuts.DataAdapter
import com.example.daggertuts.R
import com.example.daggertuts.model.Article
import com.example.daggertuts.viewmodel.NetworkViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: NetworkViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Log.d("testString",myString)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
val adapter = DataAdapter(this)
recyclerView.adapter = adapter
// Create the observer which updates the UI.
val dataObserver = Observer<List<Article>> { article ->
if(article.isNotEmpty()){
adapter.setItems(article)
}else{
Toast.makeText(this,"No articles matching bitcoin topic",Toast.LENGTH_LONG).show()
}
}
// Observe the LiveData
viewModel.getData()
viewModel.data.observe(this, dataObserver)
}
}
Find the complete source code at github.com/mwaijohn/daggerhiltTutorial