Dependency Injection In Android With Koin

Metehan Gokbel
4 min readMar 7, 2024

--

Koin

When discussing Dependency Injection in Android, Dagger-Hilt is often the first tool that comes to mind for many of us. Let me first briefly explain Dependency Injection(DI) — it can be defined as one of the five principles of S.O.L.I.D. It’s used for controlling and managing dependencies. As I mentioned earlier, thanks to Dagger-Hilt developed by Google, we can efficiently utilize this in Android applications. However, Koin, which we will discuss now, does this job quite well.

Let’s Begin With Koin Now

Koin is a DI framework for Kotlin developers, written in Kotlin. It is very lightweight. It supports the Kotlin DSL feature. It is one of the easy DI frameworks that doesn’t require a steep learning curve to get hold of it. The usage could be much easier compared to Dagger-Hilt. There are certain differences between them, and the biggest distinction is that Dagger-Hilt is a compile-time framework, whereas Koin is a run-time framework. This is why Dagger-Hilt is more widely used in the industry. However, considering that Koin is also quite prevalent in the industry, it should be learned.

Let’s Dive into Koin and see the example based on it.

We can write a simple application to fetch data from the JSONPlaceholder API.

Here is an example: https://jsonplaceholder.typicode.com/posts

Step 1: Add dependency in your build.gradle

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

// Koin
implementation "io.insert-koin:koin-android:$koin_version"
implementation "io.insert-koin:koin-androidx-navigation:$koin_version"
testImplementation "io.insert-koin:koin-test-junit4:$koin_version"

// Retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

Step 2: MyModelItem data class

We will create the model class for our response. This represents the response from the API

data class MyModelItem(
@SerializedName("body")
val body: String,
@SerializedName("id")
val id: Int,
@SerializedName("title")
val title: String,
@SerializedName("userId")
val userId: Int
)

Step 3: Define MyApi interface

Create a GET request and also create a suspend function.

interface MyApi {
@GET("posts")
suspend fun doNetworkCall(): Response<List<MyModelItem>>
}

Step 4: Create an AppModule file

Creating a singleton is much easier with just a scope function when compared to Dagger-Hilt. Inside the AppModule, we can directly retrieve a previously injected object using get() method. We can also use viewModel to create a ViewModel scope.

val appModule = module {
// singleton scope
single {
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MyApi::class.java)
}

single<MyRepository> {
MyRepositoryImpl(get())
}

viewModel {
MyViewModel(get())
}
}

In addition, if we use the factory scope, we can create a new instance every time we inject.

val appModule = module {
factory{
...
}
}

Step 5: Resource data class

We have created a Resource data class that allows us to return a status of any type. Now we can change our return in the Repository.

data class Resource<out T>(val status: Status, val data: T?, val message: String?) {

companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}

enum class Status {
SUCCESS,
ERROR,
LOADING
}

Step 6: MyRepository interface

interface MyRepository {
suspend fun doNetworkCall(): Resource<List<MyModelItem>>
}

Step 7: MyRepositoryImpl class

class MyRepositoryImpl(private val api: MyApi) : MyRepository{
override suspend fun doNetworkCall(): Resource<List<MyModelItem>> {
return try {
val response = api.doNetworkCall()
if(response.isSuccessful){
response.body()?.let {
return@let Resource.success(it)
} ?: Resource.error("Error", null)
}else{
Resource.error("Error", null)
}
}catch (e: Exception){
Resource.error("No Data!", null)
}
}
}

Step 8: MyViewModel class

We can fetch the data and observe it within the Activity using LiveData.

class MyViewModel(
private val repository: MyRepository
) : ViewModel() {

val myModelItem = MutableLiveData<Resource<List<MyModelItem>>>()

fun doNetworkCall(){
CoroutineScope(Dispatchers.IO).launch {
val resource = repository.doNetworkCall()
withContext(Dispatchers.Main){
resource.data?.let {
myModelItem.value = resource
}
}
}
}
}

Step 9: Create an Application class

We haven’t initialized our Koin yet. When our app is created, we want to initialize. On the one hand, we need to provide our module.

class MyApplication: Application(){
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(appModule)
}
}
}

Make sure not to forget to add your application class to the AndroidManifest.xml file.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>

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

</manifest>

Step 10: MainActivity

Now all we need to do is inject the view model and observe the data!

class MainActivity : AppCompatActivity(), AndroidScopeComponent {

override val scope: Scope by activityScope()
private val viewModel by viewModel<MyViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

viewModel.doNetworkCall()
observeLiveData()
}

private fun observeLiveData(){
viewModel.myModelItem.observe(this) { item ->
item?.let {items->
items.data?.forEach {item->
println("id: ${item.id}")
println("body: ${item.body}")
println("title: ${item.title}")
println("userId: ${item.userId}")
}
}
}
}
}

In summary, compared to Dagger-Hilt, it’s really easy to use. However, as I mentioned in the article, it also has some disadvantages. The decision is yours!

--

--