Dependency Injection in Modularized Android Applications

Dependency injection is one of the most beneficial architectural patterns in object-oriented design. It embodies separation of concerns at the highest level of abstraction and applications that use it become much more maintainable.

However, dependency injection isn’t simple to understand or use. That’s especially true in Android applications due to multitude of components with different lifecycles, and the framework suffering from unfortunate past architectural choices. And if all that isn’t enough, dependency injection becomes even more complicated the moment you try to modularize your Android project.

In this post I’ll discuss dependency injection in modularized Android projects and list some approaches that you can use to tame this beast.

Modularization of Android Applications

You modularize Android projects by dividing their source code between multiple Gradle modules. As far as I know, there are three main reasons for modularization:

  1. Organize and decouple independent pieces of functionality
  2. Improve project build times
  3. Define clear ownership boundaries between different teams

Even though there are just three main reasons for modularization, the exact approaches can vary greatly between different projects.

However, irrespective of the specific details, you’ll always want to follow some best practices related to modules inter-dependencies.

Modules inter-dependencies

Just like with regular classes, you should modularize your applications such that inter-dependencies between different modules form a direct acyclic graph (DAG). In essense, this means that there are no “back references” between modules and that “lower level” modules do not depend on “higher level” modules.

[Side note: if you’d like to implement Instant App feature, you’ll be required to reverse modules’ inter-dependencies. Therefore, Instant Apps are outside the scope of this post]

When modules’ inter-dependencies form DAG, you get the best of modularization: lower level modules serve as standalone library projects that higher level modules import and use. These modules will expose their functionality to the outside world through one or more public classes, while hiding the low level implementation details.

The top module in this “hierarchy”, the one that no other module depends upon, is the main module of your application. That’s the place where you should put all the construction and configuration logic. In other words, that’s where dependency injection lives.

At this point it’s important to be clear about the terminology and I’d like to make sure that we’re on the same page. When I say “dependency injection”, I mean the large scale architectural pattern, not just passing arguments into classes from outside. The later is insufficient and useless definition in my opinion. If you don’t fully understand this point (which isn’t simple), then you might want to read this article that debunks some common myths about dependency injection.

So, given that dependency injection is an architectural pattern that manifests separation of concerns at application level, construction concern should be confined to the main module in your application. Library modules should not depend on dependency injection framework that you’re using.

To get a bit more context on this topic, I recommend reading Mark Seemmann’s post titled DI-Friendly Library. Mark is probably one of the most authoritative experts in the world in context of dependency injection, so you can trust his judgement.

Activities in Library Modules

In most frameworks you’d just follow the aforementioned rules and automatically get clean and maintainable dependency injection in modularized project. However, Android framework introduces one big challenge in this regard: you don’t construct your Activities. Therefore, you can’t perform constructor injection into them.

When you’ve got just one module in the project, you address this issue by explicitly grabbing a reference to injector object (e.g. instance of Dagger 2 Component) inside your Activities and then performing field injection.

The same approach will work in multi-module project where all Activities confined to the main module. For example, that’s the case if you modularize your Android application in such a way that there are no dependencies on Android framework in library modules. In this situation, you’d simply instantiate classes from library modules in the main module using constructor injection.

However, the moment you put Activities into library modules, the picture complicates a lot. These Activities will probably require additional dependencies (e.g. AnalyticsManager), but, as we already discussed, you won’t be able to inject them into Activities’ constructors. That’s an intrinsic limitation of Android framework and there are several ways to work around it.

Before we discuss the workarounds, let me make sure that I’m clear with my general recommendation: avoid putting Activities into library modules as much as possible. If you absolutely must do it (e.g. to reuse the same UI in multiple applications), then treat these cases as exceptions and keep their number to a minimum.

Manual Method Injection Into Activities

Since you can’t use constructor injection with Activities, the next best bet would be to use method injection.

However, to use method injection you’ll need to have a reference to the Activity you want to inject into. To obtain this reference, you’ll need to register ActivityLifecycleCallbacks in your custom Application class:

public class MyApplication extends Application implements Application.ActivityLifecycleCallbacks {

    ... other functionality

    @Override
    public void onCreate() {
        super.onCreate();
        ... other code
        registerActivityLifecycleCallbacks(this);
    }

    ... other callbacks of ActivityLifecycleCallbacks

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (activity instanceof SomeActivity) {
            ((SomeActivity) activity).setSomeService(mSomeService);
        }
    }
}

There is a downside to this approach though: it will become a bit tedious if you’ll need to inject many services into many Activities.

Reflective Field Injection Into Activities

We don’t usually use reflection for dependency injection in Android due to performance considerations. However, if you do use reflection, then you can easily use automatic field injection even for Activities located in library modules.

Reflective injection would look like this (the only change from the previous scenario is implementation of onActivityCreated method):

public class MyApplication extends Application implements Application.ActivityLifecycleCallbacks {

    ... other functionality

    @Override
    public void onCreate() {
        super.onCreate();
        ... other code
        registerActivityLifecycleCallbacks(this);
    }

    ... other callbacks of ActivityLifecycleCallbacks

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        mInjector.inject(activity);
    }
}

Injecting Activities Using Service Locator Object

Service Locators should be avoided in general because their usage violates the Law of Demeter, but I find this specific use-case of working around Android’s limitation reasonably justified.

In some place inside your main module’s code, before you actually launch Activities from other modules, push the required services into ServiceLocator:

mServiceLocator.addService(mSomeService);

And then you can do the following:

public class MyApplication extends Application implements Application.ActivityLifecycleCallbacks {

    ... other functionality

    @Override
    public void onCreate() {
        super.onCreate();
        ... other code
        registerActivityLifecycleCallbacks(this);
    }

    ... other callbacks of ActivityLifecycleCallbacks

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (ServiceLocatorInitiated.class.isAssignableFrom(activity.getClass())) {
            ((ServiceLocatorInitiated) activity).initFromServiceLocator(mServiceLocator);
        }
    }
}

Each Activity that implements ServiceLocatorInitiated will receive a reference to ServiceLocator and will be able to grab whatever dependencies it needs from it.

The benefit of this approach over manual method injection is that it’s less tedious if you need to inject multiple Activities with multiple services. The downside is that it introduces a temporal coupling: you need to make sure that you push the services into ServiceLocator before they are being used by Activities. In addition, it will be hard to track the usages and exclude services from ServiceLocator once they aren’t required to be there anymore.

If you use ServiceLocator, be careful. That’s a special workaround (hack) and you should use it to the least extent possible. Do not make use of this object in other places where you can do proper dependency injection.

Dagger 2 in Library Modules

As I already said, your best bet is to keep dependency injection confined withing the main module. However, when it comes to Dagger 2, many developers seem to like the idea of extending Dagger’s reach into library modules.

Let me state right away that I don’t like this approach and find it very unfortunate. It’s especially true in cases when library modules don’t contain Activities at all and their functionality can be exposed using standard classes that support constructor injection.

However, since this entire post was inspired by the repeated questions of developers who took my online course about dependency injection and Dagger 2, I would like to discuss usage of Dagger in library modules in a bit more details.

No Official or Standard Pattern for Dagger 2 in Library Modules

First of all, there is no officially recommended pattern for using Dagger 2 in library modules that I’m aware of. The official documentation doesn’t mention multi-module Android projects at all, and the only example bundled with Dagger’s source is the notoriously ridiculous Thermosiphon. If you know about any other official resource – please let me know in the comments section.

Since there is no official documentation on this subject, let’s turn to community generated content. I googled the relevant keywords a bit and found a couple of simple examples that show conceptual approaches for using Dagger in library modules.

“Classical” Dagger 2 in Library Modules

This example is relatively recent. It shows how to use Dagger’s “dependent components” feature to support multi-module Android project. This additional example is even newer and shows a similar approach. I invite you to review these examples and try to understand the structure of dependency injection implemented in them. To be honest, even though I have quite a bit of experience with dependency injection and Dagger 2, it took me some time to grasp the general scheme.

AndroidInjection from dagger.android in Library Modules

The aforementioned examples use the “classical” Dagger constructs to extend its reach to library modules. However, as you might know, since Dagger 2.10 there is a new way of using Dagger in Android. This approach should be implemented using AndroidInjection and the associated classes from dagger.android package.

I tried to find an example that would demonstrate how to use dagger.android in multi-module application, but I couldn’t find any. As the last resort I decided to have a look at the source of Google IO 2018 official application. I reviewed this app in the past and found it inexcusably bad, especially given the fact that “the 2018 version of the app constitutes a comprehensive rewrite”. Nevertheless, I remembered that there are several modules in it and that the app uses dagger.android, so I thought that maybe there will be some utility in its source after all.

The results of this additional review were non-comforting, to say the least. Turns out that mobile module in this app uses dagger.android in a sinlge-module fashion, while tv module uses the “classical” Dagger approach. In essence, this is not multi-module application, but two different applications packaged in the same repository that use inconsistent coding practices.

The above discussion leads me to a conclusion that, as of today, there is no proper example of dagger.android usage in multi-module Android project. Again, if you happen to know about any – please let me know in the comments.

In addition, I think dagger.android is counter-productive even in case of single module projects, and I recommend staying away from it in general.

Summary of Dagger 2 in Library Modules

All in all, as I already said, I find the practice of extending Dagger’s reach into library modules a very unfortunate.

You can make it work, but this practice greatly increases the complexity of the code and introduces major barrier to entry for less experienced developers. Given the fact that there are no established best practices for this setup, you’ll be essentially experimenting on your own if you choose to use Dagger 2 in library modules.

In addition, in light of the recent addition of experimental support for incremental annotation processing into Dagger (finally!), I suspect that this practice can go counter-productive to one of the main goals of modularization which is to achieve faster build times (not sure about that though).

Fragments in Library Modules

If you have Fragments in library modules, you can use the same techniques as with Activities to inject into them. However, the nature of Fragments changes the techniques a bit because, unlike Activities, we can construct Fragments.

Manual Method Injection Into Fragments

The general form of manual method injection with Fragments looks like this:

@Override
public void onCreate() {
    super.onCreate();
    ... other code
    SomeFragment fragment;
    if (savedInstanceState == null) {
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        fragment = new SomeFragment();
        ft.add(R.id.frame_content, fragment).commit();
    } else {
        fragment = (SomeFragment) getSupportFragmentManager().findFragmentById(R.id.frame_content);
    }
    fragment.setSomeService(mSomeService);
}

Note that you should account for the fact that on configuration change or save & restore your Fragments will be destroyed, but Android will then re-create them for you. In this case you’ll need to just grab a reference to the new instance instead of instantiating the Fragment, and that’s what you see happening in the else clause of the conditional.

If you’re wondering why I use method injection instead of passing the dependencies into Fragment’s constructor, then it’s simple: even though we can construct Fragments, we shall use no-args constructors to do that because that’s what Android will use when re-creating our Fragments. IIRC, Android Studio will even give you a warning if you try to use non-default Fragment’s constructor.

Reflective Field Injection Into Fragments

If you’re using a reflective framework to inject dependencies, then you can use the following approach with Fragments:

@Override
public void onCreate() {
    super.onCreate();
    ... other code
    SomeFragment fragment;
    if (savedInstanceState == null) {
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        fragment = new SomeFragment();
        ft.add(R.id.frame_content, fragment).commit();
    } else {
        fragment = (SomeFragment) getSupportFragmentManager().findFragmentById(R.id.frame_content);
    }
    mInjector.inject(fragment);
}

Injecting Fragments Using Service Locator Object

Very similar to previous two methods:

@Override
public void onCreate() {
    super.onCreate();
    ... other code
    SomeFragment fragment;
    if (savedInstanceState == null) {
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        fragment = new SomeFragment();
        ft.add(R.id.frame_content, fragment).commit();
    } else {
        fragment = (SomeFragment) getSupportFragmentManager().findFragmentById(R.id.frame_content);
    }
    fragment.initFromServiceLocator(mServiceLocator);
}

Dagger 2 in Library Modules with Fragments

Virtually everything I wrote about Dagger 2, mutliple modules and Activities apply to Fragments as well. In my opinion, you should avoid introducing dependency injection frameworks into library modules at all costs.

The Future of Dependency Injection in Android

The fact that many core Android components don’t allow for proper constructor injection is a huge Android’s legacy architectural flaw. In my opinion, this was one of the worst architectural decisions ever made. Fortunately, after almost 10 years, Google finally acknowledged the mistake and decided to provide a workaround.

It’s included in Android Pie in the form of AppComponentFactory. This factory will finally allow us to instantiate Activities ourselves while doing proper constructor injection. Unfortunately, this is not something that can be back-ported using support library. Therefore, we’ll have to wait for several more years before this API will be safe to use in our projects (unless you want to support only Pie and onward).

On the positive note, there is already a ticket for supporting a similar use case for Fragments and Google said that they will implement that. Such a factory for Fragments can actually be implemented in support library. If this will happen, we’ll be able to use proper constructor injection with Fragments and the aforementioned workarounds won’t be required.

Therefore, in my opinion, we should all put a healthy pressure on Google devs to allow for a proper constructor injection into Fragments as soon as possible. The moment this happens, you’ll be able to simply expose library modules’ functionality through Fragments and use standard dependency injection techniques in the main module to instantiate them. That would be great in my estimation.

Conclusion

As you see, implementing proper dependency injection in a mutli-module Android project is not easy. However, given the prominence and importance of dependency injection architectural pattern, I recommend that you give this topic serious attention because it will have profound impact on maintainability of your application.

I sincerely recommend keeping library modules simple and having all Activities and Fragments in the main module. If that’s not an option, then use one of the several techniques described in this article. It’s in your best interests to avoid using dependency injection frameworks in library modules at all costs. That would greatly increase the complexity of your application and might incur additional not-so-evident penalties.

As usual, please leave your comments and questions below and consider subscribing to my posts if you liked this article.

Check out my premium

Android Development Courses

2 comments on "Dependency Injection in Modularized Android Applications"

    • Hello Igor,
      Just like with DI, there is fair bit of confusion about the meaning of Service Locator in the community. The issue here is overloaded terminology.
      There are service locator objects, and there is Service Locator architectural pattern. I’m not sure I can draw a perfectly consistent picture right now, but I’m thinking about this topic and will write a full article once I have the details worked out.
      The rule of thumb would be something like this: if most of your classes are initialized with proper constructor injection and don’t violate the Law of Demeter, and you only use service locator objects with classes like Activities and Fragments, you’re in DI domain. It’s only when you start passing service locators into constructors or have service locators statically accessible by many classes that DI degenerates into Service Locator architectural pattern.
      As I said, that’s my sense of the distinction between the two right now. It might be a bit unpolished.
      Therefore, as far as I understand from the examples I’ve seen, Koin DI framework isn’t Service Locator. At least not intrinsically. It’s only when DI is misused and LoD violated on the large scale that DI arch pattern degenerates into SL arch (anti) pattern.
      And that can happen with practically any framework (even with Dagger), which is why I think it’s so important to understand the theory of DI in addition to learning the “smart” tricks of DI frameworks.

      Reply

Leave a Comment