series

  • Dagger2 in Android (a) Popular basics
  • Dagger2 in Android (2) Advanced
  • Dagger2 in Android (iii) Scope with life cycle
  • Dagger2 in Android (四). Android extension library

The basic annotation for the Dagger has been covered, and finally we have built a simple Dagger injection.

In this post we’ll continue learning more about the Dagger annotations and how to manage them modularly. This will help us properly organize the different components and define their respective lifecycles.

@Named

Dependency injection lost

We said that @Module and @provides work together to wrap constructors that don’t have @inject annotation. But what if you wrap a class that already has @Inject? Actually, these two have a priority. Dagger will first look for the instantiated method from Module, and if not find the constructor tagged by Inject. It makes sense that the average person would choose to go to the supermarket first rather than visit the factory directly.

In general, we wrap modules in a single layer for ease of management, whether or not constructors are annotated. This helps us better manage dependency structures and lifecycles, which will be covered later.

But what if the Module has two Provides that return the same type? Consider the following code:

class Stove() {
    var name: String? = null

    constructor(name: String) : this() {
        this.name = name
    }
}

@Module
class MainModule() {
    @Provides
    provideStove():Stove {
        return Stove()
    }
	
	@Provides
    provideStove():Stove { // Now there are two Provides returning to the furnace
        return Stove("Boom")}}Copy the code

Now there are two stoves in Carrefour, Dagger has no idea which one to buy, we call this situation “Dependency Injection lost”. Dependency injection errors are reported at compile time and are easy to spot.

To solve it

To fix this, a new annotation @named has to be introduced, where the cook will specify the type of stove they want so they don’t buy the wrong one. Also, remember to specify the model number of the stoves on the shelf, otherwise how to buy right -. –

Module and Chef after transformation are as follows:

@Module
class MainModule() {
    @Provides
	@Named("noname")
    provideStove():Stove {
        return Stove()
    }
	
	@Provides
	@Named("boom")
    provideStove():Stove { // Now there are two Provides returning to the furnace
        return Stove("Boom")}}Copy the code
class Chef() {
    @Inject
	@Named("noname")
    val stove1: Stove
	
	@Inject
	@Named("boom")
    val stove2: Stove
}
Copy the code

Our chef is greedy. He wants both models. But instead of buying randomly at first, he now clearly indicates that I need two sizes and can distinguish between them. So there will be no errors.

@Qualifier

Qualifier serves exactly the same purpose as Named. Although Named is a simple string, Qualifier requires custom annotations first. Now let’s implement the previous example with the Qualifier.

// Define a new annotation called StoveQualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class StoveQualifier
Copy the code
@Module
class MainModule() {
    @Provides
	@StoveQualifier("noname")
    provideStove():Stove {
        return Stove()
    }
	
	@Provides
	@StoveQualifier("boom")
    provideStove():Stove { // Now there are two Provides returning to the furnace
        return Stove("Boom")}}Copy the code
class Chef() {
    @Inject
	@StoveQualifier("noname")
    val stove1: Stove
	
	@Inject
	@StoveQualifier("boom")
    val stove2: Stove
}
Copy the code

See? That’s exactly the same as “Named”. I’m sure someone will ask, so why not just use Named?

You can think of Qualifier as a custom namespace. All previous models are labeled under the Named space. It is air conditioning, stove, induction cooker, refrigerator and so on, model all mixed together, apparently this is not a good way. By customizing qualifiers, we can give each class its own model namespace without worrying about conflicts or confusion.

Modular management

As mentioned in the beginning, we will encapsulate a layer with modules for management purposes, and modules will eventually be associated with Components. The key, then, is how to organize the Components.

Classification principles

The title is Dagger2 in Android. No programmer in his right mind would write all the injections into a Component, which would be too large and difficult to maintain. However, the granularity of partitioning should not be too small, and creating a Component for each class can be very complex and difficult to maintain.

So let’s go back to the beginning what exactly does Dagger do? After a round of study, I believe everyone has their own answers. I think of it primarily as “creating and managing objects and injecting them into classes that need them.” Since it is a managed object, you have to consider the life cycle. So a division based on life cycle might be a good idea.

An Android application has many life cycles, and there are generally two types:

  • Application: This is the longest life cycle, from the time we start the Application until it is completely destroyed.
  • Activity/Fragment: Both represent a page. Start when opened, destroy when left.

So we can categorize components by life cycle.

Tissue Component

We know that Component is essentially an interface (abstract class), so it can also be related to each other. There are two kinds of relationships: dependency and inclusion.

Dependencies (component dependencies)

Now we have two Components, AppComponent and ActivityComponent. The former holds a global Context object, and we want the latter to depend on the former. Here’s what you can do:

@Module
class AppModule(private val context: Context) {
    @Provides
    fun provideContext(a): Context = context
}

@Component(modules = [AppModule::class])
interface AppComponent {
	fun context(a): Context // Pay attention to this line
}
Copy the code
@Module
class ActivityModule {
    @Provides
    fun provideSp(context: Context) =
            context.getSharedPreferences("Cooker", Context.MODE_PRIVATE)
}

// Declare dependencies
@Component(dependencies = [AppComponent::class], modules = [ActivityModule::class])
interface ActivityComponent {}Copy the code

Analyze this code:

The ActivityModule defines an instance of the Provides that returns SharedPreferences. But creating this instance requires context, where did it come from? Since it declares a dependency on the AppComponent, and the AppComponent has a Provides in the AppModule that Provides the context, So the ActivityModule gets the context from the AppComponent.

But this is not unconditional, depending on others only if they are willing to be dependent on you. So the AppComponent must explicitly define a function that returns the Context type to the Component that depends on it. If you don’t define it, if you do, you won’t give it to anyone.

The Provides function in Component (1) Provides objects to the Component (2) Provides objects to the Component (3). The latter merely creates objects.

Inclusion relationships (subcomponents) (Component inheritance)

Dependence is like a friend, you can only share it if the other person is willing to. Inclusion is like parenting, sharing is unconditional.

The following steps are required to declare inheritance

  1. The child Component with@SubcomponentAnnotation.
  2. The child Component declares a Builder to tell the parent Component how to create itself.
  3. Module corresponding to the parent ComponentsubcomponentsProperty to specify which child Components to own.
  4. The parent Component declares an abstract method to get the child Component’s Builder.

The above example could be rewritten with inclusion:

@SubComponent(modules = [ActivityModule::class]) // Child Component annotated with @subComponent.
interface ActivityComponent {
	
	// Declare a Builder to tell the parent Component how to create itself
	@Subcomponent.Builder
    interface Builder {
        fun build(a): ActivityComponent
    }
}

// The Module corresponding to the parent Component uses the subComponents property to indicate which child Components it has
@Module(subcomponents = [ActivityComponent::class])
class AppModule(private val context: Context) {
    @Provides
    fun provideContext(a): Context = context
}

@Component(modules = [AppModule::class])
interface AppComponent {
	//fun context(): context // No need to display the definition

	// The parent Component declares an abstract method to get the child Component's Builder
	fun activityComponent(a): ActivityComponent.Builder
}
Copy the code

After the inclusion relationship is declared, all modules under the object subinterface provided by the parent interface can be used directly, without the need to display the declaration.

For inclusion, the child Component will no longer generate the DaggerXxxComponent class and will need to be created from an instance of the parent Component.

contrast

Similarities:

  • Can use objects provided by the parent interface.

Difference:

  • The generated code is different. Dependencies Each Component generates a DaggerXxxComponent class; The containment relationship will only generate one.
  • Different access restrictions on the parent interface object. Dependencies must be actively declared to obtain them; The inclusion relationship is obtained by default.

So which to choose, there seems to be no accurate specification, in more practice experience it. (Typically on Android, the Activity is included in the AppComponent.)

conclusion

This chapter focuses on the modular management of the Dagger. As mentioned at the beginning, Dagger also manages the life cycle of an object, a very important and very confusing aspect that we will discuss separately in the next chapter.

With the foundation of the previous chapter, this chapter is not too much analogy, if there is a concept forgotten (especially when talking about modularity) must go back to the previous chapter to see, otherwise the next chapter will be more painful.