Android Kotlin

Architect Your Android App Using Kotlin Koin Dependency Injection and Retrofit-MVVM

So, I finally get a chance to explore the new Kotlin koin dependency injection for Android and I had to say unlike Dagger2 were so many things to explore and it is hard to digest–at once. On the other hand, Koin is a light-weight dependency injection written purely in Kotlin, with no annotations to describe a module or components, no code generation, and no reflection at all.

While in the docs of koin they just making coffee using the MVP pattern, which most of the Android developers quit. So, in this quick article or tutorial, we’re going to see how we can use koin DI when using the MVVM (Model View-ViewModel) architecture.

This post was written with the aim to show how I made working together with Retrofit and koin dependency injection (DI) in an android app using MVVM architecture.

You can find the full source code of this project at the end of this article.

Table of contents

So, let’s get started.

The Project Intro we’ll Develop

Our project will be based on TheMovieDb API in order to display a list of popular movies. The application will simply hit a url to TheMovieDb server, retrieve the movie’s data, and populate them inside the GridView. Below you can find the demo.

add The Kotlin koin to your project

There are multiple dependencies available for koin. You can check out all the available dependencies here on this link. For the article, we only need to add the following dependencies in our app-level build.gralde which just one line of code.

// kotlin based dependency injection koin
implementation 'org.koin:koin-androidx-viewmodel:2.0.1'

The kotlin koin brings us this special dependency in order to inject our view models into activities and fragments.

Add the Retrofit Into Your Project

Now that koin dependency is done, we will have to retrieve a list of movies and for that, we need to add Retrofit into our project. We also need a Retorift converter as we will use retrofit2-kotlinx-serialization-converter to parse the Json data. Just add the following lines to the build.gradle file.

// Retrofit dependencies
implementation 'com.squareup.retrofit2:retrofit:2.6.2'

// kotlinx serialization converter for retrofit
implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.4.0'

 // Kotlinx Serialization
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.1'

We also need to add the plugin for kotlinx-serialization, add the following line at the top of your app-level build.gradle file.

apply plugin: 'kotlinx-serialization'

Wrote a couple of articles on Kotlinx Serialization in case you wanna read it.

Add The Picasso Into Your Project

Since we are fetching movies from TheMovieDb server and every item has the movie_poster url in it. So in order to download a movie poster and show inside the ImageView, we’re going to use Picasso library.

// Picasso dependency
implementation 'com.squareup.picasso:picasso:2.71828'

Add the Kotlin Coroutines Into Your Project

It’s not fair if we add the kotlinx-serialization and kotlin-koin in our project and not using the kotlin coroutines. I mean, it’s like Batman movie without Joker in it.

// Kotlin coroutines dependencies
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

Defining the Koin App Modules

All the dependencies that our application needs in the entire app we mostly declare them in the app-module (Or some people say core module). So, in our case, it’s only the Retrofit and Picasso instance on which the entire app dependent. We also make the app-modules dependencies singleton’s as we do not want to instantiate every time we need it.

So, let us create a new kotlin file named RetrofitModule.kt and declare retrofitModule related modules inside it.

val retrofitModule = module {    // 1
    
    single {   // 2
         okHttp()  // 3
    }

    single {     
         retrofit(Constants.BASE_URL)  // 4 
    }

    single { 
        get<Retrofit>().create(ServiceUtil::class.java)   // 5 
    }
}

private fun okHttp() = OkHttpClient.Builder()
    .build()

private fun retrofit(baseUrl: String) = Retrofit.Builder()
    .callFactory(OkHttpClient.Builder().build()) 
    .baseUrl(baseUrl)
    .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))  // 6
    .build()

As you can see the above code creates a new koin module, and here are some important entities.

  1. Creates a new koin module using the module {} DSL.
  2. The single{} DSL depicts a singleton instance that is unique across the application. Now everything inside single {} will be initialized only once and in our case the OkHttpClient instance.
  3. Get the OkHttpClient instance.
  4. Get the Retrofit instance and passing the BASE_URL.
  5. Requesting the Retrofit instance directly with the get method and generates the implementation of ServiceUtil class. We’ll see the implementation of the ServiceUtil class in the upcoming section.
  6. Returns a Converter.Factory which uses Kotlin serialization for deserialization of JSON content.

Next, create a new PicassoModule.kt file and declare a picassoModule in it.

val picassoModule = module {

        single {
              val downloader = okHttp3Downloader(get())  // 1
              picasso(
                   androidContext(), downloader  // 2
              )
       }
}

private fun okHttp3Downloader(client: OkHttpClient) = OkHttp3Downloader(client)

private fun picasso(context: Context, downloader: OkHttp3Downloader) = Picasso.Builder(context)
    .downloader(downloader)
    .build()

Here, what’s going in the above code:

  1. Creating a new instance of OkHttp3Downloader and resolving the OkHttpClient dependency using the get function.
  2. Here we use the androidContext to provide a context to our Picasso.Builder class using the koin library.

Starting Koin

Since we have our AppModules.Kt is ready. In Android apps, we mostly declare koin modules inside the Application class. Now let’s create a new Application class named MyCustomApp and include the following code snippet.

class MyCustomApp : Application() {
   
    override fun onCreate() {
         super.onCreate()
         startKoin {   // 1
              androidLogger(Level.DEBUG)  // 2
              androidContext([email protected])  // 3
              modules(listOf(retrofitModule, picassoModule))  // 4
         }
    }
}
  1. To start the koin we just need to call startkoin function and declare all the properties inside it.
  2. Enable android logging for object allocation and looking-up. By default, koin uses the EmptyLogger.
  3. Inject android application context into koin container. This will be useful when we need to get the application context st the time of creating our module dependencies.
  4. Including a list of dependencies which we wish to initialize with. In our case we adding our retrofit and picasso module.

Note: All the modules we’re adding inside the startKoin {} will not initialize eagerly instead koin will create instance lazily only when we need them.

Next, make the MyCustomApp visible in the AndroidManifest.xml file.

<application
        android:name=".MyCustomApp"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        .....
        .....

Setting Up The Retrofit Interface

The ServiceUtil interface is in charge of writing popular movies endpoint function for TheMovieDb server.

interface ServiceUtil {

    @GET(value = "popular")
    suspend fun popularMovies(@Path(value = "api_key") apiKey: String, @Path(value = "page") pageNumber: Int) : MovieCollection
}

As you can see we’re adding suspend keyword to popularMovies method so that it will not be a blocking method. The MovieCollection is a response class returned by the popularMovies method.

We’ll see the MovieCollection class implementation at the end of this article.

Setting Up Activity & ViewModel

First thing first, I think before setting up activity we need to cover the logic first. So, let us create a MovieViewModel class which will be our ViewModel. For now, we get the result inside the ViewModel and send the data to the activity.

class MovieViewModel
    (private val serviceUtil: ServiceUtil) : ViewModel() {   // 1

    private val _uiState = MutableLiveData<MovieDataState>()  // 2
    val uiState: LiveData<Event<MovieDataState>> = _uiState   

    init {
        retrieveMovies()   // 3
    }

    fun retrieveMovies() {
        viewModelScope.launch {   // 4
            runCatching {  //  5
                emitUiState(showProgress = true)
                serviceUtil.popularMovies(Constants.API_KEY)  // 6
            }.onSuccess {
                emitUiState(movies = Event(it))    // 7
            }.onFailure {
                emitUiState(error = Event(R.string.internet_failure_error))   //  8
            }
        }
    }

    private fun emitUiState(
        showProgress : Boolean = false,
        movies : Event<List<MovieCollection.Movie>>? = null,
        error : Event<Int>? = null
    ) {
         val dataState = MovieDataState(showProgress, movies, error)
         _uiState.value = dataState  
    }
}

data class MovieDataState(   // 9
    val showProgress : Boolean,
    val movies : Event<List<MovieCollection.Movie>>?,
    val error : Event<Int>?
) 

The above code does the following:

  1. The first thing we need an instance of ServiceUtil in order to get the movies result from the API. The instance will be passed when we create the ViewModel instance.
  2. Not exposing the MutableLiveData publicly instead of given access to only uiState LiveData which our activity observes.
  3. Calling out the retreiveMovies function when the MovieViewModel initializes.
  4. Launches a new coroutine because of the popularMovies is a suspend method.
  5. Calls the popularMovies method inside a runCatching block. The runCatching is nothing just a functional-way to handling the error and successful results. If you wanna read more about runCatching block check out this link.
  6. Make an Http request to TheMovieDb server and pass out a result to the onSuccess block if the API successfully returns the result.
  7. Emits the fetched movie data to observers. Another thing I want to point out is that you see we’re sending Event<List<Movie>> instead of simple List<Movie>. You can see the detailed discussion on why we need to use this approach when sending data from ViewModel to activity or fragment at this link.
  8. Emits the error data in the case of failure.
  9. UI model for the MovieActivity.

Note: We don’t need to cancel the coroutine in the onCleared method of ViewModel because the ViewModel class automatically cancels all the inner coroutine child of viewModelScope.

It is now time to create MovieActivity class and observes data which emits by MovieViewModel.

class MovieActivity : AppCompatActivity() {

    private val movieViewModel: MovieViewModel by viewModel()   // 1
    private val picasso: Picasso by inject()   //  2

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

        val adapter = MovieAdapter(picasso)  // 3
        recycler_view.apply {   // 4
            layoutManager = GridLayoutManager([email protected], 2)
            addItemDecoration(GridSpacingItemDecoration(2, 50, true))
            this.adapter = adapter
        }

        movieViewModel.uiState.observe(this, Observer {   // 5
            val dataState = it ?: [email protected]
            progress_bar.visibility = if (dataState.showProgress) View.VISIBLE else View.GONE
            if (dataState.movies != null && !dataState.movies.consumed)
                dataState.movies.consume()?.let { movies ->  
                    adapter.submitList(movies)    // 6
                }
           if(dataState.error != null && !dataState.error.consumed)
                dataState/error.consume()?.let { errorMessage ->
                       //  handle error 
                }
        })
    }
}
  1. Start by how MovieViewModel instance is injected into MovieActivity lazily with koin viewModel() delegate. Also, we don’t need to pass the factory instance in order to create a parameterized ViewModel. This all will be handled internally. The koin viewModel() delegation introduced in the koin-android-viewmodel. We’ll see how to write the MovieModule.kt for viewmodel in a couple of minutes.
  2. If you recall in the PicassoModule.kt file, we define the Picasso instance inside the koin single {}. So, here we simply injecting the picasso instance lazily.
  3. Creating the ListAdapter instance for RecyclerView. Also, passing the picasso instance in order to download the movie posters.
  4. Setting up the RecyclerView properties.
  5. Observing the uiState exposed by MovieViewModel and perform operations according to the observed events.
  6. Submit the list of movies fetched by our popularMovies API and display inside the GirdView.

Writing the Kotlin koin ViewModel module (MovieModule.Kt)

Let us take advantage of koin-android-viewmodel dependency and write the koin viewmodel{} for MovieViewModel class. Create a new kotlin file named MovieModule.kt.

val movieModule = module {  

    viewModel {   // 1
        MovieViewModel(get())  // 2
    }
}

A couple of things happen in the above code.

  1. The viewMode{} DSL helps us to declare the ViewModel component.
  2. If you remember that our MovieViewModel requires the instance of ServiceUtil. So, we’re resolving the dependency with the koin get method. An interesting thing here is that we don’t need to tell the koin library from where to get the ServiceUtil instance. It’ll automatically retrieve them from previously defined dependencies.

Note: The viewmodel component instance created with viewmode{} automatically destroys from memory when activity or fragment is destroyed.

Another thing we need to do is to register our movieModule with the previously registered modules. So, again open the MyCustomApp class and update it like the following.

class MyCustomApp : Application() {
       
       override fun onCreate() {
             ......
             startKoin {
                  androidLogger(Level.DEBUG)
                  androidContext([email protected])
                  modules(listOf(retrofitModule, picassoModule, movieModule))
             }
             .....
       }
}

ResponseBody class For Retrofit

Before we go and start writing the adapter class, I think it’s good if we focus on our data classes for the retrofit response.

@Serializable  
data class MovieCollection(@SerialName(value = "results") val movies: ArrayList<Movie>) {

    @Serializable
    data class Movie(
        @SerialName(value = "id") val id: Long, @SerialName(value = "poster_path") val posterUrl: String, @SerialName(
            value = "original_title"
        ) val name: String, @SerialName(
            value = "vote_average"
        ) val rating: Float, @SerialName(value = "overview") val description: String, @SerialName(
            value = "release_date"
        ) val releaseDate: String
    ) {
        var isFavorite: Boolean = false
    }
}

The @Serializable and @SerialName annotations present in the kotlinx-serialization library. We apply @Serializable to classes for the deserialization purpose and the @SerialName to override the name.

Setting Up the ListAdapter For The RecyclerView

So, we successfully downloaded popular movies JSON content from TheMovieDb server and parse the data inside the List<Movie>. Now let us create the MovieAdapter class.

class MainMovieAdapter(private val picasso: Picasso) :
    ListAdapter<MovieCollection.Movie, MainMovieViewHolder>(
        DIFF_CALLBACK
    ) {  

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        MainMovieViewHolder.create(parent, picasso)

    override fun onBindViewHolder(holder: MainMovieViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    companion object {

        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MovieCollection.Movie>() {

            override fun areItemsTheSame(
                oldItem: MovieCollection.Movie,
                newItem: MovieCollection.Movie
            ) = oldItem.id == newItem.id

            override fun areContentsTheSame(
                oldItem: MovieCollection.Movie,
                newItem: MovieCollection.Movie
            ) =
                oldItem == newItem
        }
    }
}

Check out the complete documentation on why you need to work with ListAdapter for RecyclerView when working with LiveData. See this link.


I think all the basic components for MVVM (Model View ViewModel) architecture when working with the kotlin koin library I presented. I know some classes remains but I think they are not important to discuss because it’s most basic. Any way you can find all the code of the above android app from koinMVVM repository on GitHub.

Thank you for being here and keep reading…

Write A Comment