In the past couple of years dependency injection became a hot topic among professional Android developers. That’s great because dependency injection is one of the best architectural patterns in object-oriented design and applications that use it are much easier to develop and maintain. However, many developers still struggle with its adoption. Well, no wonder.

Dependency injection is a very complex architectural pattern which, in addition, suffers from a terminological ambivalence. Furthermore, the term itself is often conflated with another closely related, but distinct concept: dependency injection framework. Usually these frameworks are very complex, so if you start your journey with them, you risk being overwhelmed by complexity.

Therefore, in my opinion, if you want to master dependency injection, then your best bet is to learn it from the very first theoretical principles. In this post I’ll explain the theoretical foundation of dependency injection and give several practical recommendations about its usage in Android applications.

Client service terminology:

First things first, so let’s talk about the terminology a bit.

When code in class A references class B, I say that class A depends on class B. I can also say that class A is a “client” of class B, or class B is a “service” for class A. This client-service terminology is very common in software development and has nothing to do with Android specific classes like Service.

Client Service

Please note that if code in class B also references class C, then class B is a client of class C, and class C is a service for class B. Therefore, the same class can be a client and a service at the same time when looked from different perspectives.

Dependency injection:

Dependency injection is an overloaded term and different developers can mean different things when using it. Therefore, let me explain what I mean when I use this term exactly.

The basic usage of “dependency injection” refers to the action of providing services to clients from “outside”. In other words, whenever clients need services they simply ask for them instead of trying to construct these services internally.

Dependency Injection (note the capitalization), on the other hand, is an architectural pattern (you can think of architectural patterns as design patterns on steroids). Dependency Injection architectural pattern includes the theory and the practices provide you with a guidance as to how to structure components in your application. Don’t worry if this sounds unclear right now. I will explain these concepts in great details in the following sections.

Note that the action of injecting dependencies is related to a single class, while Dependency Injection architectural pattern involves the entire application. In other words, these concepts “live” at vastly different levels of abstraction and, therefore, should be discussed separately.

Dependency injection fundamental techniques:

As I already said, the basic meaning of dependency injection is the action of providing (injecting) services to clients from “outside”.

In Java (which is similar to Kotlin and many other object-oriented languages), there are three ways of doing that: Constructor Injection, Method Injection and Field Injection. You can think of these three injection methods as the fundamental techniques of dependency injection.

Dependency Injection architectural pattern:

The main characteristic of correct implementation of Dependency Injection architectural pattern is segregation of application’s logic into two disjoint sets of classes:

  • Functional set. Classes in this set encapsulate core application’s functionality
  • Construction set. Classes in this set resolve dependencies and construct objects from the Functional set

Construction and Functional Sets

Note that I used the word “disjoint”, which is a rigorous mathematical definition. In order for the Functional and Construction sets of classes to be disjoint, the following conditions must be satisfied:

  1. Classes that encapsulate core application’s functionality mustn’t resolve dependencies or instantiate classes from Functional set
  2. Classes that resolve dependencies or instantiate classes from Functional set mustn’t encapsulate any of core application’s functionality

So, if you use dependency injection you’ll end up with two piles of classes. One pile contain just pure functionality, and another pile knows how to wire objects from the former pile together. This segregation of logic into Functional and Construction sets of classes is manifestation of Separation of Concerns principle at application level of abstraction.

Functional and Construction sets integration:

Though disjoint, Construction and Functional sets must be integrated together. At the end of a day, they complement each other and constitute a single application.

Construction and Functional Sets Integration

There are two main approaches to this integration:

  • Pure Dependency Injection (aka. Poor Man’s Dependency Injection)
  • Dependency injection frameworks

Let’s review each of them individually.

Pure Dependency Injection:

Pure Dependency Injection is a manual approach. If you choose to use Pure Dependency Injection, you are in charge of designing and implementing all the integration logic.

Pure Dependency Injection

The advantage of Pure Dependency Injection is that you have a complete control over implementation and do not depend on any third party libraries and tools. In many cases, this can also be the simplest approach because the flow of control is easy to follow and there is no “magic” involved.

The downside of Pure Dependency Injection is that it is very easy to get wrong. If the team is not skilled or not disciplined, an attempt at Pure Dependency Injection can turn your application into a mess. In addition, all the logic, including a considerable amount of boilerplate, will need to be written manually from scratch.

While it sounds kind of risky (and it is), don’t e quick to dismiss Pure Dependency Injection. It might become a good alternative to dependency injection frameworks if your project grows to the point when the overhead introduced by frameworks becomes an issue.

Dependency Injection frameworks:

Dependency injection frameworks are libraries that assist you with dependency injection architectural pattern. In essence, these frameworks are templates for your implementation of Construction set of classes and the logic that integrates it with Functional set.

Dependency Injection Framework

The template provided by the framework will usually promote many best practices which will make your life easier. In addition, if you use dependency injection framework you’ll need to make much fewer decisions. This means that you’ll have less space to make a mistake, and that’s a big plus if you implement dependency injection for the first time.

The integration template provided by dependency injection frameworks is usually built according to Convention over Configuration principle. These conventions can be annotation based, use XML documents, or other similar approaches. In addition, frameworks can resolve dependencies at different stages: some frameworks resolve dependencies at compile time, while others postpone the resolution to runtime.

All in all, if you use a mature dependency injection framework and you understand how it works, it can spare you a lot of effort and headache.

Dependency injection in Android:

The topic of Dependency Injection has been neglected for a very long time by Android official documentation and guidelines. Recently, however, it started to gain a lot of attention.

This is, undoubtedly, a welcome change and a sign of ongoing maturing of the platform. However, lack of good guidelines in this context causes a massive abuse of dependency injection frameworks, which is the opposite extreme that should be avoided. In the remaining of this article I’ll share with you several best practices related to Dependency Injection in Android that will help you avoid some of the more common mistakes.

Since the most popular choice for dependency injection in Android is Dagger 2 dependency injection framework, the code snippets that you’ll see will use its syntax. However, keep in mind that these best practices are universal and apply to any other framework you might want to use as well.

1. Use constructor injection by default:

Whenever possible, clients should ask for all their services through constructor arguments.

The advantages of constructor injection are:

  1. The code becomes more readable because all the dependencies are explicitly stated in constructors.
  2. You can’t forget to pass services to clients because the compiler will flag missing constructor arguments.
  3. Services injected into constructors can be finalized which is important in the context of multi-threaded code.
  4. Constructor arguments are the easiest to mock in unit tests.

So, the first rule is that you should always use constructor injection, unless there is very specific reasons not to do that.

2. Use field injection for Android top-level components:

There are two groups of “top-level” components in Android:

  • Components that Android framework instantiates for you: Application, Activity, Service, etc.
  • Fragment

Since you don’t instantiate the components from the first group yourself, you can’t use constructor injection with them. And even though you can instantiate Fragments, you should still do that using their “no-arguments” default constructor, so you won’t be passing services into them directly.

Since you can’t use constructor injection, you must fall back to either method injection or field injection. I recommend that you choose field injection in these cases because method injection won’t provide you any benefits, but can definitely make your code more difficult to read and understand.

3. Don’t use dependency injection framework to inject into custom View subclasses:

If you need any service to be injected into a subclass of View, and this View can be instantiated programmatically, use constructor injection. That’s simple.

However, even if the View is declared in XML, don’t resolve to dependency injection frameworks. Use regular Method Injection instead.

For example, if you need to inject ImageLoader into a custom View, then instead of this:

public class SomeClient extends LinearLayout {
    @Inject ImageLoader mImageLoader; 

    public SomeClient(Context context) {
        super(context);
        init();
    }

}

do this:

public class SomeClient extends LinearLayout {
    private ImageLoader mImageLoader;

    public SomeClient(Context context) {
        super(context);
        init();
    }

    public void setImageLoader(ImageLoader imageLoader) {
        mImageLoader = imageLoader;
    }

}

Advantages of using Method Injection in this case are:

  • Dependencies are visible at the API level.
  • Method Injection does not open door to Single Responsibility Principle violation.
  • No dependency on the framework.
  • Better performance.

Let’s unpack the above claims a bit.

First of all, dependencies injected with method injection will appear as part of clients’ public API and readers of the source code will immediately see them.

Secondly, there are not many use cases in which sub-classes of View need additional dependencies. However, by injecting even one single dependency using a framework you basically open a door for Single Responsibility Principle violation. In many cases it will be very tempting to compromise the quality of the design a bit and inject “that one additional object” into a custom View to implement a little hack.

These little compromises will accumulate and after some time your custom Views might turn into spaghetti of UI and business logic. And if you’re confident that it won’t happen to you, don’t forget that other less experienced developers can make this mistake and they won’t be able to realize their mistake.

The third advantage of using Method Injection with custom Views is that you don’t couple them to dependency injection framework. Just imagine that some time from now the framework needs to be replaced or completely removed. The fact that you will probably have tens of Activities and Fragments to start with already make such a refactoring a big project. You definitely don’t want to additionally handle tens or hundreds of custom Views.

The last advantage is performance.

One screen can contain one Activity, several Fragments and tens of custom Views. Bootstrapping this number of classes using dependency injection framework might degrade application’s performance. It is especially true for reflection based frameworks, but even Dagger carries some performance cost.

4. Don’t violate the Law of Demeter:

Law of Demeter, when applied in context of Dependency Injection, states that “a client should be injected with the exact services that it needs”.

In Android, Law of Demeter is commonly violated when you inject Context into clients to just obtain yet another object from it.

So, instead of this:

    public class SomeClient {
        private final SharedPreferences mSharedPreferences;

        public SomeClient(Context context) {
            mSharedPreferences = 
                    context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE);
        }
    }

do this:

    public class SomeClient {
        private final SharedPreferences mSharedPreferences;

        public SomeClient(SharedPreferences sharedPreferences) {
            mSharedPreferences = sharedPreferences;
        }
    }

Not violating the Law of Demeter gives you the following advantages:

  • Clients’ APIs reflect their real dependencies.
  • Clients can be unit tested as “black boxes” – no need to read their code to find out which classes should be mocked.
  • Unit testing is easier because you don’t need to mock chains of objects

As a starting point to obeying the Law of Demeter, just stop passing Context around when it’s not strictly required.

5. Differentiate between objects and data structures:

As Matt Carroll explained in this post , subclasses of Object class in Java can be divided into two sets: (object-oriented) objects and data structures.

Objects expose behavior and hide implementation details. For example, UserManager class could expose logIn() method.

Data structures expose data. For example, User class could expose getFirstName(), getLastName(), etc. methods.

Dependency Injection is applicable to objects, but not applicable to data structures. I would even go as far as saying that Construction set should not be aware of your application’s data structures at all. If you find yourself in position of referencing data structures in Construction set, then you’re probably already polluting Construction set with functional logic.

Conclusion:

In this article you learned what dependency injection is and understood the distinction between fundamental dependency injection techniques and dependency injection architectural pattern.

Now you know that dependency injection frameworks are basically templates for your implementation of Construction set. You also understand that frameworks aren’t strictly required because you can always use Pure Dependency Injection, even though it requires a fair bit of experience to get right. The provided list of best practices for dependency injection in Android will further make your adoption of dependency injection easier and help you avoid common pitfalls.

As I said at the beginning of this post, dependency injection is one of the most beneficial architectural patterns in object-oriented design and it’s a hot topic in Android community. If you learn it properly, you’ll advance your skill of writing decoupled and maintainable code. That’s both professionally and commercially rewarding.

Now, if you’ve read this far, you might also be interested in my best-selling course Dependency Injection in Android with Dagger 2. In this course I explain the theory of dependency injection and show how to implement Pure Dependency Injection in Android application form ground up. Then I refactor that application to use Dagger 2 dependency injection framework. Such a structured, step-by-step approach allows you to see what dependency injection really is and fully appreciate the role of dependency injection frameworks.

That’s all for now, thanks for reading.

Feel free to comment and ask questions below, and consider subscribing to our newsletter if you liked this post.

If you liked this post, then you'll surely like my courses

Subscribe for new posts!