Dagger 2

Time:2020-10-15

Dagger dagger, another blade of the famous square company (yes! There is also a butter knife, called butterknife; hence the title dagger 2.

Dagger2, originated from dagger, is an open source library based on Java annotation, which can complete dependency injection completely in the compilation phase. It is mainly used for decoupling between modules and improving the robustness and maintainability of the code. Dagger2 automatically generates java code by using java annotations through apt in the compilation phase, and then combines with handwritten code to automatically help us complete the dependency injection work.

At first, square was inspired by Guice to develop dagger, but Dagger’s semi-static and semi run-time framework still has some performance problems (although dependency injection is completely static, its directed acyclic graph or reflection based generation is not the optimal solution for large server-side applications or Android Applications). So Google engineer fork took the dagger project and transformed it. So dagger2 evolved into the dagger2 we are going to discuss today, so dagger2 is actually a high configuration version of dagger.

Dependency injection

So what is dependency injection? Before we explain this concept, let’s look at a little bit of code:

public class Car{

    private Engine engine;
    
    public Car(){
        engine = new Engine();
    }
}

In this java code, the car class holds a reference to the engine instance, which we call car class and has a dependency on the engine class. Dependency injection refers to the implementation of dependencies between classes by injection. The following are three common ways of dependency injection

1. Construction injection: through the constructor to pass parameters to the dependent member variables, so as to achieve injection.

public class Car{

    private Engine engine;
    
    public Car(Engine engine){
        this.engine = engine;
    }
}

2. Interface injection: implement the interface method, and also implement the injection in the way of transmitting parameters.

public interface Injection<T>{

    void inject(T t);
}

public class Car implements Injection<Engine>{

    private Engine engine;
    
    public Car(){}

    public void inject(Engine engine){
        this.engine = engine;
    }

}

3. Annotation injection: Java annotations are used to generate code for injection at compile time or reflection at run time.

public class Car{

    @Inject
    Engine engine;
    
    public Car(){}
}

The first two injection methods require us to write a lot of template code, while the witty dagger2 implements dependency injection at compile time through Java annotations.

Why do I need dependency injection

What we need is dependency injection. The most important thing is to decouple, achieve the purpose of high cohesion and low coupling, and ensure the robustness, flexibility and maintainability of the code.

Let’s take a look at two implementations of the same business:

1. Option a

public class Car{

    private Engine engine;
    private List<Wheel> wheels;

    public Car(){
        engine = new Engine();
        wheels = new ArrayList<>();
        for(int i = 0; i < 4; i++){
            wheels.add(new Wheel());
        }
    }

    public void start{
        System.out.println ("start the car");
    }
}

public class CarTest{

    public static void main(String[] args){
        Car car = new Car();
        car.start();
    }
}

2. Option B

public class Car{

    private Engine engine;
    private List<Wheel> wheels;

    public Car(Engine engine, List<Wheel> wheels){
        this.engine = engine;
        this.wheels = wheels;
    }

    public void start{
        System.out.println ("start the car");
    }
}

public class CarTest{

    public static void main(String[] args){

        Engine engine = new Engine();
        List<Wheel> wheels = new ArrayList<>();
        for(int i = 0; i < 4; i++){
            wheels.add(new Wheel());
        }
        Car car = new Car(engine, wheels);
        car.start();
    }
}

Scenario a: since there is no dependency injection, we need to create engine and wheel objects in car constructors.

Scenario B: we manually inject dependencies in the form of constructors, passing in engine and wheels as parameters instead of displaying the creation in the car’s constructor.

Scheme a obviously loses flexibility. All the dependencies are created inside the car class. Car is seriously coupled with engine and wheel. Once the creation method of engine or wheel has changed, we must modify the constructor of car class (for example, the constructor of creating wheel instance has changed and needs to be passed into rubber); in addition, we can’t replace dynamic replacement of Lai instance (for example, we want to change car wheel from Dunlop to Mickey) Lin (tire brand). This kind of problem is more serious in large-scale commercial projects, often a depends on B, B depends on C, C depends on D, D depends on E; once there is a slight change, it will affect the whole body, it is terrible to think about it! Dependency injection is a good way to solve this problem.

Why dagger2

Whether it is constructor injection or interface injection, it is inevitable to write a lot of template code. Smart apes, of course, are not happy to do this repetitive work, so various kinds of dependency injection frameworks come into being. But with so many dependency injection frameworks, why do we prefer dagger2? Let’s start with inversion of control (IOC) in spring.

When it comes to dependency injection, students who have done J2EE development will surely think of spring IOC. The way of configuring dependencies through XML addiction is really annoying. Moreover, the separation of XML and Java code also makes the code chain hard to trace. After that, a more advanced Guice (roboguice on Android side) appeared. We no longer need to configure dependencies through XML, but its runtime injection method makes us extremely painful in tracking and locating errors. It is mentioned in the beginning that dagger was developed under the inspiration of Guice; dagger inherited the ideas of his predecessors and crushed its predecessor Guice in performance. It can be said that the waves behind the Yangtze River push the waves ahead, and the front waves die on the beach.

As I mentioned in my introduction at the beginning, dagger is a semi static and semi runtime Di framework. Although dependency injection is completely static, generating directed acyclic graph (DAG) or implementing it based on reflection is not the optimal solution for large server-side applications or Android applications. The upgraded version of dagger2 solves this problem. It changes from semi-static to completely static, from map based API to declarative API (@ module). The generated code is more elegant and efficient. In addition, we can find out any errors during compilation. So dagger2 is more friendly to developers. Of course, dagger2 has lost some flexibility, but on the whole, the advantages far outweigh the disadvantages.

As mentioned above, the continuous dependency of a, B, C, D and e will trigger a chain reaction once the creation method of E is changed, which may lead to the need for targeted modification of a, B, C, D. however, do you think that this is just a matter of workload? When we create objects on a, we must make sure that the order of creating objects on a is the correct order. Dagger2 solves this problem very well (not only dagger2, but also developers don’t need to pay attention to these problems in other Di frameworks).

Dagger2 notes

At the beginning, we mentioned that dagger2 is based on Java annotations to implement dependency injection, so we need to understand the annotations in dagger2 before using them formally. In the process of using dagger2, we usually come across the following annotations: @ inject, @ module, @ supplies, @ component, @ qulifier, @ scope, @ singleten.

  • @Inject: @ inject has two functions: one is to mark the variable that needs to be dependent, so as to tell dagger2 to provide dependency for it; the other is to mark the constructor. Dagger2 can find this constructor and construct relevant instances when the class instance is needed through @ inject annotation, so as to provide dependency for variables marked by @ inject;

  • @Module: @ module is used to label the classes that provide dependencies. You may be a little confused. Didn’t it mention that you can provide dependencies by marking constructors with @ inject? Why do you still need @ module? Most of the time, we need to provide a dependent constructor that is a third-party library. We can’t annotate it with @ inject. For example, the constructor provided has parameters. If we simply use @ inject to mark it, how can its parameters come from? @Module helps us solve these problems.

  • @Provides: @ provides is used to label the method in the class marked by the module. This method is called when the dependency needs to be provided, so that the pre provided object is regarded as the dependency and assigned to the variable marked with @ inject;

  • @Component: @ component is used to label the interface, which is a bridge between the dependent demander and the dependent provider. The interface annotated by component will generate the implementation class of the interface at compile time (if the interface marked by @ component is carcomponent, the implementation class generated during compilation is daggercarcomponent). We complete the injection by calling the method of this implementation class;

  • @Qulifier: @ qulifier is used for custom annotations, that is, @ qulifier is used to mark annotation classes just like several basic meta annotations provided by Java. When we use @ module to annotate methods that provide dependency, we can define the method name at will (although we usually define the method name with provide, this is not mandatory, it is just to increase readability). So how does dagger2 know who this method provides dependencies for? The answer is the type of the return value. Dagger2 determines which variable is marked by @ inject according to the type of the return value. But the problem is that once there are multiple return types of the same type, dagger2 will be confused. @The existence of qulifier is formal. To solve this problem, we use @ qulifier to define our own annotations, and then annotate the methods providing dependencies and the dependent demanders (i.e., the variables annotated by @ inject), so dagger2 will know who to provide the dependency for. —-A simpler definition: we can use this annotation when the type is not enough to identify a dependency;

  • @Scope: @ scope is also used for user-defined annotation. I can define the scope of annotation through the annotation defined by @ scope to realize local singleton;

  • @Singleton: @ singleton is actually an annotation defined by @ scope. We usually use it to implement global singletons. But in fact, it can’t advance the global singleton. Whether the global singleton can be provided depends on whether the corresponding component is a global object.

We mentioned that both @ inject and @ module can provide dependencies. If we provide dependencies by marking @ inject on the constructor, how can dagger2 choose if we provide dependencies through @ module? The specific rules are as follows:

  • Step 1: first find out whether there is a method to provide dependency in the class annotated by @ module.

  • Step 2: if there is a method to provide dependency, check whether the method has parameters.

    • a: If there are parameters, initialize each parameter according to step 1;

    • b: If it does not exist, the class instance will be initialized directly to complete a dependency injection.

  • Step 3: if there is no method to provide dependency, find the constructor of @ inject annotation to see whether there are parameters in the constructor.

    • a: If there are parameters, initialize each parameter in turn from step 1

    • b: If it does not exist, the class instance will be initialized directly to complete a dependency injection.

Getting started with dagger2

The basic concepts of the previous lengthy discussion are introduced. Let’s take a look at the basic application of dagger2. The dependency configuration of dagger2 will not be described here. You can go to its GitHub home page to see the official tutorialhttps://github.com/google/dagger。 Let’s take car and engine as examples.

1. Case a

Car class is a requirement dependent party, which depends on engine class; therefore, we need to add @ inject to class variable engine to tell dagger2 to provide dependency for ourselves.

public class Car {

    @Inject
    Engine engine;

    public Car() {
        DaggerCarComponent.builder().build().inject(this);
    }

    public Engine getEngine() {
        return this.engine;
    }
}

The engine class is a dependency provider, so we need to add @ inject to its constructor

public class Engine {
    
    @Inject
    Engine(){}
    
    public void run(){
        System.out.println ("the engine is running ~ ~");
    }
}

Next, we need to create an interface carcomponent annotated with @ component. This carcomponent is actually an injector, which is used to inject engine into car.

@Component
public interface CarComponent {
    void inject(Car car);
}

After completing these, we need to build the project, and let dagger2 help us generate relevant Java classes. Then we can invoke the Dagger2 generated DaggerCarComponent in the constructor of Car to achieve injection (which is already reflected in the code of the preceding Car class).

public Car() {
    DaggerCarComponent.builder().build().inject(this);
}

2. Case B

What if the constructor for creating engine takes parameters? For example, making an engine requires gears. Or is the eggine class something we can’t modify? Now you need @ module and @ provide to play.

Similarly, we need to add @ inject to engine, a member variable of car class, to indicate that we need dagger2 to provide dependency for ourselves; and @ inject on the constructor of engine class also needs to be removed, so we don’t need to provide dependency through @ inject in constructor now.

public class Car {

    @Inject
    Engine engine;

    public Car() {
        DaggerCarComponent.builder().markCarModule(new MarkCarModule())
                .build().inject(this);
    }

    public Engine getEngine() {
        return this.engine;
    }
}

Then we need a module class to generate dependent objects. The @ module described above is used to standard this class, while @ provide is used to mark the methods that provide specific dependent objects (there is an unwritten rule here. The methods marked by @ provide are named with “provide”. This is not mandatory, but it is beneficial to improve the readability of the code).

@Module
public class MarkCarModule {

    public MarkCarModule(){ }

    @Provides Engine provideEngine(){
        return new Engine("gear");
    }
}

Next, we need to modify the carcomponent a little bit. The previous @ Component annotation does not take parameters. Now we need to add itmodules = {MarkCarModule.class}, which tells dagger2 to provide dependenciesMarkCarModuleThis class.

@Component(modules = {MarkCarModule.class})
public interface CarComponent {
    void inject(Car car);
}

We also need to modify the constructor of car class, which is more than beforemarkCarModule(new MarkCarModule())Method, which is equivalent to telling the injectorDaggerCarComponentholdMarkCarModuleThe provided dependency is injected into the car class.

public Car() {
   DaggerCarComponent.builder()
           .markCarModule(new MarkCarModule())
           .build().inject(this);
}

This is the most basic dependency injection. Let’s test our code.

public static void main(String[] args){
    Car car = new Car();
    car.getEngine().run();
}

output

The engine is spinning~~~

3. Case C

So what if a car has two engines (that is, there are two engine variables in the car class)? It doesn’t matter, we still have @ qulifier! First, we need to define two annotations using qulifier:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierA { }
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierB { }

At the same time, we need to modify the dependent provider

@Module
public class MarkCarModule {

    public MarkCarModule(){ }

    @QualifierA
    @Provides
    Engine provideEngineA(){
        return new Engine("gearA");
    }

    @QualifierB
    @Provides
    Engine provideEngineB(){
        return new Engine("gearB");
    }
}

Next, the car class that depends on the demander also needs to be modified

public class Car {

    @QualifierA @Inject Engine engineA;
    @QualifierB @Inject Engine engineB;

    public Car() {
        DaggerCarComponent.builder().markCarModule(new MarkCarModule())
                .build().inject(this);
    }

    public Engine getEngineA() {
        return this.engineA;
    }

    public Engine getEngineB() {
        return this.engineB;
    }
}

Finally, we make some adjustments to the engine class to facilitate testing

public class Engine {

    private String gear;
    
    public Engine(String gear){
        this.gear = gear;
    }
    
    public void printGearName(){
        System.out.println("GearName:" + gear);
    }
}

Test code

public static void main(String[] args) {
    Car car = new Car();
    car.getEngineA().printGearName();
    car.getEngineB().printGearName();
}

Implementation results:

GearName:gearA
GearName:gearB

4. Case D

Next, let’s look at how @ scope is scoped to implement a local singleton.

First, we need to define a carscope annotation through @ scope:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface CarScope {
}

Then we need to mark the dependency provider markcarmodule with @ carscope.

@Module
public class MarkCarModule {

    public MarkCarModule() {
    }

    @Provides
    @CarScope
    Engine provideEngine() {
        return new Engine("gear");
    }
}

You also need to use @ scope to annotate the injector component

@CarScope
@Component(modules = {MarkCarModule.class})
public interface CarComponent {
    void inject(Car car);
}

To facilitate testing, we have made some modifications to the car and engine classes

public class Car {

    @Inject Engine engineA;
    @Inject Engine engineB;

    public Car() {
        DaggerCarComponent.builder()
                .markCarModule(new MarkCarModule())
                .build().inject(this);
    }
}
public class Engine {

    private String gear;
    
    public Engine(String gear){
        System.out.println("Create Engine");
        this.gear = gear;
    }
}

If we don’t apply @ scope, the above code will instantiate the engine class twice, so there will be “create engine” output twice. Now we test the results of our work with @ scope:

public static void main(String[] args) {
    Car car = new Car();

    System.out.println(car.engineA.hashCode());
    System.out.println(car.engineB.hashCode());
}

output

Create Engine

bingo! We do implement a local singleton with @ scope.

Analysis of dagger2 principle

The basic use of dagger2 is introduced in detail. Next, we will analyze the implementation principle. This article will not analyze the principle of dagger2 generating various codes according to annotations. I will write another article about Java annotations later. The following part mainly analyzes how various classes generated by dagger2 help us implement dependency injection. In order to understand it, I chose the relatively simple one aboveCase BTo do the analysis.

The code generated during dagger2 compilation is located in thebuild/generated/source/apt/debug/your package name/below:
Dagger 2

First of all, let’s look at dagger2 based on dependency providersMarkCarModuleGenerated corresponding factory classMarkCarModule_ProvideEngineFactory。 In order to facilitate your understanding and comparison, I will release the classes written by myself and those generated by dagger2.

/**
*Our own class
*/
@Module
public class MarkCarModule {

    public MarkCarModule(){ }

    @Provides Engine provideEngine(){
        return new Engine("gear");
    }
}

/**
*Factory class generated by dagger2
*/
public final class MarkCarModule_ProvideEngineFactory implements Factory<Engine> {
  private final MarkCarModule module;

  public MarkCarModule_ProvideEngineFactory(MarkCarModule module) {
    assert module != null;
    this.module = module;
  }

  @Override
  public Engine get() {
    return Preconditions.checkNotNull(
        module.provideEngine(), "Cannot return null from a [email protected] @Provides method");
  }

  public static Factory<Engine> create(MarkCarModule module) {
    return new MarkCarModule_ProvideEngineFactory(module);
  }

  /** Proxies {@link MarkCarModule#provideEngine()}. */
  public static Engine proxyProvideEngine(MarkCarModule instance) {
    return instance.provideEngine();
  }
}

We can see thatMarkCarModule_ProvideEngineFactoryGet() in calledMarkCarModuleOfprovideEngine()Method to get the dependencies we needEngineMarkCarModule_ProvideEngineFactoryInstantiations of arecrate()Create, andMarkCarModuleAn example ofcreate()The method came in. So this onecreate()It must be called somewhere. Let’s move on.

As mentioned above, @ component is the bridge between mark carmodule and car. Let me see how dagger2 connects the two through carcomponent.

/**
*Our own class
*/
@Component(modules = {MarkCarModule.class})
public interface CarComponent {

    void inject(Car car);
}

/**
*Implementation class of carcomponent generated by dagger2
*/
public final class DaggerCarComponent implements CarComponent {
  private Provider<Engine> provideEngineProvider;

  private MembersInjector<Car> carMembersInjector;

  private DaggerCarComponent(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {
    return new Builder();
  }

  public static CarComponent create() {
    return builder().build();
  }

  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {

    this.provideEngineProvider = MarkCarModule_ProvideEngineFactory.create(builder.markCarModule);

    this.carMembersInjector = Car_MembersInjector.create(provideEngineProvider);
  }

  @Override
  public void inject(Car car) {
    carMembersInjector.injectMembers(car);
  }

  public static final class Builder {
    private MarkCarModule markCarModule;

    private Builder() {}

    public CarComponent build() {
      if (markCarModule == null) {
        this.markCarModule = new MarkCarModule();
      }
      return new DaggerCarComponent(this);
    }

    public Builder markCarModule(MarkCarModule markCarModule) {
      this.markCarModule = Preconditions.checkNotNull(markCarModule);
      return this;
    }
  }
}

From the code above, we can see that dagger2 is based onCarComponentInterface generates the implementation classDaggerCarComponent(yes, that’s exactly what we use daggercarcomponent in car’s constructor.).DaggerCarComponentInstantiated in buildDaggerCarComponentObject and first call theMarkCarModule_ProvideEngineFactory.create(builder.markCarModule)It’s beginning to changeprovideEngineProviderVariable, and then callCar_MembersInjector.create(provideEngineProvider)InitializedcarMembersInjectorVariable. When we manually call in the constructor of the Car classinject(Car car)MethodcarMembersInjector.injectMembers(car)。 So we’re going to take a lookCar_MembersInjectorImplementation of.

public final class Car_MembersInjector implements MembersInjector<Car> {
  private final Provider<Engine> engineProvider;

  public Car_MembersInjector(Provider<Engine> engineProvider) {
    assert engineProvider != null;
    this.engineProvider = engineProvider;
  }

  public static MembersInjector<Car> create(Provider<Engine> engineProvider) {
    return new Car_MembersInjector(engineProvider);
  }

  @Override
  public void injectMembers(Car instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.engine = engineProvider.get();
  }

  public static void injectEngine(Car instance, Provider<Engine> engineProvider) {
    instance.engine = engineProvider.get();
  }
}

Car_MembersInjectorMediumcreate()This method is used to instantiate itself. As we saw earlier, theDaggerCarComponentInvoked in.injectMembers(Car instance)takeengineProvider.get()The return value of is assigned to the engine variable that depends on the car of the demander, andengineProvider.get()That’s what we mentioned at the beginning of this sectionMarkCarModule_ProvideEngineFactoryMediumget()method. At this point, the whole process of dependency injection is completed. More complex scenarios generate more complex code, but the principle is similar to the previous analysis.

summary

This article only introduces the concept and use of dagger2 through some simple examples. The application in the actual project is much more complicated than the example here. The application of dagger2 in the actual project can refer to this open source projecthttps://github.com/BaronZ88/MinimalistWeather(the project adopts MVP architecture, in which the decoupling of view layer and presenter layer is realized by dagger2).

MinimalistWeatherIs an open source weather app, the development of this project is mainly to show the use of various open source library and the architecture of Android project, and as a part of the team development specification. Every letter, every name and every line of code in the project are carefully studied; however, due to the limited time and energy, the project UI is not strictly required. In line with the principle of improving, providing better open source projects and more beautiful weather applications, we hope that interested developers and ued students can complete this project together.

Recommended Today

NOR flash memory begins the development of automobile field

As cars become more intelligent and require more storage space, many technologies are struggling for the driver’s seat, but it is certain that the NOR flash can at least use a shot gun.With its programmable ability, nor flash exists in many applicationsEEPROMIt has found new opportunities in applications that require fast, nonvolatile memory, including communications, […]