Dagger Series: Elegantly handle your Activities and Fragments by AndroidInjector

dagger-in-android.png?w=2924

The story

I recently see many people have trouble with Google Dagger 2 library. To be honest, I think those Googlers should be carefully considering there are so many newbies like me can’t understand what their document exactly wants to explain.

giphy.gif

  • Every time, when I try to read the documents.*

That’s the reason that I decided to write a sample by using Dagger2, which we can reuse it in the future as a boilerplate without rewriting too many codes for it. Let’s start creating an Android application sample.

Create a new project

It’s pure and straightforward. Open the Android Studio IDE and create a Kotlin project.
giphy.gif

Modify build.gradle file of the App module

  1. add kapt plugin
  2. add dagger gradle implementations
1
2
3
4
5
6
// dagger implementations
kapt 'com.google.dagger:dagger-compiler:2.24'
implementation 'com.google.dagger:dagger:2.24'

// add dagger-support if you are using support library
implementation 'com.google.dagger:dagger-android-support:2.24'

Create application-level DI graph

Let’s create an ApplicationComponent and an application-level injector to help create the entire application DI graph first. Keep this diagram in mind. We need to think of the scope of our injection graph carefully.

pub?w=960&h=720

I also create an Injectable interface to describe a particular fragment that could be injected by AndroidInjector from Dagger. We can see the detail later.

Implement HasAndroidInjector

You need to implement the HasAndroidInject interface to let Dagger know how to correctly inject your android components included Activity, Fragment, and Service these native components. We already create an application-level component in the previous section. It would make sense to implement HasAndroidInjector in your Application:

1
2
3
4
5
6
7
8
9
10
11
12
13
class SampleApplication: Application(), HasAndroidInjector {
@Inject lateinit var activityInjector:
DispatchingAndroidInjector<Any>

override fun androidInjector() = activityInjector

override fun onCreate() {
super.onCreate()

// init application component.
AppInjector.inject(this)
}
}

In this way, we can get rid of writing injections in every activity or fragment. You might notice why we use Any type instead of using the Activity type. That’s because Dagger changes the way to inject android components - check this out
. The reason to use Any type is to make it more generic.

Are you still with me now? Ok, it seems we have everything we need at this moment. Let’s run this sample application and see what would happen. Okay, it crashed.

giphy.gif
It doesn’t work !!

Crash logs:

1
2
3
4
5
6
Caused by: java.lang.IllegalArgumentException: No injector factory bound for Class at dagger.android.DispatchingAndroidInjector.inject(DispatchingAndroidInjector.java:136)
at dagger.android.AndroidInjection.inject(AndroidInjection.java:181)
at dagger.android.AndroidInjection.inject(AndroidInjection.java:55)
at idv.chauyan.androiddaggersample.di.AppInjector.handleActivity(AppInjector.kt:47)
at idv.chauyan.androiddaggersample.di.AppInjector.access$handleActivity(AppInjector.kt:13)
at idv.chauyan.androiddaggersample.di.AppInjector$inject$1.onActivityCreated(AppInjector.kt:27)

The log said Dagger can’t do injection without an injector factory. Then, what’s the injector factory? Well, you would need to create AndroidInjector. Factory for each Activity to let Dagger know how to create the Activity instance. To create an injector factory, you would need to create Subcomponent for each Activity, check my post about Subcomponent - here

Subcomponent for each Activity

All activities would need to create their own Subcomponents and Modules. Subcomponents are the factories that create AndroidInjectors for a concrete subtype of a core Android type such as Activity/Fragment. Modules are the resources that a specific Activity.class needs. A Module can descript how to create the entry of Activity.class and Subcomponent. Factory to the map of injector factories used by DispachAndroidInjector. Dagger-Android uses this entry to build an Activity Subcomponent and perform injections for Activity.class.

MainActivityComponent.kt

1
2
3
4
5
@Subcomponent
interface MainActivityComponent: AndroidInjector<MainActivity> {
@Subcomponent.Factory
interface Factory : AndroidInjector.Factory<MainActivity>
}

MainActivityModule.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Module (
subcomponents = [
// If you need to export the resources in
// a module to a subcomponent, use this way.
MainActivityComponent::class
]
)
abstract class MainActivityModule {
@Binds
@IntoMap
@ClassKey(MainActivity::class)
internal abstract fun bindMainActivityFactory(factory:
MainActivityComponent.Factory): AndroidInjector.Factory<*>
}

A tip to the subcomponents and modules if you don’t have any other resources needed to inject into your activities. You can just create a simple module with @ContributesAndroidInjector annotation. Here’s an example.

1
2
3
4
5
6
7
8
@Module
abstract class AltMainActivityModule {
@ContributesAndroidInjector (
/* modules to install into the subcomponent */
modules = []
)
internal abstract fun contributeYourAndroidInjector(): MainActivity
}

Dagger compiler would compile it into this way:

Generated code of AltMainActivityModule.kt by Dagger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Module(
subcomponents =
AltMainActivityModule_ContributeYourAndroidInjector$app_debug.MainActivitySubcomponent.class
)
public abstract class AltMainActivityModule_ContributeYourAndroidInjector$app_debug {
private AltMainActivityModule_ContributeYourAndroidInjector$app_debug() {}
@Binds
@IntoMap
@ClassKey(MainActivity.class)
abstract AndroidInjector.Factory<?> bindAndroidInjectorFactory(
MainActivitySubcomponent.Factory builder);
@Subcomponent
public interface MainActivitySubcomponent extends
AndroidInjector<MainActivity> {
@Subcomponent.Factory
interface Factory extends AndroidInjector.Factory<MainActivity> {}
}
}

You can see if we use the annotation @ContributeAndroidInjector for our module, Dagger almost does the same thing for us. After having these two foundational parts for our MainActivity, now we put our MainActivityModule into ApplicationComponent. After doing this, everything should be ok. You can see the App runs well:

1*dGWDFmW4OW84J5QDqQiXpQ.png
Activities with Dagger support

Ok, it’s time to verify if we are still on the correct road. Let’s create a Car model as a data model and try to inject it into MainActivity to see if it works as what we expect. I create a Model package and put this Car data class inside. It primarily provides a vehicle model name. You can put any vehicle model name here for testing injection. For convenience, I create a ResourceModule to provide the Car resource to ApplicationComponent. Here is the source code –

Car.kt

1
2
3
4
5
class Car {
// Let's pretend we have a nice car.
private var carModel: String = “mercedes-benz GLC 300
fun getModelName(): String = carModel
}

ResourceModule.kt

1
2
3
4
5
6
7
8
9
@Module (
subcomponents = [
MainActivityComponent::class
]
)
class ResourceModule {
@Provides
fun provideCar(): Car = Car()
}

We inject the Car into the MainActivity.

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : AppCompatActivity() {
// Inject car here, try to print the car model name out.
@Inject lateinit var car: Car
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// See if we can successfully show the car model name on the screen.
CarModule.text = car.getModelName()
}
}

Successfully show the car model name
Successfully show the car model name

Everything is so far so good, isn’t it? The next question would be: how to handle Fragments in the project? Let’s do it. To make it simple, I am not going to use the NavigationController by Google support library. Instead, I just create a simple Fragment and put it in another Activity. Our current sample App looks like this:

1*j1tUI7WOKTtwupNh7X6_Og.png
New Fragment introduced in the project

Let’s do the same thing we did on MainActivity for DetailActivity beforehand. Then, here comes a question you might be curious:

Question: Which scope does your fragment want to bind?

giphy.gif
Question about Fragment scope

It’s not just to define modules for our activities, and you have to decide where to install modules for your Fragments. You can make your Fragment component as a subcomponent of a Fragment component, an Activity component, or the Application component -  it depends on what bindings your Fragment requires. For example, your fragments might need resources from an Activity, and then this fragment’s life cycle may come with the Activity. It would make sense to bind with your ActivityComponent. You should think about what your fragment wants to do first. In this sample, it’s more convenient that we just bind a fragment with ActivityComponent. You might notice I create an interface called Injectable which is an easier way for us to annotate which fragment should be injected by Dagger:

In AppInjectr.kt We have this:

handleActivity function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private fun handleActivity(activity: Activity) {
// inject activity itself
AndroidInjection.inject(activity)

if (activity is FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentCreated(
fm: FragmentManager,
f: Fragment,
savedInstanceState: Bundle?
) {
if (f is Injectable) {
AndroidSupportInjection.inject(f)
}
}
}, true
)
}
}

We check if the fragment is an Injectable object. If this Fragment implements Injectable, we can use AndroidSupportInjection to handle the Fragment. In this way, we don’t have to write code in a Fragment’s onAttach function like this for each Fragment:

1
2
3
4
5
6
7
public class FragmentSample: Fragment() {
@Inject SomeDependency someDep;
override fun onAttach(context: Context?) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
}

How does AndroidSupportInjection.inject() work? Check the source code comments in AndroidSupportInjection.java:

1
2
3
4
5
6
7
8
9
10
Injects {@code fragment} if an associated {@link AndroidInjector} implementation can be found, otherwise throws an {@link IllegalArgumentException}. Uses the following algorithm to find the appropriate {@code AndroidInjector} to
use to inject {@code fragment}:

1. Walks the parent-fragment hierarchy to find the fragment that implements {@link HasAndroidInjector}, and if none do

2. Uses the {@code fragment}’s {@link Fragment#getActivity() activity} if it implements {@link HasAndroidInjector}, and if not

3. Uses the {@link android.app.Application} if it implements {@link HasAndroidInjector}.

If none of them implement {@link HasAndroidInjector}, a {@link IllegalArgumentException} is thrown. @throw IllegalArgumentException if no parent fragment, activity, or application implements {@link HasAndroidInjector}.

In our case, we’ve implemented the HasAndroidInjector at the Application level. According to the comment, it seems that we don’t need to implement the HasAndroidInjector in DetailActivity because this fragment eventually finds the Application has implemented the HasAndroidInjector. It sounds pretty reasonable. Doesn’t it? Ok, except this, what a Fragment needs just like what an Activity needs, it would needs Subcomponent and module for creating its own DI graph.

DetailFragmentComponent.kt

1
2
3
4
5
@Subcomponent
internal interface DetailFragmentComponent : AndroidInjector<DetailFragment> {
@Subcomponent.Factory
interface Factory : AndroidInjector.Factory<DetailFragment>
}

DetailFragmentModule.kt

1
2
3
4
5
6
7
8
9
10
@Module(subcomponents = [
DetailFragmentComponent::class
]
)
internal abstract class DetailFragmentModule {
@Binds
@IntoMap
@ClassKey(DetailFragment::class)
internal abstract fun bindDetailFragmentInjectorFactory(factory: DetailFragmentComponent.Factory): AndroidInjector.Factory<*>
}

We want to bind this fragment within DetailActivity, so we also need to update DetailActivityComponent itself.

1
2
3
4
5
6
7
8
9
10
@Subcomponent (
modules = [
// here we bind with DetailActivity's DI graph
DetailFragmentModule::class
]
)
interface DetailActivityComponent: AndroidInjector<DetailActivity> {
@Subcomponent.Factory
interface Factory : AndroidInjector.Factory<DetailActivity>
}

It looks very similar to Activity’s part. It’s time to verify if we correctly handle the Fragment Injection now. But, it shows errors.

We see this error:

1
Caused by: java.lang.IllegalArgumentException: No injector factory bound for Class<idv.chauyan.dagger_boilerplate.ui.detail.ui.detail.DetailFragment

It’s bizarre. How come we already implement the HasAndroidInjector and two essentials for this DetailFragment, but it still shows this error? If you use the breakpoint, you can see Dagger does find the SampleApplication, which implemented the HasAndroidInjector, but it obviously can’t find the injector factory from the map of SampleApplication.class & ApplicationComponent. In the DispatchAndroidInjector.java:

1
2
3
4
5
6
7
8
public boolean maybeInject(T instance) {
Provider<AndroidInjector.Factory<?>> factoryProvider =
injectorFactories.get(instance.getClass().getName());
if (factoryProvider == null) {
return false;
}
...
}

Although we have HasAndroidInjector in Application-level, we still need to implement that in Activity-level. In our case, we put the FragmentModule in ActivityComponent, which means Activity should take care of Fragment Injection stuff.

DetailActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
class DetailActivity : AppCompatActivity(), HasAndroidInjector {

@Inject lateinit var fragmentInjector:
DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> =
fragmentInjector

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
}
}

Ok, we still need to test it a little bit to avoid any beautiful fantasy. Let’s update the Car model and provide a car model detail string to show:

Car.kt

1
2
3
4
5
6
7
8
class Car {

private val carModel: String = "mercedes-benz GLC 300"
private val carModelDetail: String = "here is the car model detail information"

fun getModelName(): String = carModel
fun getModelDetail(): String = carModelDetail
}

Inject it into DetailFragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DetailFragment : Fragment(), Injectable {

@Inject lateinit var car: Car

companion object {
fun newInstance() = DetailFragment()
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.detail_fragment, container, false)
}

override fun onResume() {
super.onResume()

message.text = car.getModelDetail()
}
}


New Fragment introduced in the project

It works very well. Ok, you can find the whole project at HERE!. I am going to dive a little bit deeper into Dynamic-Feature-Module in the next post.

Happy coding. Enjoy.