Coroutines API call with Dagger Hilt
Home > coroutines-dagger-hilt

Coroutines API call with Dagger Hilt Example

In this example app as proof of concept to demonstrate about Kotlin Coroutines in Android. And it's capabilities for writing Unit test cases.


We will cover this in 2 parts:

Part 1: Coroutines API call with Dagger Hilt

Part 2: JUnit Tests for APIs with Dagger Hilt


Since, the stable version of Coroutines has released I really wanted to explore it's capabilities and went throgh the documentation but was searching for one of the simple example of the implemenation. Then I thoght let's code it on your own and make it simpler for others too.

So, I tried coding this in best possible simple code for coroutines API call with Dagger Hilt.




API Call Design


Create a New Project

I believe you already know how to create a new Project in Android Studio. Since, you are following this Dagger Hilt Example not a "How to create a first Android Application" 😀


Anyways, hit this "New Project" button once you see this Android Studio window

*Note: I'm using Arctic Fox version of Android Studio.

Once you create a dagger hilt example project.

Add Dependencies:

app's build.gradle file

    plugins {
        id 'dagger.hilt.android.plugin'
    }

    android {

      buildFeatures {
            viewBinding true
        }

     // Other code snippet
        ...

    }
    // Other code snippet
    ...

    dependencies {

    // other dependencies

    implementation "com.google.dagger:hilt-android:2.38.1"
    kapt "com.google.dagger:hilt-compiler:2.38.1"

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

    //Network library
    implementation "com.google.code.gson:gson:2.8.6"
    implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
    implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"
    implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    testImplementation "com.google.dagger:hilt-android-testing:2.38.1"
    kaptTest "com.google.dagger:hilt-android-compiler:2.38.1"

    testImplementation "org.mockito:mockito-core:3.11.2"
    testImplementation "org.robolectric:robolectric:4.5.1"
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1'

    }
              

Project's build.gradle file

dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:2.38.1"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
}

Let's Code:

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class HiltCoroutinesApplication: Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
*Don't forget to add this Application class in manifest file.
data class User(
    val avatar_url: String,
    var followers: Int,
    val id: Int,
    var location: String,
    var name: String,
    val public_repos: Int,
    val url: String
)
@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
    
    @Provides
    @Singleton
    fun providesOkHttp(): OkHttpClient = OkHttpClient.Builder().apply {
        if (BuildConfig.DEBUG) {
            addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
        }
    }.build()

    @Provides
    @Singleton
    fun providesRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder().apply {
        baseUrl("https://api.github.com")
        client(okHttpClient)
        addConverterFactory(MoshiConverterFactory.create())
    }.build()
}

Here, you will notice a `suspend` keyword is added. We need to add this `suspend` keyword just before the `fun` to call API endpoint with coroutines.

interface ApiServices {

    @GET("/users/github-username")
    suspend fun getUser(): User

    companion object Factory {
        fun create(retrofit: Retrofit): ApiServices = retrofit.create(ApiServices::class.java)
    }
}

Since, our API endpoint function is marked with `suspend` we have to make all other calling functions to be suspend as well.

interface ApiRepository {
    suspend fun getUser(): Flow< User >
}
class ApiRepositoryImpl(private val services: ApiServices) : ApiRepository {

    override suspend fun getUser(): Flow< User > = flow {
        emit(services.getUser())
    }
}
@InstallIn(SingletonComponent::class)
@Module
class AppRepoModule {
    @Provides
    fun providesApiRepository(apiServices: ApiServices): ApiRepository =
        ApiRepositoryImpl(apiServices)
}

@InstallIn(SingletonComponent::class)
@Module
class ApiServiceModule {
    @Provides
    fun providesApiServices(retrofit: Retrofit): ApiServices = ApiServices.create(retrofit)
}
sealed class UIState< T >(val data: T? = null, val error: String? = null) {
    class Success< T >(data: T?) : UIState< T >(data)
    class Loading< T > : UIState< T >()
    class Error< T >(message: String?) : UIState< T >(error = message)
}
@ExperimentalCoroutinesApi
@HiltViewModel
class HomeViewModel @Inject constructor(private val apiRepository: ApiRepository) : ViewModel() {

    private val _userFlow = MutableStateFlow< UIState < User > >(UIState.Loading())
    val userFlow: StateFlow< UIState < User > > = _userFlow

    fun loadUserData() {
        viewModelScope.launch {
            apiRepository.getUser()
                .catch { exception ->
                    _userFlow.value = UIState.Error(exception.message)
                }
                .collect {
                    _userFlow.value = UIState.Success(it)
                }
        }
    }
}

Now Design your XML Layout file as per what data from API you want to display and on what manner.


@AndroidEntryPoint
@ExperimentalCoroutinesApi
class HomeActivity : AppCompatActivity() {

    private lateinit var binding: ActivityHomeBinding

    private val homeViewModel: HomeViewModel? by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityHomeBinding.inflate(layoutInflater)
        setContentView(binding.root)
        observeFlowData()
        homeViewModel?.loadUserData()
    }

    private fun observeFlowData() {
        lifecycleScope.launchWhenStarted {
            homeViewModel?.userFlow?.collect { state ->
                when (state) {
                    is UIState.Loading -> {
                        showProgress()
                    }
                    is UIState.Success -> {
                        hideProgress()
                        state.data?.let { updateUI(it) } ?: showError()
                    }
                    is UIState.Error -> {
                        hideProgress()
                        showError()
                    }
                }
            }
        }
    }

    private fun showProgress() {
        binding.progressBar.visibility = View.VISIBLE
    }

    private fun hideProgress() {
        binding.progressBar.visibility = View.GONE
    }

    private fun showError() {
        binding.errorMessage.text = getString(R.string.error_message)
        binding.errorMessage.visibility = View.VISIBLE
    }

    private fun updateUI(user: User) {
        with(binding) {
            name.text = user.name
            location.text = user.location
            followers.text = getString(R.string.followers, user.followers)
            Glide.with(this@HomeActivity).load(user.avatar_url)
                .placeholder(R.drawable.ic_launcher_background)
                .into(image)
        }
    }
}

*Do not forget to add Internet permission in your manifest



Now, for JUnit Tests cases for APIs in Part 2

View Source