Modtion logo
Modtion
android

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

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 😊