DEV Community

Dev Arena
Dev Arena

Posted on • Originally published at dev.to

Implementation of Hilt in Retrofit

In this article let's explore how easy to implement the Hilt DI and annotate the Network module to invoke the Countries API.

Components:

Components used in the Project.

  • Hilt
  • Coroutine
  • Retrofit
  • Gson
  • ViewBinding
  • Timber
  • ConstraintLayout
  • RecyclerView

API

Rest Countries - Get information about countries via a RESTful API.

Project Structure

Alt Text

Package Structure

Alt Text

Implementation

Follow the steps to implement the Hilt DI.

Add Dependencies

Project Level
  • Add the hilt-android-gradle-plugin into the classpath of the project level build.gradle.
buildscript {
    ext.kotlin_version = "1.5.20-M1"
    ext.gradle_version = "4.2.1"
    ext.hilt_version = '2.35'
    ext.core_version = '1.5.0'
    ext.appcompat_version = '1.3.0'
    ext.material_version = '1.3.0'
    ext.constraint_version = '2.0.4'
    ext.coroutines_version = '1.5.0'
    ext.lifecycle_version = '2.3.1'
    ext.activity_version = '1.2.3'
    ext.retrofit_version = '2.6.0'
    ext.httplogging_version = '3.12.0'
    ext.json_version = '2.8.6'
    ext.junit_version = '4.13.2'
    ext.extjunit_version = '1.1.2'
    ext.espressocore_version = '3.3.0'
    ext.timber_version = '4.7.1'

    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"

    }
}
Enter fullscreen mode Exit fullscreen mode
App Level
  • Add the dagger.hilt.android.plugin plugin into the app level build.gradle.
  • Enable the support for Java 8 features by adding compileOptions in the build.gradle.
  • Add the dagger:hilt-android implementation and dagger:hilt-compiler kapt into the build.gradle.
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'kotlin-android-extensions'
    id 'dagger.hilt.android.plugin'
}
android {
  compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding true
    }
}
dependencies {
    //  Kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

    // Core
    implementation "androidx.core:core-ktx:$core_version"
    implementation "androidx.appcompat:appcompat:$appcompat_version"
    implementation "androidx.activity:activity-ktx:$activity_version"

    // UI
    implementation "com.google.android.material:material:$material_version"
    implementation "androidx.constraintlayout:constraintlayout:$constraint_version"

    //  Lifecycle Components
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

    //  Logging
    implementation "com.jakewharton.timber:timber:$timber_version"

    // Hilt DI
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"

    //  Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

    //Retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
    implementation "com.squareup.okhttp3:logging-interceptor:$httplogging_version"
    implementation "com.google.code.gson:gson:$json_version"

    // Test
    testImplementation "junit:junit:$junit_version"
    androidTestImplementation "androidx.test.ext:junit:$extjunit_version"
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressocore_version"
}
Enter fullscreen mode Exit fullscreen mode

Manifest

  • Internet permission(uses-permission) is required for accessing API.
  • Application class should be referred in the android:name attribute.
 <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:name=".core.HiltApplication"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyHiltApplication">
        <activity android:name=".main.view.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
Enter fullscreen mode Exit fullscreen mode

Application

Create Application class and annotate with @HiltAndroidApp.

@HiltAndroidApp annotation generates Hilt code for Application class and creates parent container.

@HiltAndroidApp
class HiltApplication : Application()
Enter fullscreen mode Exit fullscreen mode

Activity

Create an Activity and setup the UI & livedata observers. Here, viewbinding is used for inflating the UI. And viewmodel is initialized using the by viewModels() ktx.

@AndroidEntryPoint annotation generates Hilt components for Android classes like Activity, Fragment etc., based on the lifecycle of the respective component.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val mainViewModel: MainViewModel by viewModels()

    @Inject
    lateinit var countryAdapter: CountryAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setUpUI()
        setUpObservers()
    }
}
Enter fullscreen mode Exit fullscreen mode

Adapter

Create the Adapter class for recyclerview with constructor injection.

@Inject annotation used in constructor, field and method injection where dependency is requested. Field injected cannot be private.

class CountryAdapter @Inject constructor() : RecyclerView.Adapter<CountryAdapter.ViewHolder>() {
    var countries: List<Country> = emptyList()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
        ViewHolder(
            CountryItemBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )

    override fun onBindViewHolder(holder: ViewHolder, position: Int) =
        holder.bind(countries[position])

    override fun getItemCount(): Int = countries.size

    inner class ViewHolder(private val binding: CountryItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(country: Country) {
            binding.apply {
                country.also { (name, capital) ->
                    nameTextview.text = name
                    capitalTextview.text = capital
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

ViewModel

Create ViewModel class and add method for loading the country list.

@HiltViewModel is annotated to enable viewmodel injection.

@HiltViewModel
class MainViewModel @Inject constructor(private val repository: Repository) : ViewModel() {
    private val countryLiveData = MutableLiveData<List<Country>?>()

    fun getCountry() = countryLiveData

    init {
        loadCountries()
    }
}
Enter fullscreen mode Exit fullscreen mode

Module

Create object class with various annotations like @Module, @InstallIn, @singleton and @Provides which provides dependencies. The Module class supplies the necessary dependent methods for the Network module.

@Module annotated class provides required instances as dependency for various classes.

Hilt Module annotated with @InstallIn specify the scope of the Module. Here, SingletonComponent::class generates singleton container for the class.

@Provides is annotated in the method and provides objects to inject when required.

@Singleton is annotated to create singleton instance of the dependency object and use it throughout the app.

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    private const val BASE_URL = "https://restcountries.eu/rest/v2/"

    @Singleton
    @Provides
    fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor()
        .apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

    @Singleton
    @Provides
    fun providesOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient =
        OkHttpClient
            .Builder()
            .addInterceptor(httpLoggingInterceptor)
            .build()

    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .build()

    @Singleton
    @Provides
    fun provideApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java)

    @Singleton
    @Provides
    fun providesRepository(apiService: ApiService) = Repository(apiService)
}
Enter fullscreen mode Exit fullscreen mode

Service

Define the Countries list API's in the Service.

interface ApiService {
    @GET("region/europe")
    suspend fun getCountries(): Response<Countries>
}
Enter fullscreen mode Exit fullscreen mode

Repository

Create an Repository class which returns the list of countries network response.

class Repository(private val apiService: ApiService) {
    suspend fun getCountries() = apiService.getCountries()
}
Enter fullscreen mode Exit fullscreen mode

Screenshot

Alt Text

Code

Project code is accessible in the GitHub Repo DevArena/HiltRetrofitApp

Happy Coding! 😀

Top comments (1)

Collapse
 
devrauluis profile image
Raul Luis

Great tutorial!
How do I add a second API to the project, I've tried adding it to the same module, but it doesn't work, I get a duplicate bindings error