目录:
- @Qualifier @Named 注解的作用
- 懒加载 Lazy 和 Provider
- @Binds 的作用
- @BindsOptionalOf、Optional 的作用
- @BindsInstance 的作用
- Set 注入
- 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 又不能多个,如何复合键值?这里就不多讲了,如果需要,请使劲点这里, 并滑到文章最后。