[JAVA] Understand the official sample Coffee of Dagger2

About Dagger2, which is a DI library for Java / Android, the official sample Coffee app I would like to explain the mechanism based on (/ master / examples / simple / src / main / java / coffee). However, the Coffee app was a little hard to understand (@Binds, Lazy, etc.), so the code described below has been slightly modified. Dagger2 User Guide can be understood with knowledge from the top to the middle (Singletons and Scoped Bindings).

What are you trying to do

What this sample is trying to do is create an instance of the CoffeeMaker class with Dagger2.

The CoffeeMaker class has the following dependencies.

image.png https://docs.google.com/presentation/d/1fby5VeGU9CN8zjw4lAb2QPPsKRxx6mSwCe9q7ECNSJQ/pub?start=false&loop=false&delayms=3000&slide=id.p

Resolving this dependency and getting a CoffeeMaker instance can be a bit tricky.

Heater heater = new ElectricHeater();
Pump pump = new Thermosiphon(heater);
CoffeeMaker coffeeMaker = new CoffeeMaker(heater, pump).maker();
coffeeMaker.brew();

Using Dagger2, the above code looks like this:

CoffeeMaker coffeeMaker = DaggerCoffeeShop.create().maker();
coffeeMaker.brew();

In this way, CoffeeMaker client code is freed from the complexity of building dependencies. The following describes how Dagger2 builds these dependencies.

Declaration of dependence

Dagger instantiates using a constructor annotated with @Inject.

class Thermosiphon implements Pump {
    private final Heater heater;

    @Inject
    Thermosiphon(Heater heater) {
        this.heater = heater;
    }

    @Override
    public void pump() {
        if (heater.isHot()) {
            System.out.println("=> => pumping => =>");
        }
    }
}

If a Thermosiphon instance is requested, a Thermosiphon instance will be created using the above constructor.

Here, the Thermosiphon class depends on the Heater class. Dagger identifies the dependencies defined in the constructor and resolves them.

Satisfy dependencies

Dagger tries to satisfy the dependency by instantiating the requested type, but the declaration of dependency by @Inject does not always work. In the following cases, @Inject will not work.

--Type is an interface --3rd party classes cannot be annotated --Configurable objects (it seems to be objects that are set by setter etc. after instantiation) need to be set.

In the Coffee app, the types that CoffeeMaker depends on (Heater and Pump) are declared as interfaces, which is the case above. In these cases, the method annotated with @Provides creates an instance.

For example, Heater declares a dependency as follows: Define a method annotated with @ Provides and specify Heater as the return type.

@Provides
static Heater provideHeater() {
    return new ElectricHeater();
}

Similarly, Thermosiphon declares: The @ Provides method can have its own dependencies.

@Provides
static Pump providePump(Thermosiphon pump) {
    return pump;
}

The @ Provides method must belong to a module. A module is a class that is annotated with @Module.

@Module
class DripCoffeeModule {
    @Provides
    static Heater provideHeater() {
        return new ElectricHeater();
    }

    @Provides
    static Pump providePump(Thermosiphon pump) {
        return pump;
    }
}

By convention, it seems that the @ Provides method is prefixed with provide and the module is suffixed with Module.

Build a dependency graph

Classes and methods defined by @ Inject and @ Provides form a dependency graph of objects. To create this graph, define a component. A component is an interface annotated with @Component and has methods with no arguments. The return type of these methods will be the type you want to resolve.

The following defines a CoffeeShop component that has a method that returns a CoffeeMaker instance. Pass modules to the components to build their subordinate dependencies. Here we are passing the DripCoffeeModule.

@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
    CoffeeMaker maker();
}

Dagger will automatically generate an implementation of CoffeeShop based on this definition. Implementation classes generated by Dagger are prefixed with Dagger. Here, an implementation class called DaggerCoffeeShop is generated.

The component implementation class has a builder method, which is used to build dependencies.

CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();

In the above example, the DripCoffeeModule instance is manually created and passed to the builder, but if the following conditions are met, this process is unnecessary.

--If you have access to the module's default constructor --If all @ Provides methods in the module are static

For components, if all of the following modules apply to the above, the implementation class has a create method that allows you to instantiate directly without going through the builder.

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

Finally, the CoffeeMaker client code looks like this:

CoffeeMaker coffeeMaker = DaggerCoffeeShop.create().maker();
coffeeMaker.brew();

Coffee app code (modified version)

Here is the code with a slight modification of the Official Coffee App. It is implemented only with the functions in the range explained above.

** * Only the @Singleton annotation will be described later. ** **

Heater.java(Remain official)


interface Heater {
    void on();
    void off();
    boolean isHot();
}

Pump.java(Remain official)


interface Pump {
    void pump();
}

ElectricHeater(Remain official)


class ElectricHeater implements Heater {
    boolean heating;

    @Override
    public void on() {
        System.out.println("~ ~ ~ heating ~ ~ ~");
        this.heating = true;
    }

    @Override
    public void off() {
        this.heating = false;
    }

    @Override
    public boolean isHot() {
        return heating;
    }
}

Thermosiphon(Remain official)


class Thermosiphon implements Pump {
    private final Heater heater;

    @Inject
    Thermosiphon(Heater heater) {
        this.heater = heater;
    }

    @Override
    public void pump() {
        if (heater.isHot()) {
            System.out.println("=> => pumping => =>");
        }
    }
}

DripCoffeeModule.java


@Module
class DripCoffeeModule {
    @Singleton
    @Provides
    static Heater provideHeater() {
        return new ElectricHeater();
    }

    @Provides
    static Pump providePump(Thermosiphon pump) {
        return pump;
    }
}

CoffeeMaker(Remain official)


class CoffeeMaker {
    private final Heater heater;
    private final Pump pump;

    @Inject
    CoffeeMaker(Heater heater, Pump pump) {
        this.heater = heater;
        this.pump = pump;
    }

    public void brew() {
        heater.on();
        pump.pump();
        System.out.println(" [_]P coffee![_]P ");
        heater.off();
    }
}

CoffeeShop.java


@Singleton
@Component(modules = DripCoffeeModule.class)
public interface CoffeeShop {
    CoffeeMaker maker();
}

CoffeeApp.java


public class CoffeeApp {
     public static void main(String[] args) {
          CoffeeMaker coffeeMaker = DaggerCoffeeShop.create().maker();
          coffeeMaker.brew();
     }
}

@Singleton annotation

When resolving dependencies, Dagger will try to instantiate a type every time it is requested by default. However, in the case of the Coffee app, the default behavior does not work as intended.

CoffeeMaker's brew method is to turn the heater on (heater.on ()), pour water (pump.pump ()), brew coffee (println ()), and turn the heater off again. Pour water only when the heater is on. Confirmation of whether the heater is turned on is delegated to the Heat instance held by the Pump instance, but at this time, the Heat instance held by the CoffeeMaker instance and the Heat instance held by the Pump instance are You must be referring to the same thing.

As mentioned earlier, Dagger is instantiated every time a type is requested, so it will not behave as intended. You can then use the @ Singleton annotation to specify that the same instance should always be returned when the type is requested.

In the above code, the provideHeater method of DripCoffeeModule is annotated with @Singleton to specify that the Heater instance is a singleton. The CoffeeShop interface is also annotated with @ Singleton so that the component and its subordinate instances have the same life cycle.

Summary

I've explained the basics that are the first step in understanding Dagger. Since this is only the first part of the user guide, I think there is still a lot of knowledge that is not enough to practice Dagger, but those who want to use Dagger from now on, and my own a few days later. I thought it would be helpful for you.

Recommended Posts

Understand the official sample Coffee of Dagger2
Read the official Dagger2 documentation to understand the basics
Understand the basics of docker
Understand the basic mechanism of log4j2.xml
Official logo list of the service
Understand the basics of Android Audio Record
Zeller's official (asking for the day of the week)
About the official start guide of Spring Framework
The official name of Spring MVC is Spring Web MVC
[For beginners] Quickly understand the basics of Java 8 Lambda
Understand the characteristics of Scala in 5 minutes (Introduction to Scala)
I wrote a sequence diagram of the j.u.c.Flow sample
The world of clara-rules (2)
Judgment of the calendar
The world of clara-rules (4)
The world of clara-rules (1)
Let's understand the function!
The world of clara-rules (3)
The world of clara-rules (5)
The idea of quicksort
The idea of jQuery
Now, I understand the coordinate transformation method of UIView (Swift)
Follow function association memorandum (understand the description of the User model)