Dagger2 进阶使用

目录:
  1. @Qualifier @Named 注解的作用
  2. 懒加载 Lazy 和 Provider
  3. @Binds 的作用
  4. @BindsOptionalOf、Optional 的作用
  5. @BindsInstance 的作用
  6. Set 注入
  7. Map 注入
@Named 注解的作用

当我们使用 Dagger 的时候,可能需要在 Module 中提供返回不同效果的实例。

举个栗子,我们需要不同功率的电热器(Heater), 然后我们程序如下:

电热器 Heater 类代码如下:

class Heater(val power: Int)

创建 Module 类 HeaterActivityModule,提供一个 36v 的低功率电热器,以及一个 220v 的高功率电热器,代码如下:

@Module
class HeaterActivityModule {
    @Provides
    fun provideLowPowerHeater(): Heater {
        Log.i("HeaterActivity", "provideLowPowerHeater")
        return Heater(36)
    }

    @Provides
    fun provideHighPowerHeater(): Heater {
    Log.i("HeaterActivity", "provideHighPowerHeater")
        return Heater(220)
    }
}

创建 Component 接口 HeaterActivityComponent,用于注入 HeaterActivity,并注册 HeaterActivityModule,代码如下:

@Component(modules = [HeaterActivityModule::class])
interface HeaterActivityComponent {
    fun inject(heaterActivity: HeaterActivity)
}

最后在 HeaterActivity 中添加两个需要依赖注入的变量 lowPowerHeater 和 highPowerHeater,代码如下:

class HeaterActivity : AppCompatActivity() {
    @Inject
    lateinit var lowPowerHeater: Heater
    @Inject
    lateinit var highPowerHeater: Heater

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_heater)
        DaggerHeaterActivityComponent.create().inject(this)
        Log.i("HeaterActivity", "lowPower: ${lowPowerHeater.power}")
        Log.i("HeaterActivity", "highPower: ${highPowerHeater.power}")
    }
}

接着我们运行程序,这个时候发现程序编译出错,查看日志,发生错误如下:

HeaterActivityComponent.java:9: 错误: com.np.daggerproject.
named.Heater is bound multiple times:...

从错误可以看出,Heater 对象被绑定了多次。为什么会出现这个错误呢?这是因为我们在 Module 类中提供了两个返回值都为 Heater 对象的方法,这就导致了绑定了 2 次。

那么我们该怎么解决这个问题呢?这个时候 @Named 注解就派上用场了。

我们只要将被注入类中的 Heater 变量通过 @Named 注解命名为不同的名字,然后在 Module 类中提供的方法上通过 @Named 注解一一对应上这些名字就可以成功了。

首先修改被注入类 HeaterActivity,代码如下:

class HeaterActivity : AppCompatActivity() {
    @Inject
    @field:Named("lowPower")
    lateinit var lowPowerHeater: Heater
    @Inject
    @field:Named("highPower")
    lateinit var highPowerHeater: Heater

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_heater)
        DaggerHeaterActivityComponent.create().inject(this)
        Log.i("HeaterActivity", "lowPower: ${lowPowerHeater.power}")
        Log.i("HeaterActivity", "highPower: ${highPowerHeater.power}")
    }
}

注意:这里使用的 Kotlin 语法写的,不能使用 @Name(value) 去标注该属性,而是应该使用 @field:Name(value) 去标注。因为在 Kotlin 中使用注解对属性进行标注时,从相应的 Kotlin 元素生成的 Java 元素会有多个,具体原因点这里. 否者将会报如下错误:

HeaterActivityComponent.java:9: 错误: com.np.daggerproject.named.Heater 
cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.

然后将 HeaterActivityModule 的代码修改如下:

@Module
class HeaterActivityModule {
    @Provides
    @Named("lowPower")
    fun provideLowPowerHeater(): Heater {
        Log.i("HeaterActivity", "provideLowPowerHeater")
        return Heater(36)
    }

    @Provides
    @Named("highPower")
    fun provideHighPowerHeater(): Heater {
    Log.i("HeaterActivity", "provideHighPowerHeater")
        return Heater(220)
    }
}

这个时候运行程序,输入日志如下:

I/HeaterActivity: provideLowPowerHeater
I/HeaterActivity: provideHighPowerHeater
    lowPower: 36
    highPower: 220
懒加载 Lazy 和 Provider

dagger.Lazy 和 javax.inject.Provider 接口都可以实现懒加载的效果。

Lazy 的使用

有时你需要一个懒惰地实例化的对象。对于任何有约束力的 T,你可以创建一个 Lazy 会推迟实例化,直到第一次调用 Lazy 的 get() 方法。如果 T 是单例,那么 Lazy 对于所有注射,它将是相同的实例 ObjectGraph。否则,每个注入站点将获得自己的 Lazy 实例。无论如何,对任何给定实例的后续调用 Lazy 将返回相同的底层实例 T。

class GrindingCoffeeMaker {
  @Inject Lazy lazyHeater;

  public void brew() {
    while (needsHeatering()) {
      // Heater 在第一次调用 get() 时创建一次,并缓存.
      // 以后每次调用 get() 都将使用缓存的值.
      lazyGrinder.get();
    }
  }
}
Provider 的使用

有时您需要返回多个实例而不是仅注入单个值。虽然你有几个选项(工厂,建筑商等),但有一个选择是注入 Provider 而不仅仅是 T。一个 Provider 每次调用 get() 方法都会执行绑定逻辑。如果该绑定逻辑是 @Inject 构造函数,则将创建新实例,但 @Provides 方法没有这样的保证(因为如果绑定逻辑是单例的,那么每次创建的都是同一个实例)。

class BigCoffeeMaker {
  @Inject Provider filterProvider;

  public void brew(int numberOfPots) {
    // ...
    for (int p = 0; p < numberOfPots; p++) {
        // 每次调用都将创建一个 Filter 对象
        maker.addFilter(filterProvider.get()); 
    }
  }
}
@Binds 的作用

@Binds 注解和 @Provides 注解的功能类似,它两者的不同之处在于,@Provides 注解可以提供第三方类和接口的注入,==@Binds 注解只能提供接口的注入,且只能注解抽象方法==。

使用 @Provides 提供接口的注入。

// 注入的类
interface IPresenter
class Presenter: IPresenter

// Module
@Module
class BindsPersonModule {
    @Provides fun providePresenter(): IPresenter {
        return Persenter()
    }
}

// Component 接口代码:
@Component(modules = [BindsPersonModule::class])
interface BindsComponent {
    fun inject(activity: BindsActivity)
}

// 将 IPresenter 注入的到 Activity 中:
class BindsActivity : AppCompatActivity() {
    // 注意这里是接口 IPresenter 的注入, 而非实现类.
    @Inject lateinit var presenter: IPresenter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_binds)
        DaggerBindsComponent.create().inject(this)
        Log.i("BindsActivity", presenter.toString())
    }
}

使用 @Binds 注解实现接口的注入, 只需修改在实现类 Presenter 的构造方法添加 @Inject 注解,以及使用修改 Moudle 为抽象类,并且使用 @Binds 注解提供 IPresenter 接口的实例化:

// 需要注入的类
interface IPresenter
// 注意 Presenter 子类的构造方法需要 @Inject 注解.
class Presenter @Inject constructor(): IPresenter

// Module: 注意必须使用抽象类或接口定义.
@Module
abstract class BindsPersonModule {
    @Binds
    abstract fun bindPresenter(presenter: Presenter): IPresenter
}

注意,使用 @Binds 注解暴露出去的方法,参数类必须是返回值类的子类,且方法只能有一个形参。

所以,当我们需要提供接口的注入时可以有使用 @Provides 注解和 @Binds 注解两种方法(因为 @Inject 注解不能注解接口)。

@BindsOptionalOf、Optional 的作用

可选的绑定:使用 @BindsOptionalOf 注解避免 Dagger2 中的 Nullable 依赖项。

我们知道如果某个变量标记了 @Inject,那么必须要为它提供实例,否则无法编译通过。但是现在我们可以通过将变量类型放入 Optional 泛型参数,则可以达到:即使没有提供它的实例,也能通过编译。

Optional这个类是什么呢?它的引入是为了解决Java中空指针的问题,您可以去这里了解一下:Java 8 Optional 类

直接来看一个咖啡的栗子,这里有一个杯子,杯子里可以有咖啡,也可以没有咖啡!

首先我们定义一个咖啡类 Coffee:

class Coffee

然后我们定一个抽象的 Module 类,用于将 Coffee 定义为可选的绑定。定义 @BindsOptionalOf 注解标记的,返回值为 Coffee 的抽象方法。

@Module
abstract class CModule {
    @BindsOptionalOf abstract fun optionalCoffee(): Coffee
}

然后我们再创建一个 Module 类,用于提供 Coffee 的实例。

@Module
class CoffeeModule {
    @Provides fun provideCoffee(): Coffee {
        return Coffee()
    }
}

然后定义杯子 Cup,用于测试 Coffee 是否为有值。

class Cup @Inject constructor() {
    @Inject
    lateinit var coffee: Optional
    @RequiresApi(Build.VERSION_CODES.N)
    fun coffeeIsNullable() {
        if (coffee.isPresent) {
            Log.i("Coffee", "杯子里有咖啡")
        } else {
            Log.i("Coffee", "杯子里没有咖啡")
        }
    }
}

接着定义 Component 接口,将 CModule 和 CoffeeModule 添加进去(也可将 CModule includes 进 CoffeeModule,然后只添加 CoffeeModule 到 Component 即可),注入杯子 Cup 实例。

@Component(modules = [CModule::class, CoffeeModule::class])
interface CupComponent {
    fun getCup(): Cup
}

最后在 BindsActivity 对其进行测试:

class BindsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_binds)
        val cup = DaggerCupComponent.create().getCup()
        cup.coffeeIsNullable()
    }
}

运行程序,然后输出接口如下:

I/Coffee: 杯子里有咖啡

如果我们将 CoffeeModule 中提供 Coffee 实例的方法注释掉:

@Module
class CoffeeModule {
//    @Provides fun provideCoffee(): Coffee {
//        return Coffee()
//    }
}

接着运行程序,发现程序编译通过,并且输出结果如下:

I/Coffee: 杯子里没有咖啡

Optional 除了上述写法以外,还可以使用以下写法:

  • Optional
  • Optional>
  • Optional>
  • Optional>>
@BindsInstance 的作用

绑定实例,大家可以想象一下:如果我们在提供实例的时候,需要在运行时提供参数去创建,那么该如何做呢?

我们可以使用 Builder 绑定实例来做!(当然我们也可以使用 Module 来传参, 但是这里主要讲解的是 @BindsInstance 注解)这里我们举例一个需要参数姓名 name 和性别 sex 才能创建的 User 对象。

User 类的构造属性 名字 name 和性别 sex 都是 String 类型,因此这里需要定义了两个 @Scope 注解来标识,否者 Dagger 不知道对应哪个参数,将编译不通过。

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Name

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Sex

然后创建一个 User 类,该类为需要提供的对象,在构造方法上用 @Inject 标识,并且由于姓名 name 和性别 sex 都属于 String 类型,所以我们需要 @Scope 注解标记一下区分:

class User @Inject constructor(@Name val name: String, @Sex val sex: String)

当然,如果构造 User 需要不同类型的参数或者只需一个参数,这里也可以不添加 @Scope 注解标记。

接着创建 Component 接口,这里是关键部分了;

  • 首先我们需要在该接口内部在定义 Builder 接口,该接口用 @Component.Builder 标记,表示该接口会由 Component 的 Builder 静态内部类实现。
  • 然后我们需要为 Builder 接口定义抽象方法 name() 和 sex(),加上注解 @BindsInstance,返回类型为 Builder。传入的参数需要用注解标识,去对应 User 构造参数。需要注意一点的就是 @BindsInstance 注解的方法只能有一个参数,如有多个参数就会报错。
  • 最后 UserComponent build(); 就是我们通常最后调用的那个 build() 方法,创建返回 Component 实例。
@Component
interface UserComponent {
    fun getUser(): User
    // 表示该接口会由 Component 的 Builder 静态内部类实现
    @Component.Builder
    interface Builder {
        @BindsInstance fun name(@Name name: String): Builder
        @BindsInstance fun sex(@Sex sex: String): Builder
        fun build(): UserComponent
    }
}

最后在 BindsActivity 中测试:

class BindsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_binds)
        val userComponent = DaggerUserComponent.builder()
                .name("张三").sex("男").build()
        val user = userComponent.getUser()
        Log.i("User", "name: ${user.name}, sex: ${user.sex}")
    }
}

运行程序,输出结果如下:

I/User: name: 张三, sex: 男
Set 注入 @IntoSet 和 @ElementsIntoSet

之前介绍的内容都是单个对象的注入,那么我们是否能将多个对象注入到容器中呢?首先是 Set。

直接看栗子,将图书添加图书馆的栗子,代码如下:

定义图书 Book:

class Book

定义 Module 类,使用 ==@IntoSet== 注解添加 Book 实例到 Set 集合中。

@Module
class LibraryModule {
    @Provides
    @IntoSet
    fun provideBook1(): Book {
        return Book()
    }

    @Provides
    @IntoSet
    fun provideBook2(): Book {
        return Book()
    }
}

然后定义 Component 接口,在其中定义注入 Set 的方法:

@Component(modules = [LibraryModule::class])
interface LibraryComponent {
    fun getBookSet(): Set
}
// 或者在 SetActivity 中注入 Set
@Inject lateinit var bookSet: Set

如果注入了多个 Set,就需要在注入点和 Module 类中用 @Qualifier 注解标记区分。

最后在 SetActivity 中测试结果:

class SetActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_set)
        val bookSet = DaggerLibraryComponent.create().getBookSet()
        bookSet.forEach(::println)
    }
}

运行程序,输出结果如下:

I/System.out: com.np.daggerproject.set.Book@2fb8215
    com.np.daggerproject.set.Book@1483fcc

当然我们也可以通过 ==@ElementsIntoSet== 注解一次性返回一个 Set 对象。

@Provides
@ElementsIntoSet
fun provideMultiBook(): Set {
    val bookSet = HashSet()
    bookSet.add(Book())
    bookSet.add(Book())
    return bookSet
}
Map 注入

当然,有 Set 注入,也应有 Map 注入,但是 Map 注入和 Set 注入约有不同,Map 注入需要添加 Key。

同样以图书添加到图书馆为栗子:

@Module
class LibraryModule {
    @Provides
    @IntoMap
    @StringKey("book1")
    fun provideBook1(): Book {
        return Book()
    }

    @Provides
    @IntoMap
    @StringKey("book2")
    fun provideBook2(): Book {
        return Book()
    }
}

@IntoSet 变成了 @IntoMap ,并且使用 @StringKey 注解提供了 String 类型的 key 值。

ps:Dagger 还提供了一些内置的 Key 类型,包括 ClassKey、IntKey、LongKey、StringKey。android 辅助包中也提供了 ActivityKey 等。

Component 接口和 MapActivity 类定义如下:

@Component(modules = [LibraryModule::class])
interface LibraryComponent {
    fun inject(activity: SetActivity)
}

class SetActivity : AppCompatActivity() {
    // 注入 Map, 如有多个, 需 @Qualifier 注解标识.
    @Inject lateinit var bookMap: Map
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_set)
        DaggerLibraryComponent.create().inject(this)
        bookMap.forEach {
            Log.i("Map", "${it.key} : ${it.value}")
        }
    }
}

运行程序,输出结果如下:

I/Map: book1 : com.np.daggerproject.set.Book@2fb8215
    book2 : com.np.daggerproject.set.Book@7f6f92a

虽然 Set 注入有 @ElementsIntoSet 注解注入 Set 对象,但是 Map 注入没有一次性注入多个的方法。

@MapKey 自定义 Map Key 注解

StringKey 的源码,StringKey 的 value 类型为 String ,应该是指定了 Key 的数据类型为String。而 StringKey 又被 @MapKey 注解,是不是表明该注解是 Map 的 Key 的注解呢?(IntKey/LongKey/ClassKey 都使被 @MapKey 注解的)

注释类型中声明的方法的返回类型,如果不满足指定的返回类型,那么编译时会报错:

  • 基本数据类型
  • String
  • Class
  • 枚举类型
  • 注解类型
  • 以上数据类型的数组

接下来,我们自定义一个以枚举为 Key 的注解:

我们首先创建一个名为 MyEnum 的枚举类:

enum class MyEnum {
    A, B, C
}

然后我们创建一个 Map key 注解 MyEnumKey:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class MyEnumKey(val value: MyEnum)

在 Module 中使用如下:

@Module
class LibraryModule {
    @Provides
    @IntoMap
    @MyEnumKey(MyEnum.A)
    fun provideBook1(): Book {
        return Book()
    }

    @Provides
    @IntoMap
    @MyEnumKey(MyEnum.B)
    fun provideBook2(): Book {
        return Book()
    }
}

最后在 MapActivity 中测试如下:

// Component 接口代码如下
@Component(modules = [LibraryModule::class])
interface LibraryComponent {
    fun inject(activity: MapActivity)
}

// MapActivity
class MapActivity : AppCompatActivity() {
    @Inject lateinit var bookMap: Map
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)
        DaggerLibraryComponent.create().inject(this)
        bookMap.forEach {
            Log.i("Map", "${it.key} : ${it.value}")
        }
    }
}

然后运行程序,输出结果如下:

I/Map: A : com.np.daggerproject.map.Book@48f131b
I/Map: B : com.np.daggerproject.map.Book@a9f8cb8

使用复合键值,这个厉害了,因为 map 的 key 又不能多个,如何复合键值?这里就不多讲了,如果需要,请使劲点这里, 并滑到文章最后。

参考文章链接

Dagger2 的深入分析与使用

你可能感兴趣的:(Dagger2 进阶使用)