Hi, I’m Halliday. The Series will take a look at some of the technical frameworks from the trunk to the twig, and this article introduces the open source project Dagger.

The passage is about 3800 words and takes about 10 minutes to read.

Dagger source code is based on the latest version 2.28.3

background

The Dependency Injection (DI) follows the principle of Inversion of Control (IoC). When you create an object, you pass in dependencies to the object. When you pass in dependencies to the object, you use different instances to implement different behaviors (Control). For example, common constructors and setters are called injections.

To recap Google’s car-building business,

The Engine instance is created by the Car class itself. When the Car Engine needs to be replaced, the Car class needs to be modified, which violates the open and closed principle.

class Car {
    private Engine engine = new Engine();
}
Copy the code

Manual dependency injection, such as constructors and setters, is used to replace the Car Engine by passing in a different Engine implementation (such as ElectricEngine extends Engine) without modifying the Car class.

//1. Constructor injection
class Car {
    private final Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine; }}/ / 2. The setter injection
class Car {
    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine; }}Copy the code

3. As the number of auto parts increases, there will be more and more code for dependency injection. For example, cars need engines, tires, and engines need cylinders and spark plugs.

class Car {
    private final Engine engine;/ / the engine
    private final Wheel wheel;/ / tire

    public Car(Engine engine,Wheel wheel) {
        this.engine = engine;
        this.wheel = wheel; }}class Engine {
    private final Cylinder cylinder;/ / cylinder
    private final SparkPlug sparkPlug;/ / the spark plug

    public Car(Cylinder cylinder,SparkPlug sparkPlug) {
        this.cylinder = cylinder;
        this.sparkPlug = sparkPlug; }}void main(a){
    // In order to build a car, you need to create a bunch of new objects, and you need to inject constructor methods
    Cylinder cylinder = new Cylinder();
    SparkPlug sparkPlug = new SparkPlug();
    Engine engine = new Engine(cylinder,sparkPlug);
    Wheel wheel = new Wheel();
    Car car = new Car(engine,wheel);
}
Copy the code

There are two big pain points from the code, one is that there are many objects to create, and the second is that the object creation process needs to be ordered, that is, the cylinder and the spark plug first to build the engine, and the engine and the tires first to build the car, so the user has to spend time understanding the dependencies between the parts, which is inconvenient.

As a result, there are several libraries to implement automatic dependency injection. There are two ways to implement it (more on koIN implementation later).

  1. One is run-time reflection of connection dependencies, which has little compile impact but is slow to run
  2. Second, dependencies are connected at compile time. Creating helper classes requires additional IO and compilation time, which slows compilation but runs faster

Dagger For an Android terminal with limited memory and computing power, the dagger is of course an option for thought 2. The dagger creates the helper class at compile time by annotating the pose, dependency, scope, etc. of the marked object, thus enabling automatic dependency injection. Dagger was a little expensive to get started with, and Google later introduced Hilt to make it easier for us to use,

Hilt is the recommended Jetpack library for implementing dependency injection in Android. Hilt defines a standard way to perform DI in your application by providing a container for each Android class in your project and automatically managing its lifecycle for you.

Hilt is built on top of the popular DI library Dagger and thus can benefit from the compile time correctness, runtime performance, scalability, and Android Studio support that Dagger provides.

– Google

So Hilt, let’s start with the dagger tour

Bonus: For the pain point of manual injection, take a look at Google’s manual dependency Injection (which may give you a better understanding of the dagger design).

The trunk

Simple to use

Rely on,

implementation 'com. Google. Dagger: a dagger: 2.28.3'
annotationProcessor 'com. Google. Dagger: a dagger - compiler: 2.28.3'
Copy the code

@ Inject and @ Component

@inject flags instance creation pose, car and engine classes,

class Car {
    private final Engine mEngine;

    @Inject // Tell the dagger how to create a Car
    public Car(Engine engine) {
        mEngine = engine;
    }

    public void start(a) { mEngine.start(); }}class Engine {
    @Inject // Tell the dagger how to create the Engine
    public Engine(a) {}public void start(a) {
        Log.e("Halliday"."Start engines and head for Akimaki."); }}Copy the code

So the dagger generates code like new Car(new Engine()) to create the instance,

@Component indicates which instances are to be created. For example, in the Build car drawing (interface), declare build car.

@Component // Tell Dagger what to build
interface CarGraph {
    / / building cars
    Car makeCar(a);
}
Copy the code

Create Car_Factory, Engine_Factory, and DaggerCarGraph classes. Use them in the Activity.

class DaggerActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dagger);
        // Get the building drawings
        CarGraph carGraph = DaggerCarGraph.create();
        // Build a car
        Car car = carGraph.makeCar();
        // Start the enginecar.start(); }}Copy the code

Injection position two, you can also inject the car directly into the Activity,

@Component // Tell Dagger what to build
public interface CarGraph {
    / / building cars
    Car makeCar(a);

    // Added: Tell the dagger there is an Activity to inject
    void inject(DaggerActivity activity);
}
Copy the code

If we make that, we’ll have an extra class called DaggerActivity_MembersInjector, which we’ll see later, but in the Activity,

class DaggerActivity extends AppCompatActivity {
    // Inject the car into the Activity
    @Inject
    Car mCar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dagger);
        // Get the building drawings
        CarGraph carGraph = DaggerCarGraph.create();
        // Tell the dagger there is an Activity to inject
        carGraph.inject(this);
        // At this point, the mCar has been automatically assigned to the dagger, and we can start the engine directlymCar.start(); }}Copy the code

Either way, you can see that the Activity just picks up the Car and doesn’t care about the details of building the Car (what engine it uses). This way, the Activity and Car don’t have to change their code if they need to change the engine.

@ the Module and @ Binds

So the question is, what if you want to replace Engine with an IEngine interface? Because I have two engines, the GasEngine for gasoline cars and the ElectricEngine for electric cars, how to inject the interface without the construction method? This is where the @Module and @Binding annotations come in.

Define engine interfaces and implementation classes,

interface IEngine {
    public void start(a);
}

class GasEngine implements IEngine {// Gasoline engine
    @Inject
    public GasEngine(a) {}@Override
    public void start(a) {
        Log.e("Halliday"."Gasoline engine on your way to Akimaki."); }}class ElectricEngine implements IEngine {// Electric engine
    @Inject
    public ElectricEngine(a) {}@Override
    public void start(a) {
        Log.e("Halliday"."Electric engine to mount Akimingshan."); }}Copy the code

And then Car, let’s not do it, let’s write a NewCar class, NewCar, and make it rely on the interface IEngine instead of the class,

class NewCar {
    // Depends on the interface
    private final IEngine mEngine;

    @Inject // Tell the dagger how to create the NewCar
    public NewCar(IEngine engine) {
        mEngine = engine;
    }

    public void start(a) { mEngine.start(); }}Copy the code

Next up, @Module and @Sharing! Build an abstract class GasEngineModule that represents the gasoline engine module and provides the gasoline engine,

@Module
abstract class GasEngineModule {// Gasoline engine module
    @Binds
    abstract IEngine makeGasEngine(GasEngine engine);
}
Copy the code

Then support the creation of NewCar in CarGraph, introduce Module into Component,

@Component(modules = {GasEngineModule.class})
// Introduce the gasoline engine module
public interface CarGraph {
    / /...

    / / to build a new car
    NewCar makeNewCar(a);
}
Copy the code

The Activity is used,

//DaggerActivity.java
void onCreate(Bundle savedInstanceState) {
    CarGraph carGraph = DaggerCarGraph.create();
    NewCar newCar = carGraph.makeNewCar();
    newCar.start();
}
Copy the code

Do ~

If you want to replace NewCar with an electric engine, you can simply rely on the new module, the ElectricEngineModule, which provides an electric engine,

@Module
public abstract class ElectricEngineModule {// Electric engine module
    @Binds
    abstract IEngine makeElectricEngine(ElectricEngine engine);
}
Copy the code

GasEngineModule instead of ElectricEngineModule,

@Component(modules = {ElectricEngineModule.class})/ / replace
public interface CarGraph {
    / /...
}
Copy the code

So we don’t need to modify NewCar to replace the engine.

@ IntoMap and @ StringKey

One day, I suddenly want to drive a dual hybrid, which means I want both the gasoline engine and the electric engine, and we find that @Component modules can be passed into an array, so let’s try passing in both engine modules.

@Component(modules = {GasEngineModule.class, ElectricEngineModule.class})
public interface CarGraph {
    / /...
}
Copy the code

So I’m going to make a mistake, I don’t even have to think about it, NewCar only has one interface that relies on the IEngine, so now I don’t know which engine I’m putting in to build NewCar,

So what? Enter @intomap and @Stringkey, which support multiple bindings.

IntoMap means that the Module will be stored in the map, StringKey means the Module name, which will also be the key of the map,

@Module
abstract class GasEngineModule {// Gasoline engine module
    @Binds
    @IntoMap
    @StringKey("Gas")
    abstract IEngine makeGasEngine(GasEngine engine);
}

@Module
abstract class ElectricEngineModule {// Electric engine module
    @Binds
    @IntoMap
    @StringKey("Electric")
    abstract IEngine makeElectricEngine(ElectricEngine engine);
}
Copy the code

Then modify NewCar so that instead of relying on one engine interface, the IEngine, it relies on a set of engines,

class NewCar {
    // I am a hybrid now, I need to use the MAP reception engine
    private final Map<String, IEngine> mEngineMap;

    @Inject // Tell the dagger how to create the NewCar
    public NewCar(Map<String, IEngine> engineMap) {
        mEngineMap = engineMap;
    }

    public void start(a) {
        // Double mix move ~
        for (Map.Entry<String, IEngine> entry : mEngineMap.entrySet()) {
            entry.getValue().start();
            // Start the gasoline engine and head for Akiaki Mountain
            // Start the electric engine and head for Akimingshan}}}Copy the code

The Activity code does not need to be changed, and the dual hybrid car is built.

@Singleton

If you want to limit the number of newcars in the world to one, you can specify @singleton as a Singleton,

@Singleton // For example, I am a limited edition hybrid
public class NewCar {
    / /...
}

@Singleton // The car drawing should follow the singleton
@Component(modules = {GasEngineModule.class, ElectricEngineModule.class})
public interface CarGraph {
    / /...
}
Copy the code

Run the Activity,

//DaggerActivity.java
void onCreate(Bundle savedInstanceState) {
    CarGraph carGraph = DaggerCarGraph.create();
    NewCar newCar = carGraph.makeNewCar();
    newCar.start();
    NewCar newCar2 = carGraph.makeNewCar();
    newCar2.start();
    //newCar and newCar2 are the same instance
    Log.e("Halliday", newCar.hashCode() + "," + newCar2.hashCode());
}
Copy the code

I’ll stop here with the dagger. I believe I have a preliminary understanding of the dagger. There are still some annotations left, such as:

@Provides: We can provide @Module and @provides when we can’t mark the creation pose of the instance with @inject. For example, Retrofit is a three-party library class and we can’t mark its constructor, so we can provide it with @Provides.

@Module
public class NetworkModule {
    @Provides
    public Retrofit provideRetrofit(a) {
        return new Retrofit.Builder()
            .baseUrl("xxx") .build(); }}Copy the code

@scope: Scope…

@subcomponent: Subcomponent…

There are a lot of notes, ready to put into the branch to write… 🤣

Realize the principle of

So instead of parsing the annotations and creating the helper class during the compile period, let’s just look at the helper class that he generates,

Note: In the beginning of writing the interface name, I use CarGraph instead of CarFactory to avoid confusion with the dagger generation class. I use CarGraph, which has the meaning of geometric graph, to understand the blueprint of building a car. Let’s choke our dream together.

We saw DaggerCarGraph, who implemented our CarGraph interface,

class DaggerCarGraph implements CarGraph {
    //Provider, provide multiple engine map, < engine name, engine instance >
    private Provider<Map<String, IEngine>> mapOfStringAndIEngineProvider;
    //Provider provides NewCar
    private Provider<NewCar> newCarProvider;

    private DaggerCarGraph(a) {
        initialize();
    }

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

    public static CarGraph create(a) {
        // Create DaggerCarGraph instance with Builder
        return new Builder().build();
    }

    private void initialize(a) {
        // Create a Provider for gasoline engines and an electric engine
        // Save them with put and combine them into a single Provider that provides multi-engine maps
        this.mapOfStringAndIEngineProvider = MapFactory.<String, IEngine>builder(2)
            .put("Gas", (Provider) GasEngine_Factory.create())
            .put("Electric", (Provider) ElectricEngine_Factory.create())
            .build();
        // Pass the Provider that provides the multi-engine map to NewCar_Factory,
        // Then wrap the layer with DoubleCheck, which will lock and handle the singleton logic
        this.newCarProvider = DoubleCheck.provider(
            NewCar_Factory.create(mapOfStringAndIEngineProvider));
    }

    @Override
    public Car makeCar(a) {
        //1. Make a car with a makeCar
        return new Car(new Engine());
    }

    @Override
    public void inject(DaggerActivity activity) {
        //2. Old build car: position 2, @inject ↓
        injectDaggerActivity(activity);
    }

    private DaggerActivity injectDaggerActivity(DaggerActivity instance) {
        //2. Old build: Position two, first create and then inject
        // The instance is also created with makeCar. If our interface does not define this method, the dagger generates a getCar with the same function
        DaggerActivity_MembersInjector.injectMCar(instance, makeCar());
        return instance;
    }

    @Override
    public NewCar makeNewCar(a) {
        //3. Obtain the new vehicle from the Provider
        return newCarProvider.get();
    }

    public static final class Builder {
        private Builder(a) {}public CarGraph build(a) {
            return newDaggerCarGraph(); }}}Copy the code
  1. Old build: Position one, build directly with makeCar

In building the old Car, posture one is directly adjusted interface makeCar method to build, the implementation is a simple new example.

  1. Old build car: position two, @inject

Pose the same @ Inject injection as an example, he is also the first makeCar (for instance), and adjustable DaggerActivity_MembersInjector injectMCar for injection,

//DaggerActivity_MembersInjector.java
@InjectedFieldSignature("com.holiday.srccodestudy.dagger.DaggerActivity.mCar")
public static void injectMCar(DaggerActivity instance, Car mCar) {
    // Reference a member of the DaggerActivity and assign it a value. MCar cannot be declared private
    instance.mCar = mCar;
}
Copy the code
  1. New build, obtained from Provider

Get (). If @singleton is used, NewCar_Factory will be wrapped by DoubleCheck. DoubleCheck will lock and handle the Singleton logic. Let’s just look at the get of NewCar_Factory,

//Factory<T> extends Provider<T>
class NewCar_Factory implements Factory<NewCar> {
    private final Provider<Map<String, IEngine>> engineMapProvider;

    public NewCar_Factory(Provider<Map<String, IEngine>> engineMapProvider) {
        this.engineMapProvider = engineMapProvider;
    }

    @Override
    public NewCar get(a) {
        // Obtain the multi-engine map through engineMapProvider
        return newInstance(engineMapProvider.get());
    }

    public static NewCar_Factory create(Provider<Map<String, IEngine>> engineMapProvider) {
        return new NewCar_Factory(engineMapProvider);
    }

    public static NewCar newInstance(Map<String, IEngine> engineMap) {
        //new indicates a new car instance
        return newNewCar(engineMap); }}Copy the code

To see how the multi-engine map is obtained, enginemapprovider.get (),

//MapFactory.java
//MapFactory extends AbstractMapFactory implements Factory
public Map<K, V> get(a) {
    // Create a new LinkedHashMap
    Map<K, V> result = newLinkedHashMapWithExpectedSize(contributingMap().size());
    // Iterate over the saved engine and save the new map
    for (Entry<K, Provider<V>> entry : contributingMap().entrySet()) {
        result.put(entry.getKey(), entry.getValue().get());
    }
    // Return an unmodifiable map
    return unmodifiableMap(result);
}
Copy the code

At this point, all the production lines of Dagger are analyzed,

twigs

@Scope Scope, @subComponent, SPI (Service Provider Interface), GRPC (Google’s Remote Procedure call framework), etc.

Usage scenarios

So what’s the use of Dagger in Android? First of all, from an architectural perspective, in the Google example, I use the dagger in combination with Activity, fake ViewModel, Repository, DataSource, and Retrofit.

Then we used the dagger separately in some scenarios in the project, from a business perspective, on individual business lines with high complexity. For example, wallet business has a large number of instances and a large number of pages/views with many-to-many relationships. For example, wallet Act requires wallet Api and wallet user information Manager. Recharge Act requires payment of Api and recharge Service; Bank card list View needs bank Service… A scene like this, with many-to-many, object dependencies all over the place, lends itself well to the dagger injection.

@PurseScope // Wallet scope
@Component(modules = PurseModule.class) // Instances are provided by the PurseModule
public interface PurseComponent {

    // Inject for act
    void inject(Refund details Act xxxAct);
    void inject(Bank card homepage Act xxxAct);
    void inject(Wallet front page Act xxxAct);
    void inject(Wallet setup Act xxxAct);
    void inject(Balance page Act xxxAct);
    / /...

    // Inject the view
    void inject(Wallet homepage GridContainer xxxGridContainer);
    void inject(Bank card list View xxxView);

    // Provide network callspayApi xxxApi(a);

    // Provide wallet user informationWallet user InformationManager xxxManager(a);

    / /...
}
Copy the code

Then look at PurseModule, which provides various instances that can be new, manual singletons, factory produced…

@Module
public class PurseModule {
    @Provides
    @PurseScope
    publicProvide payment Api(...) {return newApi (...). ; }@Provides
    @PurseScope
    publicUser Information Manager User information Manager(...) {returnUser information manager.get (...) ; }@Provides
    @PurseScope
    publicProvide recharge Service(...) {return newTop-up ServiceFactory. Create (...). ; }/ /...
}
Copy the code

The end of the

A quick summary of the dagger pros and cons

  • Advantages: No reflection, support for incremental compilation
  • Disadvantages: Extra IO and compile time required during build, increased package size due to class generation, poor usability,

Follow-up plans: Dagger Twig, Hilt, Koin, and by the way, see how the injection @AutoWired on Spring works… There is no end to learning

Series of articles:

  • Retrofit
  • Okhttp for the Series
  • Glide from the Series “Never Forget”

The resources

  • GitHub & Documentation & API
  • Google – Dependency injection in Android
  • Google – Pitfalls and optimizations encountered using Dagger in Kotlin
  • Nugget – From Dagger to Hilt, why is Google so obsessed with letting us use DEPENDENCY Injection

Welcome to pay attention to the original technology public account: Halliday EI