Hold on ✋🏻 Before you Dagger or Hilt! try this Simple DI.

dagger Oct 31, 2020

Learn pure Kotlin DI then migrate it to HILT or Dagger.



Originally Published on :

Kotlin Weekly #228

ProAndroidDev

Hold on ✋🏻 Before you Dagger or Hilt! try this Simple DI.
Dagger is one of the popular frameworks that has been known for Dependency Injection (DI) for Java and Android platforms. It’s in most of the Android projects whether small or large, developers tend…

DroidCon

Hold on! Before you Dagger or Hilt! try this Simple DI
By Chetan Garg Learn pure Kotlin DI then migrate it to HILT or Dagger. Dagger is one of the popular frameworks that has been known for Dependency Injection (DI) for Java and Android platforms. It’s in most of the Android projects whether small or large, developers tend to start off with the basic sc…



Dagger is one of the popular frameworks that has been known for Dependency Injection (DI) for Java and Android platforms. It’s in most of the Android projects whether small or large, developers tend to start off with the basic scaffold of Dagger just not to deal with the huge boilerplate or to avoid huge migration of adding it later to the project. So it has become a fact that it would be present in any Android project you work in, and you will be expected to know how to use it.

Many beginners and Intermediate developers tend to scare away from it, not because of DI as a concept they perfectly understand that, but from the confusion of all wiring and learning involved how Dagger resolving their dependencies. This problem also was understood by Google devs and Hilt was their answer to it. What Hilt basically does — it abstracts all the major wiring code related to AppComponent and now the user just needs to create modules to resolve your dependencies and with some hilt scoped annotation you can do no-brainer injections.

Still, there is some learning overhead involved, when you will encounter errors in HILT, they would easier to resolved when you have working knowledge of Dagger, which again could be a lot of learning and exploring for a Junior Dev need, which doesn’t have any idea about Dagger.

In Todays Article, I will explain a simpler way to Perform DI using Kotlin, this method you can use for

  • Creating DI framework for your testing environment
  • Adding DI to small or sample projects
  • When you’re in crises or can’t deal with adding Dagger and want to ship your product fast
  • Most importantly the way I do it here, you can migrate it to Dagger with no-brainer steps which I will share in the end too.
  • If you’re new to learning Dagger, it will act as a bridge to understand how atomic dependencies are resolved.

So let's get started…


Basic MVVM Architecture 🏗

According to Google guidelines we have basic MVVM architecture explained by the below diagram.

I will show how to resolve remote data sources and you will get the gist how we are handling DI, and you can use it will all other components.

My approach mainly focuses on creating a two-class named Resolver and an Injector, as the name suggests Resolver is the class that has all the code related to providing/constructing atomic dependencies through functions, and Injector is the class which calls these atomic function to construct compounded dependencies and cache them if required.

The flow of Control :

Activity → View-Model → Repository → Remote Data Source → API Call

API Management: Retrofit 🧳

Retrofit is industry standard for REST calling in Android, going down to implementation,

  1. We create an ApiManager, which is a wrapper over Retrofit instance which provides all the API calling endpoints service classes.
  2. Retrofit instance requires a service class interface and OkhttpClient, adapters, and BaseUrl
  3. OK-HTTP client can have other dependencies but for the current project I’m making it standalone

API Manager Sample

class ApiManager(private val retrofit : Retrofit) {
    val movieService : MovieService by lazy {
        retrofit.create(MovieService::class.java)
    }
}



Network Resolver

Contains all the atomic dependencies provided by functions whose primary focus is networking only.

object NetworkResolver {
    // step 1, you first need to resolve Api Manager
    fun provideApiManager() = ApiManager(??)
    
    // step 2 Api manager requires retrofit Instance, provide it 
    // from parameter dependency
    fun provideApiManager(retrofit: Retrofit) = ApiManager(retrofit)
    
    // step 3 We haven't created a resolving function that provides
    // a retrofit instance, hence
    fun provideRetrofitClient() : Retrofit{
       return Retrofit.Builder()
          .baseUrl(??)
          .addConverterFactory(GsonConverterFactory.create()) 
          .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
          .client(??)
          .build()
    }
    
    // step 4 dependencies for retrofit can be passed from parameter
    fun provideRetrofitClient(baseURl:String, okHttpClient : OkHttpClient) : Retrofit{
       return Retrofit.Builder()
          .baseUrl(baseURL)
          .addConverterFactory(GsonConverterFactory.create()) 
          .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
          .client(okHttpClient)
          .build()
    }

    // step 5, similarly add okhttp and baseUrl
    fun provideBaseURl():String = "http://www.someURL.com"
    fun provideOkHttpClient() : OkHttpClient = OkHttpClient
            .Builder()
            .build()
}

Remote DataSource Sample 💻

Remote data source is a wrapper over API service class object which is resolved by the API manager.

class MovieSource(val movieService : MovieService) {
    fun getMovie(query : String): Single<MovieDetailResponse> {
      return  movieService.getMovie(query)
    }
}

ApplicationResolver

It contains all the atomic dependencies provided by functions that are singleton for the whole Application.

object AppResolver {
    // step 1, we provide movie data source
    fun provideMovieDataSource()
          = MovieSource(??, ??)
    // step 2 movie data source depends on movie service, 
    // we know we have already created a resolver function for
    // API Manager inside NetworkResolver,
    // so we won't create provider function for it again
    fun provideMovieDataSource(apiManager : ApiManager)
            = MovieSource(apiManager.movieService)
}

Repository Sample 🏫

In this sample, we are simply forwarding the API calls from data source without caching, mapping, etc.

class MovieRepository(private val movieSource : MovieSource) {
    fun getMovieDetail(movieDetailRequest: MovieDetailRequest): Single<MovieDetailResponse> {
       return movieSource.getMovie(movieDetailRequest.movieId)
    }
}

If you want to perform caching in the repository you can check my example from here.

AndroidBites | 5 Steps to LRU Cache in Repository.
cache in repository android | cache data in ViewModel | restore data in android | save network call | cache network response | LRU cache in android | cache in android | Easy android cache | save data in repository | best practice Repository pattern | save data android | cache using LinkedHashmap

ApplicationResolver

continuing from app resolver…

object AppResolver {
    ...previous functions

    // step 1, create provider for movie respository
    fun provideMovieRepository()
           = MovieRepository(??)
    // step 2, movie repository has been already resolved above 
    fun provideMovieRepository(movieSource: MovieSource)
          = MovieRepository(movieSource)
}

View-model Sample 🧚‍♀️

It’s Jetpacks ViewModel, which is responsible for the mapping of data with respective views and exposing it.

class MovieDetailViewModel(val movieRespository : MovieRepository) : ViewModel() {

    fun getMovieDetail(movieId : String): Single<MovieDetailResponse> {
        ...// some stuff
    }
}



ViewModel Resolver

this provider resolver View Model of the system.

object ViewModelResolver {
    // step 1, we need a viewmodel which is creted by ViewmodelProviders 
    fun provideDetailViewModel():MovieDetailViewModel = ViewModelProviders
          .of(??, ??)
          .get(MovieDetailViewModel::class.java)

    // step 2, add dependencies of ViewmodelProviders, 
    // we have used added fragment Activty, so that we can support
    // multiple activities not just MovieDetail activity
    fun provideDetailViewModel(
            factory : MovieDetailViewModelFactory, 
            activity : FragmentActivity
    ) :MovieDetailViewModel = ViewModelProviders
            .of(activity, factory)
            .get(MovieDetailViewModel::class.java)
  
    // step 3, resolver for viewmodel factory?
    fun provideDetailViewModelFactory() = 
                MovieDetailViewModelFactory(??)
    
    // step 4, repository has already been resolved in App resolver
    fun provideDetailViewModelFactory(movieRepository : MovieRepository) 
        = MovieDetailViewModelFactory(movieRepository)
}

Injector 💉

Till now we have created all the required resolvers that we need to provide though-out the app, but as already said, we won't be calling Resolver class in dependency required components, as they are just functions, every time we call them, a new object would be created, and we don’t want to keep recreating the expensive objects again, and since the resolver provides atomic dependency, compound object initializing logic would be exposed in components which will violate the DI rule.

Hence the Injector class comes in the role, focus on some points

  • We will keep those things private which we don’t want to expose for injecting
  • Things that need to be created again needs to be a functions
  • Things that are required to be constructed once, needed to be stored in a variable
  • You can optimize using lazy loading so that not all dependencies get resolved when they are not needed.
object Injector {

    // step 7, okhttp and baseURL is standalone
    private val okHttp = NetworkResolver.provideOkHttpClient()
    private val baseURL = NetworkResolver.provideBaseURL()    
    
    // step 6, retrofit needs okhttp object and baseURL
    private val retrofit = NetworkResolver.provideRetrofitClient(
          baseURl,
          okHttp
    )

    // step 5, api manager requires retrofit instance 
    private val apiManager = NetworkResolver.provideApiManager(retrofit)

    // step 4, movie source requires apimanager 
    private val movieSource by lazy {
       AppResolver.provideMovieDataSource(apiManager)
    }
  
    // step 3, factory requires movie repository
    private val movieRepository by lazy {
       AppResolver.provideMovieRepository(movieSource)
    }
    
    // step 2, Detail ViewModel requires factory 
    // things are lazy so you don't have create objects before it's getting
    // used in the system and lazy also cache the result so acting as singleton too.
    private val detailViewModelFactory by lazy {
       ViewModelResolver.provideDetailViewModelFactory(movieRepository)
    }
    
    // step 1, we need to create a viewmodel which would be again 
    // and again called from viewmodel factory, hence we create it as a function
    fun detailViewModel(activity: AppCompatActivity) : MovieDetailViewModel {
      return ViewModelResolver.provideDetailViewModel(
                detailViewModelFactory, activity
            )
    }
  
}

Activity Sample 📱

Consuming the dependencies.

class MovieDetailActivity : AppCompatActivity() {
  private laterinit var viewmodel: MovieViewModel
  override fun onCreate(savedInstanceState: Bundle?) {
      ...
      viewmodel = Injector.detailViewModel(this)
      ...// viewModle is ready to be used...
  }
}

Now you can use Injector globally to do the injection.

That’s all for the code, you’ve successfully implemented a DI which can be considered good for smaller projects.



Some advance tips. 🤑

Scoped Injectors {💉}

We can observe injector class becoming a God in no time, so you can break it down into smaller objects which includes the injections related to a particular feature.

// you can remove `detailViewModel()` from injector and add it into a scoped e
// DI packag inside of your feature.
object MovieDetailInjector {
    fun detailViewModel(activity: AppCompatActivity) : MovieDetailViewModel {
      return ViewModelResolver.provideDetailViewModel(
                Injector.detailViewModelFactory, activity
            )
    }
}

Multi-Module Projects 🏫

Core modules need to have your app-level Injector and UI and Domain modules need to have their own DIs, example

If there is a search movie module

// your core injectores need to have public all public fields

------- Domain Injection -------
// in searchmovie Domain module build.gradle
dependencies {
    implementation project(":core")
}

class SearchMovieApiManager(private val retrofit : Retrofit) {
    val movieSearchService : MovieSearchService by lazy {
        retrofit.create(MovieSearchService::class.java)
    }
}

object SearchMovieResolver {
    fun provideSearchMoviewApiManager(retrofit: Retrofit) 
          = SearchMovieApiManager(retrofit)
}

// create injector side domain layer
object SearchDomainInjector {

    // step 3, api manager requires retrofit instance 
    private val apiManager = SearchMovieResolver
          .provideSearchMoviewApiManager(Injector.retrofit)

    // step 2, movie source requires apimanager 
    private val movieSearchSource by lazy {
       AppResolver.provideMovieDataSource(apiManager)
    }
  
    // step 1, factory requires movie search repository
    val movieSearchRepository by lazy {
       AppResolver.provideMovieRepository(movieSearchSource)
    }
    
}


------- UI Injection -------
// in searchmovie module build.gradle
dependencies {
    implementation project(":searchDomain")
}

object SearchMovieUiResolver {
    fun provideSearchMovieVmFactory(movieRepository : MovieSearchRepository) 
        = MovieSearchViewModelFactory(movieRepository)
}

// in your seach module you need to create scoped injection discussed above
object SearchInjector {
    fun searchViewModel(activity: AppCompatActivity) : MovieDetailViewModel {
      return ViewModelResolver.provideSearchViewModel(
                SearchMovieUiResolver.searchViewModelFactory, activity
            )
    }
}

Easy Migrating to Dagger or Hilt. 🤩

If someone familiar with Dagger goes through the resolver would be able to recognize that resolvers are very much similar to the Modules in Dagger and Injector is something that Dagger generates internally.

Dagger Migration. 🗡

Since resolvers are modules, just annotate them, for instance

@Module
object NetworkResolver {
    @Provides
    @Singleton
    fun provideApiManager(retrofit: Retrofit) = ApiManager(retrofit)
    ....
}

After annotating your resolvers you can add them to you AppComponent. In the end, it will look something like.

@Singleton
@Component(
    modules = arrayOf(
        AndroidSupportInjectionModule::class,
        //..add here
        NetworkResolver::class,
        AppResolver::class,
        ViewModelResolver::class
    )
)
interface AppComponent : AndroidInjector<AirtelApplication> {

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(application: SampleApp): Builder

        fun build(): AppComponent
    }
}

After completing your migration you can directly go and add Inject annotation in your injection points.

class MovieDetailActivity : AppCompatActivity() {
  @Inject
  internal laterinit var viewmodel: MovieViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
      ...
      // viewmodel = Injector.detailViewModel(this) remove this line
      ...// viewModle is ready to be used...
  }
}

That’s all to it. You will be done with Dagger.



Hilt Migration 🔪

Along with module annotation, you need to add appropriate Hilt Component annotation.

@Module
@InstallIn(ApplicationComponent::class)
object NetworkResolver {
    @Provides
    @Singleton
    fun provideApiManager(retrofit: Retrofit) = ApiManager(retrofit)
    ....
}

Use below table to figure out proper component scope

HILT components

From the above sampleNetworkResolver -> ApplicationComponent
AppResolver -> ApplicationComponent
ViewModelResolver -> ActivityRetainedComponent

After completing your migration you can directly go and add Inject annotation in your injection points.

@AndroidEntryPoint
class MovieDetailActivity : AppCompatActivity() {
  @Inject
  internal laterinit var viewmodel: MovieViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
      ...
      // viewmodel = Injector.detailViewModel(this) remove this line
      ...// viewModle is ready to be used...
  }
}

And You are done with HILT.

Conclusion 💆🏻‍♀️

From the above sample, we have successfully implemented DI (only using Kotlin) which is very much easy to migrate to Dagger|Hilt.

Until next time. Happy Hacking! 👩‍💻


Understanding Maps 🗺 in the Kotlin.
Curious how Kotlin resolve the Java platform’s map? This Post is given `Silver award` in Reddit at r/android_devs,thanks community for supporting my blogs and it really motivating me! 🙇‍♂️ > AndroidBites | Java ☕️ Maps 🗺 on the Kotlin. [https://www.reddit.com/r/android_devs/comments/jle556/androidbites_java_maps_on_the_kotlin/?ref_source=embed&ref=share…

Enjoy the article?

a clap is much appreciated if you enjoyed. No sign up or cost associated :)




Chetan gupta

Hi there! call me Ch8n, I'm a mobile technology enthusiast! love Android #kotlinAlltheWay, CodingMantra: #cleanCoder #TDD #SOLID #designpatterns

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.