Dependency injection in Android – 1.Overview

Tram Ho

1. Introduction

Dependency injection (DI) is a widely used programming technique and is well suited to developing Android applications. By following DI's principles, you also lay the groundwork for a good architecture for your application.

Implementing dependency injection will give you the following advantages:

  • Reusability of code
  • Ease of refactoring
  • Ease of testing (Ease of testing)

2. Fundamentals of dependency injection (Fundamentals)

Before understanding and working with Dependency Injection in Android, we should understand in general how it works.

2.1 What is Dependency injection?

Classes often require references to other classes. For example, a Car class may need to refer to an Engine class. These classes are called dependencies, and in this example the Car class depends on having an Engine instance to run. (You can understand that in your class you will have to use data from other objects, be it an attribute or wherever it appears)

There are three ways a class can get the object it needs:

  1. The class builds a dependency that it needs. In the example above Car will initialize the Engine itself.
  2. Get dependencies from elsewhere. Some Android APIs, like getContext and getSystemService () work this way.
  3. Provide dependencies as a parameter. The application can provide these dependencies once the classes have been built or passed them (dependent objects) into the functions that need each dependency. In the above example, the Car architecture will take Engine as a parameter.

The third thing is dependency injection! With this approach, you take the dependent objects and supply them instead of creating their instances directly in the dependent class.

Here is an example of NOT Dependency injection!

In the Car class, we have created an Engine object. This is not Dependency injection because the Car class has created its Engine object, what would this cause?

  1. Car and Engine work very closely together. An instance of Car uses only one engine type. And no other subclasses or implementations can be easily used. Here a car (Car) can only use one engine (Engine) can be just gasoline (Gas) or just electricity (Electric)
  2. Hard dependency makes testing more difficult. The car uses an implementation of the Engine, thus preventing testing of multiple cases corresponding to different Engine instances.

With Dependency, what will the code look like? Instead of each Car instance, it constructs its own Engine object when initializing, it takes an Engine object as a parameter in its constructor.

Main function depends on Car, but Car depends on Engine. In main we create an Engine object, and then use it to create a Car object. Benefits of the method based on this DI are:

  1. Reuse the Car. We can transfer different engines to the car. For example, you can define a new subclass of Engine like ElectricEngine that you want the car to use. If you use DI, it means that even if you change any kind of Engine engine, Car will always work.
  2. Easy to test for Car. We can deploy multiple test cases with different Engine with the same Car. For example, you can create a FakeEngine and configure it for different test cases.

There are 2 main ways to perform Dependency injection in Android:

  1. Constructor Injection. This is the way described above. We include the dependencies of a class in its constructor function.
  2. Field Injection (or Setter Injection). In Android, some classes such as Activity or Fragment are system initialized. So using constructor injection is not possible. With field injection, the dependencies are instantiated as soon as the class is created. The code will look like this:

For Java code (i.e. using setter):

2.2 Automatic dependency injection.

In the above example, we created, provided, and managed dependencies of classes without relying on a library. This is called manually handling Dependency, or manually. In the example of the Car (Car) above, it only depends on one object is the engine (Engine) only. In practice, the problem is that a class can depend on many objects and there are overlaps, and using DI manually is very boring and can be cumbersome, it has some disadvantages. The following:

  1. For large applications, getting all the dependencies and connecting them correctly requires a huge amount of code. Then in a multi-tier architecture, to create an object for the top layer you must provide all of the subordinate classes for it. For example, to create a vehicle, besides the engine, you need gearbox, chassis and other parts … and an engine needs cylinders and spark plugs, for example …
  2. When you cannot create dependencies before moving them in, for example, when you use lazy initializations or objects that follow the application follow. Then you need to write and maintain somewhere to manage the life cycle of the dependencies in memory.

There are libraries that solve these problems by automating the process of creating and providing dependencies. They are suitable for 2 types:

  1. Reflection-based solutions, which connect runtime dependencies.
  2. Static solutions create code to connect dependencies at compile time.

Dagger is a popular Dependency injection library for Java, Kotlin, and Android, maintained by Google. Dagger facilitates the use of DI in your application by creating and managing dependencies diagrams. It provides statict and compile time dependencies, addressing development and performance issues of Reflection-based solutions.

3. Alternatives for dependency injection.

An alternative to DI is to use Service Locator. Service Locator also improves the separation of classes from specific dependencies. We can create a Service Locator class, which creates and stores dependencies and then provides them on demand.

Java code:

Kotlin:

We can see DI is different from Service Locator in the way that dependencies are consumed. For Service Locator classes have control and require objects to be injected (injected); With DI the application has control and proactively injects the necessary objects.

Disadvantages of Service Locator:

  1. The set of necessary dependencies of Service Locator makes the code difficult to test. Because all test cases must be concentrated at the same Service Locator.
  2. Dependencies are coded in the implementation of classes, not existing APIs. As a result, it's hard to know what a class needs from the outside. If you use Service Locator, the dependencies built into it will either cause a failure during compilation or the test will fail because of incorrect references.
  3. Managing the life cycle of objects will be more difficult.

4. Choose the right technique for your application.

As mentioned above there will be a number of different techniques for managing our application's dependencies:

  • Manual DI makes sense only for a relatively small application. As the project gets bigger, the transfer of dependencies requires a lot of boilerplate code.
  • Service Locator starts out with relatively little code, but also has poor extensibility. Moreover testing will become difficult because they are based on a Singleton object.
  • Dagger is built to expand. It is well suited for building complex applications.

If our application is able to develop and expand later, you should include Dagger soon.

So the size of an application is small, medium or large. Google has given a relative way to correlate with the number of screens of the application. The number of screens from 1 to 3 is small. 4 to 7 screens are of medium size, while 8 or more screens are of large size. Note this is only relative, because screen size may also have to rely on many other factors.

Another problem is if your application you are focusing on developing external SDKs or libraries. You should consider keeping manual DI or Dagger. If you use Dagger, your library can also increase in size significantly.

5. Conclusion

Above, I have an overview of what is DI? And in Android, how is it specific? In short, Dependency Injection will give your application the following advantages:

  • Ability to reuse classes and separate dependencies: Easier to change a dependency. Code reuse has been improved by Inversion of Control, and classes no longer control how dependencies are created, instead they can work with any configuration.
  • Easy to refactor: Dependencies become testable parts like the API. It is possible to check when creating the object, at compile time, but not hidden.
  • Easy for Testing: Classes do not manage its dependencies. So when testing we can pass different dependencies and handle many test cases.
Share the news now

Source : Viblo