Hold on ✋🏻 Before you Dagger or Hilt! try this Simple DI.
Learn pure Kotlin DI then migrate it to HILT or Dagger.
Originally Published on :
Featured in Kotlin-Weekly #228

ProAndroidDev

DroidCon

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,
- We create an ApiManager, which is a wrapper over
Retrofit instance
which provides all the API calling endpoints service classes. - Retrofit instance requires a
service class
interface andOkhttpClient
,adapters
, andBaseUrl
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.

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
}
}
Enjoying the Post?
a clap is much appreciated if you enjoyed. No sign up or cost associated :)
If you want to support my content do consider dropping a tip/coffee/donation💰
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.
Enjoying the Post?
a clap is much appreciated if you enjoyed. No sign up or cost associated :)
If you want to support my content do consider dropping a tip/coffee/donation💰
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

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! 👩💻
Read Next :

Enjoying the Post?
a clap is much appreciated if you enjoyed. No sign up or cost associated :)
If you want to support my content do consider dropping a tip/coffee/donation💰