美文网首页
Dagger2 进阶使用

Dagger2 进阶使用

作者: panning | 来源:发表于2018-08-03 18:12 被阅读0次
    目录:
    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<T> 会推迟实例化,直到第一次调用 Lazy<T> 的 get() 方法。如果 T 是单例,那么 Lazy<T> 对于所有注射,它将是相同的实例 ObjectGraph。否则,每个注入站点将获得自己的 Lazy<T> 实例。无论如何,对任何给定实例的后续调用 Lazy<T> 将返回相同的底层实例 T。

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

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

    class BigCoffeeMaker {
      @Inject Provider<Filter> 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<T> 泛型参数,则可以达到:即使没有提供它的实例,也能通过编译。

    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<Coffee>
        @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<Coffee>
    • Optional<Provider<Coffee>>
    • Optional<Lazy<Coffee>>
    • Optional<Provider<Lazy<Coffee>>>
    @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<Book> 的方法:

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

    如果注入了多个 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<Book> {
        val bookSet = HashSet<Book>()
        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<String, Book>
        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<MyEnum, Book>
        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 进阶使用

          本文链接:https://www.haomeiwen.com/subject/ekeuvftx.html