Android : Inject Retrofit with Dagger Hilt Dependency Injection to Android Compose

Dependency Injection
Dependency Injection (DI) is a software design pattern that can improve code quality and maintainability. why we need this?. Dependency injections are useful for developing loosely coupled programs, while also following the SOLID software design principles, as well as improving the reusability of code, while reducing the frequency of needing to change a class.
DI will make your code more testable because you can mock dependencies and increase flexibility by injecting code wherever you want. With a modular design, code with DI is well-defined and can be easily reused across different parts of the Application also can reduce coupling between class. Configuration can be simplified by centralizing the creation and management of dependencies, reducing boilerplate code, and simplifying configuration.DI will be very helpfull for large application with many dependency and logic.
Dagger Hilt
Dagger Hilt is a dependency injection (DI) framework for Android Development. It is built on top of the popular Dagger library and provides a more straightforward API for managing dependencies in Android apps. Dagger is first DI Framework for Android and have many things to do and complex to implement dagger to android project. Hilt make it simplier and reduce complex step of dagger.
You can learn more deep about dagger hilt at their documentation page.
Let's Start
We will use my previous code about retrofit in this this article. and combine it t with Koin DI.
Step 1
Implement retrofit to your project.
libs.versions.toml
[versions]
...
retrofitVersion = "2.9.0"
loggingInterceptor = "4.11.0"
ksp = "2.1.0-1.0.29"
hiltVersion = "2.51.1"
[libraries]
...
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofitVersion" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
hilt-android = { group = "com.google.dagger", name = "hilt-android" , version.ref = "hiltVersion"}
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler" , version.ref = "hiltVersion"}
[plugins]
...
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref ="hiltVersion" }
[bundles]
retrofit = [
"logging-interceptor",
"retrofit",
"converter-gson",
]
app/build.gradle.kts
plugins {
...
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
packaging {
resources {
excludes += "META-INF/gradle/incremental.annotation.processors"
}
}
}
dependencies {
...
// retrofit
implementation(libs.bundles.retrofit)
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
build.gradle.kts
plugins {
alias(libs.plugins.ksp) apply false
alias(libs.plugins.hilt) apply false
}
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
...
</manifest>
Don't forget to sync the Project.
Step 2
Create all file below.
ApiService.kt
import retrofit2.http.GET
import retrofit2.http.Query
interface ApiService {
@GET("latest")
suspend fun getLatest(
@Query("language") language: String = "us",
@Query("apikey") apikey: String = ""
): NewsResponse
}
NewsResponse.kt
data class NewsResponse(
val nextPage: String?,
val results: List<Article>?
)
Article.kt
data class Article(
val article_id: String?,
val title: String?,
val description: String?,
val content: String?,
val pubDate: String?,
val source_name: String?,
val image_url: String?
)
NewsState.kt
data class NewsState(
val isLoading: Boolean = false,
val isError: Boolean = false,
val news: List<Article> = emptyList()
)
NewsAction.kt
sealed interface NewsAction {
data object GetNews: NewsAction
}
NewsScreen.kt
@Composable
fun NewsScreen(viewModel: NewsViewModel) {
val state = viewModel.state
LaunchedEffect(true) {
viewModel.onAction(NewsAction.GetNews)
}
Scaffold(
modifier = Modifier
) { innerPadding ->
Box(
modifier = Modifier.padding(innerPadding)
.fillMaxSize(),
contentAlignment = Alignment.Center
){
if (state.isLoading) {
CircularProgressIndicator()
}
if (state.news.isNotEmpty()) {
LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
items(
count = state.news.size,
) { index ->
Log.d("NewsScreen", index.toString())
Card(modifier = Modifier.fillMaxWidth().padding(8.dp), elevation = CardDefaults.cardElevation(4.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = state.news[index].title.toString(), style = MaterialTheme.typography.headlineSmall)
state.news[index].description?.let {
Text(text = it, style = MaterialTheme.typography.bodyMedium)
}
}
}
}
}
}
}
}
}
All file above is same with previous retrofit basic article. From this, we will start implement dagger as dependency injection.
Step 3
Configure Network Module inside DI folder. Network module containing Retrofit Dependency ready to inject ApiService
into any code.
Use annotation @InstallIn(SingletonComponent::class)
and @Module
to declare this class as a module.
Use annotation @Provides
to declare class to be injected
di/NetworkModule.kt
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 java.util.concurrent.TimeUnit
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build()
}
@Provides
fun provideGson(): GsonConverterFactory =
GsonConverterFactory.create()
@Provides
fun provideApiService(
okHttpClient: OkHttpClient,
gson: GsonConverterFactory
) = Retrofit.Builder()
.baseUrl("https://newsdata.io/api/1/")
.client(okHttpClient)
.addConverterFactory(gson)
.build()
.create(ApiService::class.java)
}
Step 4
Create App.kt
and set annotation @HiltAndroidApp
. Set android:name=".App"
in AndroidManifest.
App.kt
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App: Application()
AndroidManifest.xml
<application
android:name=".App"
...
</application>
Step 5
How hilt works?
in NetworkModule
we create retrofit dependency that return ApiService. All method with @Provides
can be injected to any code.
For ViewModel we need to declare annotation @HiltViewModel
. You can inject with @Inject constructor
before parameter.
NewsViewModel.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
@HiltViewModel
class NewsViewModel @Inject constructor(
private val api: ApiService
) : ViewModel() {
var state by mutableStateOf(NewsState())
private set
fun onAction(action: NewsAction){
when(action){
NewsAction.GetNews -> fetchNews()
}
}
private fun fetchNews() {
viewModelScope.launch {
state = state.copy(
isLoading = true
)
try {
val response = api.getLatest()
state = state.copy(
news = response.results ?: emptyList()
)
} catch (e: Exception) {
e.printStackTrace()
state = state.copy(
isError = true
)
}
state = state.copy(
isLoading = false
)
}
}
}
Step 6
Set annotation @AndroidEntryPoint
to MainActivity to kicks off the code generation of the Hilt Component. Injection of Module and other Hilt Component will happen in super.onCreate()
. So, don't forget and this is mandatory.
Declare your ViewModel above your Screen class.
MainActivity.kt.kt
import dagger.hilt.android.AndroidEntryPoint
import androidx.activity.viewModels
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeRetrofitHiltTheme {
val viewModel: NewsViewModel by viewModels()
NewsScreen(viewModel)
}
}
}
}
Last Step
Happy Build & Run 😊