Modtion logo
Modtion
android

Android : Inject Retrofit with Koin Dependency Injection to Android Compose

Android : Inject Retrofit with Koin 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.

Koin

Koin is a popular dependency injection (DI) framework for Kotlin, offering a modern and lightweight solution for managing your application’s dependencies with minimal boilerplate code. Koin is more simplier that dagger2,

You can learn more deep about koin 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"

koin = "3.5.3"

[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" }

koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }

[bundles]
retrofit = [
    "logging-interceptor",
    "retrofit",
    "converter-gson",
]
koin = [
    "koin-core",
    "koin-android",
    "koin-androidx-compose"
]

app/build.gradle.kts

dependencies {
    ...
    implementation(libs.bundles.retrofit)
    implementation(libs.bundles.koin)
}

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
}

All file above is same with previous retrofit basic article. From this, we will start implement koin as dependency injection.

Step 3

Configure Network Module and App Module inside DI folder.

declare every class to be injectable inside single{}.

di/NetworkModule.kt

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

val networkModule = module {

    single {
        OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
            .connectTimeout(120, TimeUnit.SECONDS)
            .readTimeout(120, TimeUnit.SECONDS)
            .build()
    }

    single {
        Retrofit.Builder()
            .baseUrl("https://newsdata.io/api/1/")
            .client(get())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

di/AppModule.kt.kt

import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

val appModule = module {
    viewModel{ NewsViewModel(get())}
}

Step 4

Create App.kt and register every modules inside startKoin. Set android:name=".App" in AndroidManifest.

App.kt

import android.app.Application
import com.modtion.composeretrofitkoin.di.appModule
import com.modtion.composeretrofitkoin.di.networkModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class App: Application() {

    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@App)
            modules(
                networkModule,
                appModule,
            )
        }
    }
}

AndroidManifest.xml

    <application
        android:name=".App"
        ...
    </application>

Step 5

How koin works? in NetworkModule we create retrofit dependency that return ApiService and in AppModule we register the NewsViewModel like this viewModel{ NewsViewModel(get())}. get() in viewmodel is the ApiService that will be injected into NewsViewModel.

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

class NewsViewModel(
    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

Koin make implementing ViewModel more easier.You don't need to define viewmodel anymore in Screen like below:

val newsViewModel = NewsViewModel()
NewsScreen(newsViewModel)

You just need to define koinViewModel() in Screen Parameter.

NewsScreen.kt

import org.koin.androidx.compose.koinViewModel

@Composable
fun NewsScreen(viewModel: NewsViewModel = koinViewModel()) {
    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 ->
                        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)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Last Step

Happy Build & Run 😊

Additional

If you have some classes you want to be injected into any code. Declare that class inside AppModule. For Example you have class MyRepository and MainRepositoryImpl.

class MainRepository()
class MainRepositoryImpl(val repository : MainRepository)

// just declare it
val appModule = module {
  singleOf(::MainRepository)
  singleOf(::MainRepositoryImpl)
}

These class ready to inject to any code. use by inject() to inject these class in Compose Screen, Activity or Fragment.

@Composable
fun NewsScreen() {

    val repo : MainRepositoryImpl by inject()
}