kotlin使用Dagger2

Dagger2有Google接手开发的一个基于JSR-330标准的依赖注入框架,它会在编译期间自动生成相关代码,负责依赖对象的创建,达到解耦目的。

kotlin中配置Dagger2

在app模块的build.gradle文件中进行如下配置,关于kapt的相关知识

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    ...
}

dependencies {
    ...
    implementation 'com.google.dagger:dagger:2.11'
    kapt 'com.google.dagger:dagger-compiler:2.11'
}

相关常用注解:

  • @Inject
  • @Component
  • @Module
  • @Provides
  • @Qualifier和@Named
  • @Scope和@Singleton

@Inject

@Inject注解只是JSR-330中定义的注解,在javax.inject包中。 这个注解本身并没有作用,它需要依赖于注入框架才具有意义,可以用来标记构造函数、属性和方法

标记构造函数

  • 被标记的构造函数可以有0个或多个依赖作为参数。
  • 同一个类中最多只可以标记一个构造函数。
class People @Inject constructor(val name:String = "Tom")

注意在kotlin中这种写法是不被允许的,因为这等价于java中的多个构造方法People(String name), People(),正确的写法应该是这样:

data class People constructor(val name: String) {
    @Inject
    constructor() : this("Tom")
}

标记属性

  • 被标记的属性不能是final的,kotlin中不能是val。
  • 被注入进的属性不能用private修饰(是Dagger2不支持,而非@Inject不支持)。
    @Inject
    lateinit var people:People

标记方法

  • 被标记的方法可以有0个或多个依赖作为参数。
  • 方法不能是抽象的。
class HomeActivity : AppCompatActivity() {
    private lateinit var people:People

    @Inject
    fun setPeople(people:People){
        this.people = people
    }
}

这种方法注入和属性注入并没有什么本质上的不同,实现效果也基本一样。还有一种做法是@Inject标记被注入类的某个方法,该方法会在类的构造方法之后接着被调用

data class People constructor(val name: String) {
    @Inject
    constructor() : this("Tom")

    init {
        println("init:$name")
    }

    @Inject
    fun hello(){
        println("hello:$name")
    }
}

class HomeActivity : AppCompatActivity() {
    @Inject
    lateinit var people:People

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)

        //执行相关注入操作
        ...
        println(people.toString())
    }
}

运行结果是这样的:

01-02 11:57:30.995 16601-16601/? I/System.out: init:Tom
01-02 11:57:30.995 16601-16601/? I/System.out: hello:Tom
01-02 11:57:30.995 16601-16601/? I/System.out: People(name=Tom)

@Component

可以理解为一个注射器,可以算是Dagger2中最核心的一个注解,用来标记一个接口或者抽象类。使用@Component标记的接口,会在编译时自动生成一个Dagger+类名的实现类实现依赖注入。在Component中一般可以定义两种方法:

  • Members-injection methods:
    该方法有一个参数,表示需要注入到的类,提醒Dagger在该类中寻找需要被注入的属性(被@Inject标记)。

void inject(SomeType someType);//无返回值
SomeType injectAndReturn(SomeType someType);//返回它的参数类型
等价于:
MembersInjector<SomeType> getMembersInjector();//使用MembersInjector.injectMembers方法注入

  • Provision methods:
    该方法没有参数,返回一个需要被注入(或被提供)的依赖。一般用于为其他Component提供依赖的时候。

SomeType getSomeType();
Provider<SomeType> getSomeTypeProvider();//可以通过Provider.get访问任意次
Lazy<SomeType> getLazySomeType();//通过Lazy.get第一次访问时创建实例,并在之后的访问中都访问同一个实例

@Component
interface HomeComponent {
    fun inject(activity: HomeActivity)
    fun injectAndReturn(activity: HomeActivity): HomeActivity
    fun getInjectors(): MembersInjector<HomeActivity>
    
    fun getPeople():People
}

事实上,了解到这里我们已经可以使用最简单的Dagger2用法,毕竟有了依赖和注射器,只需要注入就可以了,我们来看一个最简单的Dagger2实例,只使用@Inject和@Component来完成注入。
第一步:在需要被注入的类的构造方法上添加注解@Inject

class People @Inject constructor() {
    fun hello(){
        println("hello")
    }
}

第二步:编写一个注射器接口

@Component
interface HomeComponent {
    fun inject(activity: HomeActivity)
}

第三步:注入

class HomeActivity : AppCompatActivity() {
    @Inject
    lateinit var people:People

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)

        DaggerHomeComponent.builder()
                .build()
                .inject(this)//会在这句代码时执行注入的操作

        people.hello()
    }
}

03-01 14:30:23.425 3256-3256/? I/System.out: hello
//大功告成

当然,上面这种只是最简单的用法,如果需要传入一些非自定义类的实例就不适用了,毕竟你不能在第三方的类中加入@Inject注解。此时就需要用到@Module和@Provides注解。

@Module

用来标记类,为Component提供依赖,相当于告诉Component,如果需要依赖可以来找我,当然前提是在Component中配置了该Module。同时Module可以通过includes依赖其他的Module。

@Provides

用来标记Module中的方法,该方法的返回类型是你需要提供的依赖类型。
举个自己项目中的例子,我需要在presenter中创建一个pl2303对象,pl2303对象的创建又需要context和pl2303Interface,所以我们需要提供三个依赖,因为context在其他地方也要用,我们单独提出来:

@Module
class ContextModule(private var mContext: Context) {
    @Provides
    fun getContext() = mContext
}

pl2303Interface只有这一个地方要用:

@Module(includes = arrayOf(ContextModule::class))
class Pl2303Module(private var pl2303Interface: ActivityCallBridge.PL2303Interface) {
    @Provides
    fun providePl2303(mContext: Context): Pl2303 {
        return Pl2303(mContext, pl2303Interface)
    }
}

其中includes可以是多个,我们这里把ContextModule加进来,这样创建pl2303就只差一个pl2303Interface,这是个接口对象,不能new,从构造函数注入进来。接下来创建注射器:

@Component(modules = arrayOf(Pl2303Module::class))
interface MainPresenterComponent {
    fun inject(presenter: MainPresenter)
}

最后注入:

class MainPresenter(val view: MainContract.View) : MainContract.Presenter, ActivityCallBridge.PL2303Interface, LifecycleObserver {
   
    @Inject lateinit var pl2303: Pl2303

    init {
        DaggerMainPresenterComponent.builder()
                .contextModule(ContextModule(view.context))
                .pl2303Module(Pl2303Module(this))
                .build()
                .inject(this)
    }
}

如果在大型项目中,一个Component有很多的Module,那么不需要传入参数的Module是可以省略的,看一下官方的注释文档:

public static void main(String[] args) {
       OtherComponent otherComponent = ...;
       MyComponent component = DaggerMyComponent.builder()
           // required because component dependencies must be set(必须的)
           .otherComponent(otherComponent)
           // required because FlagsModule has constructor parameters(必须的)
           .flagsModule(new FlagsModule(args))
           // may be elided because a no-args constructor is visible(可以省略的)
           .myApplicationModule(new MyApplicationModule())
           .build();
     }

@Named和@Qualifier

@Named是@Qualifier的一个实现。有时候我们会需要提供几个相同类型的依赖(比如继承于同一父类),如果不做处理的话编译器就不知道我们需要的具体是哪一个依赖而报错,比如这样:

abstract class Animal

class Dog : Animal() {
    override fun toString(): String {
        return "dog"
    }
}

class Cat : Animal() {
    override fun toString(): String {
        return "cat"
    }
}

@Module
class AnimalModule {

    @Provides
    fun provideDog(): Animal = Dog()

    @Provides
    fun provideCat(): Animal = Cat()
}

data class Pet @Inject constructor(val pet: Animal)

这时候就需要标记一下来告诉编译器我们需要的是哪个依赖:

@Module
class AnimalModule {

    @Provides
    @Named("dog")
    fun provideDog(): Animal = Dog()

    @Provides
    @Named("cat")
    fun provideCat(): Animal = Cat()
}

data class Pet @Inject constructor(@Named("dog") val pet: Animal)

上面我们说了@Named只是@Qualifier的一个实现而已,所以我们也可以用@Qualifier来达到一样的效果,实际使用中也更推荐使用@Qualifier的方式,因为@Named需要手写字符串来进行标识,容易出错。
使用@Qualifier需要注意:

  • 创建一个自定义的Qualifier至少需要@Qualifier, @Retention(RUNTIME)这两个注解。
  • 可以有自己的属性。
    我们可以看一下@Named的源码来加深一下理解:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {

    /** The name. */
    String value() default "";
}

下面我们比葫芦画瓢来改造一下上面的例子:

@Module
class AnimalModule {

    @Provides
    @DogAnim
    fun provideDog(): Animal = Dog()

    @Provides
    @CatAnim
    fun provideCat(): Animal = Cat()
}

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

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

data class Pet @Inject constructor(@CatAnim val pet: Animal)

经测试依然是可以运行的。

Pet(pet=cat)

@Scope和@Singleton

A scope annotation applies to a class containing an injectable constructor and governs how the injector reuses instances of the type

@Scope是用来标记包含可注入构造函数的类或者提供注入依赖对象的类,简单来说,可以用来标记包含@Inject构造函数的类或者@Module类。
@Scope是用来管理依赖的生命周期的。它和@Qualifier一样是用来自定义注解的,而@Singleton和@Named类似,是@Scope的默认实现。
如果一个注射器和创建依赖对象的地方没有标记@Scope,那么每次注入时都会创建一个新的对象,如果标记了@Scope,则在规定的生命周期内会使用同一个对象,特别注意是在规定的生命周期内单例,并不是全局单例,或者可以理解为在@Component内单例。
还是借助上面的例子:

data class People constructor(val name: String) {
    @Inject
    constructor() : this("Tom")

    init {
        println("init:$name")
    }

    @Inject
    fun hello(){
        println("hello:$name")
    }
}

class HomeActivity : AppCompatActivity() {
    @Inject
    lateinit var people: People
    @Inject
    lateinit var people_2: People

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)

        DaggerHomeComponent.builder()
                .build()
                .inject(this)

        println("people===people_2:${people===people_2}")
    }
}

运行结果:

people===people_2:false

说明确实是两个不同的对象,接下来我们改造一下:

@Singleton
data class People constructor(val name: String) {
    ...//和之前一样
}

@Singleton
@Component(modules = arrayOf(AnimalModule::class))
interface HomeComponent {
    fun inject(activity: HomeActivity)
}

...//HomeActivity代码和之前一样

再次看下运行结果:

people===people_2:true

说明这次两次都是访问的同一个对象。上面提到这只是一个局部单例,那么怎么实现一个全局单例呢,很简单,只要保证标记的Component在全局只初始化一次即可,比如在Application中初始化,篇幅限制代码就不贴了,有兴趣的骚年可以自己实践一下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • - 对博鳌最初的记忆是-博鳌亚洲论坛,不是因为这是个盛会,而是因为任泉参加过,我是迷妹。 12月底从长沙跑到海南去...
    会儿儿儿儿儿阅读 315评论 0 0
  • 大年初一,2018年的第一天,还在值班中,医院静悄悄的,一年之中没有哪一天能比今天更安静了。即使没有病人,我...
    小女子仗剑走天涯阅读 249评论 3 0
  • 好久没发文了,因为好久没静静地思考人生,最近,“90后佛系青年”荣登热榜,今天,我们来聊聊糊涂人生。 前两天看了一...
    JandenMa阅读 543评论 2 3
  • 你好,六方来财 看过一部电影,《朱莉与朱莉娅》,突发奇想,想学她写东西,今天是第一天,2017年12月28日。女主...
    文半子sky阅读 225评论 0 0