Skip to content

Instantly share code, notes, and snippets.

@omkar-tenkale
Last active January 10, 2023 08:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save omkar-tenkale/2234053dc2c6d41b87cf77556a1cca0b to your computer and use it in GitHub Desktop.
Save omkar-tenkale/2234053dc2c6d41b87cf77556a1cca0b to your computer and use it in GitHub Desktop.

This is a draft, a note to myself to understand dagger

Understanding Dagger2

It all starts with a messy code, or its early realization if your're well prepared As projects starts to become large, they will have lots of classes referencing lots of dependencies

Retrofit instance, logger instance, DB instance, SharedPreferenceManager instance Generally we declare them singleton and move on, accessing them directly This is fine in the beginning but it hampers testability in long term as those singletons are hard to mock And what if you have lot more classes then these basic ones lets say you're following clean architecture

There it's not practical to make all use cases singleton, neither it is optimal, because it'll cause your app to instantiate and allocate memory for unused objects, some of which might be very heavy too

So anyways we need a better solution A solution that'll also help us with situations like mocking a rest api

Dependency injection comes to the rescue

Instead of class accessing the static singleton itself, why not provide it to it This is the manual DI we always do, eg passing context to a recyclerview adapter in it's constructor

But then we have to create the depencency when we instantiate the dependent class What if the dependency has it's own dependencies? Are we going to create them all?

No. We need a seperate entity which handles this for us. Someone whose job is to provide us the dependencies when we ask without requiring us to pass it's nested dependencies So the idea is, instead of copy pasting this dependency creation logic everywhere we need the dependencies, we centralize it to one place where all logic for resolving all dependencies is placed

Let's say DependencyProvider class with methods like getX() getY() etc Writing our own framework is time consuming and this problem is so common in big projects that people must have wrote similar solutions themselves

Dagger is one such solution and we'll use it instead of writing our own solution

So how does dagger work?

Let's forget that for a min and think from the first person perspective We're somewhere in a code and we need a dependency We've decided we won't use singletons so we must have a provider instance in our class which will provide us dependencies

on the provider instance we'll call getX() getY() etc

so the provider will look something like this

class Provider(){
	fun getX() = X()
	fun getY() = Y(getX())
}

Seems good, but then where does dagger come into play? If we write it all ourselves why we need dagger? and if we let dagger do all the stuff, how will we tell it the dependencies we require

The middle ground here is an interface and annotation processing Both us and dagger follow a contract

We define an interface and dagger at compile time will create an actual class for that interface with all methods implemented So instead of declaring the Provider class, we'll do

interface Provider{
	fun getX()
	fun getY()
}

Now when we compile our app, dagger will create the required classes from the interfaces. But how does dagger know which interfaces to process and which to skip To tell dagger this, we annotate the interface with @Component

So now dagger will create a DaggerProvider class for us at compile time Notice the 'Dagger' prefix, It's added to every component name

Now as Provider is an interface we can't instantite it so we instantiate DaggerProvider in our activity for example in it's oncreate

override fun onCreate(savedInstanceState: SavedInstanceState){
	provider = DaggerProvider.build()
}

Great! If we now call provider.getY() dagger will instantiate X first then Y with X as param and return us Y This way we can replce code like this in every activity

val y = Y(X())

with

DaggerProvider.build().getY()

Not much of a difference, but it's significant in later stages of development

Problems we face

This method helped us a lot to keep our code clean but we had to face some trouble with some special cases Dagger can instantiate normal classes which have no dependencies or those depepdencies are also added in graph but it can't provide us classes whose dependencies can't be provided, because dagger doesn't know how to create them or what to pass in their constructor eg a class that has no no-arg constructor can't be instantiated by dagger because dagger can't assume the default values

Modules to the rescue

Dagger module is a way to tell the dagger component how to provide a perticular dependency It's as easy as defining a method

	fun provideZ() = Z("some_special_string")

To let dagger know this is a provider method (there could be other utility methods in the same class too), we annotate this method with @Provides and we annotate the class with @Module and connect the module with the Provider component like this

@Modules(MyModule.class)
interface Provider

where MyModule contains the above method provideZ()

One more benefit of module is we can abstract the dependency, Instead of providing APImpl we can provide API as return type, then we can dynamically replace modules declared in Modules annotation to easily swap dependencies. Yay!

Going further

But what if the provides method need runtime arguments? We can use Component builder for that

@Modules(MyModule.class)
interface Provider(){
	public interface Builder{
		fun build(): Builder
		fun arg(m: M)
	}
}

Now instead of creating a provider @Provides method for M, we can pass M at runtime like

	val m = M()
	DaggerProvider.Builder().arg(m).build()

now M is added to the dependency graph and can be injected anywhere, meaning it's accessible even to other provides method as we initially intended

The memory problem

To prevent recreating the DaggerProvider instance again and again in activity oncreate, we moved it to the application class and accessed it from activity But the problem with having a single DaggerProvider class is it will manage hundreds' of dependencies from tens' of screens. This will cause the dependencies to never be garbage collected increasing the app's memory consumption and performance

Subcomponents to the rescue Using dagger subcomponents, we can manage the lifecycle of dependencies so they'll be GC'd when not required anymore

The dagger.android package

The android extension package provides helper classes to make it easy to use dagger on android


Evolution of dagger: Dagger Dagger Android - AndroidInjection#Inject() Dagger Android - @ContributesAndroidInjetor Dagger Android - DaggerActivity/Fragment.. Hilt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment