Dagger2的使用

fondtiger發表於2021-09-09
Dagger2是什麼?

Dagger2是Dagger的升級版,是一個依賴注入框架,第一代由大名鼎鼎的Square公司共享出來,第二代則是由谷歌接手後推出的,現在由Google接手維護。(Dagger2是Dagger1的分支,但兩個框架沒有嚴格的繼承關係,亦如Struts1 和Struts2 的關係!)

那麼,什麼是依賴注入?

依賴注入是物件導向程式設計的一種設計模式,其目的是為了降低程式耦合,這個耦合就是類之間的依賴引起的.

舉個例子:

    public class ClassA{
        private ClassB b        public ClassA(ClassB b){        this.b = b    }
    }

這裡ClassA的建構函式里傳了一個引數ClassB,隨著後續業務增加也許又需要傳入ClassC,ClassD.試想一下如果一個工程中有5個檔案使用了ClassA那是不是要改5個檔案?

這既不符合開閉原則, 也太不軟工了.這個時候大殺器Dagger2就該出場了.

  public class ClassA{     @inject 
      private ClassB b 
      public ClassA(){
       }
    }

透過註解的方式將ClassB b注入到ClassA中, 可以靈活配置ClassA的屬性而不影響其他檔案對ClassA的使用.

那就有人問了,為什麼要用Dagger2?

回答:解耦(DI的特性),易於測試(DI的特性),高效(不使用反射,google官方說名比Dagger快13%),易混淆(apt方式生成程式碼,混淆後依然正常使用)

如何使用Dagger2
環境配置

這裡以Gradle配置為例子,實用得是AndroidStudio3.2:
當AndroidStudio升級到3.0後,同時也更新了gradle到4.1後,需要 去除 掉project的build.gradle配置(本文都是kotlin寫法)

//classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

app module 的 build.gradle裡的

//apply plugin: 'com.neenbedankt.android-apt'apply plugin: 'kotlin-kapt'

開啟app module 的 build.gradle ,新增

apply plugin: 'com.android.application'apply plugin: 'kotlin-android'apply plugin: 'kotlin-kapt'//...dependencies {    //...
    implementation 'com.google.dagger:dagger:2.16'
    //annotationProcessor 'com.google.dagger:dagger-compiler:2.16'
    kapt 'com.google.dagger:dagger-compiler:2.16'}
Dagger2常用的註解:
  1. @Inject

  2. @Module, @Provides

  3. @Component

  4. @Singleton

  5. @Named, @Qualifier

  6. Lazy, Provider

  7. @Scope

示例

下面我們來看一個示例,實用Dagger2到底是怎麼依賴注入的。

現在有一個Person類,然後MainActivity中又一個成員變數person。

class Person {    constructor() {
        Log.i("dagger2: ", "a person created")
    }
}
class MainActivity : AppCompatActivity() {
    var person: Person? = null
    override fun onCreate(savedInstanceState: Bundle?) {            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            person = Person()
    }
}

如果不適用依賴注入,那麼我們只能在MainActivity中自己new一個Person物件,然後使用。

使用依賴注入:

class MainActivity : AppCompatActivity() {
    @Inject
    @JvmField
    var person: Person? = null

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

kotlin寫法需要新增@JvmField,或者使用關鍵字 lateinit 修飾,不然會報錯:

Dagger does not support injection into private fields
    private com.example.ghp.dagger2testdemo.Person person;

那麼問題來了,就一個@Inject 註解,系統就會自動給我建立一個物件? 當然不是,這個時候我們需要一個Person類的提供者Module

@Moduleclass MainModule {    @Provides
    fun providesPerson(): Person {
        Log.i("dagger2: ","a person created from MainModule")        return Person()
    }
}

裡面兩個註解,@Module 和 @Provides,Module標註的物件,你可以把它想象成一個工廠,可以向外提供一些類的物件。
同時需要引入component容器。
可以把它想成一個容器, module中產出的東西都放在裡面,然後將component與我要注入的MainActivity做關聯,MainActivity中需要的person就可以衝 component中去去取出來。

@Component(modules = [(MainModule::class)])interface MainComponent {
    fun inject(mainActivity: MainActivity)//表示怎麼和要注入的類關聯}

看到一個新注入 @Component 表示這個介面是一個容器,並且與 MainModule.class 關聯,它生產的東西都在這裡。

然後在MainActivity中將component 關聯進去:

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

        var component: MainComponent  = DaggerMainComponent.builder()
                .mainModule(MainModule()).build()
        component.inject(this)
    }

然後執行專案,檢視log:

"a person created from MainModule""a person created"

說明建立了物件,並且注入到MainActivity中。
上面有一個DaggerMainComponent,是在build的過程中,APT(就是dagger-compiler)掃描到註解(@Component@Module)生成的具體的component類(命名方式是Dagger+類名).這個過程用下面這張圖表示:


圖片描述

dagger build

單例模式 @Singleton(基於Component)

上面的MainActivity程式碼不變,我們再在MainActivity中新增一個 @Inject @JvmField  var person: Person? = null,並列印兩個 person物件,結果如下:

"a person created from MainModule""a person created""a person created from MainModule""a person created"

發現person會被建立兩次,並且兩個person物件也不同,如果我們希望只有一個 person 和 person2 都指向同一個Person物件了, 使用 @Singleton 註解

兩個地方需要新增:

@Moduleclass MainModule {
    @Singleton
    @Provides
    fun providesPerson(): Person {
        Log.i("dagger2: ","a person created from MainModule")        return Person()
    }
}

@Singleton
@Component(modules = [(MainModule::class)])interface MainComponent {
    fun inject(mainActivity: MainActivity)
}

再執行,發現只建立了一次,並且兩個person指向同一個物件。

需要非常注意的是:
單例是基於Component的,所以不僅 Provides 的地方要加 @Singleton,Component上也需要加。並且如果有另外一個OtherActivity,並且建立了一個MainComponent,也注入Person,這個時候 MainActivity和OtherActivity中的Person是不構成單例的,因為它們的Component是不同的。

帶有引數的依賴物件

如果構造Person類,需要一個引數Context,我們怎麼注入呢? 要知道注入的時候我們只有一個 @Inject 註解,並不能帶引數。所以我們需要再 MainModule 中提供context,並且由 providesXXX 函式自己去構造。如:

class Person {
    var context: Context? = null
    constructor(context: Context) {        this.context = context
        Log.i("dagger2: ", "a person created with context")
    }
}@Moduleclass MainModule {
    var context: Context    constructor(context: Context){        this.context = context
    }    @Provides
    fun providersContext(): Context {        return this.context
    }    
    @Singleton
    @Provides
    fun providesPersonWithContext(context: Context): Person {
        Log.i("dagger2: ","a person created from WithContext")        return Person(context)
    }
}

這裡需要強調的是, providesPerson(Context context)中的 context,不能直接使用 成員變數 this.context,而是要在本類中提供一個 Context providesContext() 的 @Provides 方法,這樣在發現需要 context 的時候會呼叫 provideContext 來獲取,這也是為了解耦。

依賴一個元件

如果元件之間有依賴,比如 Activity 依賴 Application一樣,Application中的東西,Activity要直接可以注入,怎麼實現呢?

例如,現在由 AppModule 提供Context物件, ActivityModule 自己無需提供Context物件,而只需要依賴於 AppModule,然後獲取Context 物件即可。

@Moduleclass AppModule {    var context: Context

    constructor(context: Context){
        this.context = context;
    }

    @Provides
    fun providesContext(): Context {        return context
    }
}

@Component(modules = [(AppModule::class)])interface AppComponent {
    fun getContext(): Context
}

@Moduleclass ActivityModule {
    @Provides
    @Singleton
    fun providePerson(context: Context): Person {        return Person(context)
    }
}

@Singleton
@Component(dependencies = [(AppComponent::class)], modules = [(ActivityModule::class)])interface ActivityComponent {
    fun inject(mainActivity: MainActivity)
}

透過上面例子,我們需要注意:

  1. ActivityModule 也需要建立Person時的Context物件,但是本類中卻沒有 providesContext() 的方法,因為它透過 ActivityComponent依賴於 AppComponent,所以可以透過 AppComponent中的 providesContext() 方法獲取到Context物件。

  2. AppComponent中必須提供 Context getContext(); 這樣返回值是 Context 物件的方法介面,否則ActivityModule中無法獲取。

使用方法:
一定要在 activityComponent中注入 appComponent 這個它依賴的元件。我們可以看到,由於AppComponent沒有直接和 MainActivity發生關係,所以它沒有 void inject(...);這樣的介面

    var appComponent: AppComponent = DaggerAppComponent.builder()
            .appModule(AppModule(this))
            .build()    var activityComponent: ActivityComponent = DaggerActivityComponent.builder()
            .appComponent(appComponent)
            .activityModule(ActivityModule())
            .build()
    activityComponent.inject(this)
自定義標記 @Qualifier 和 @Named

如果Person中有兩個構造方法,那麼在依賴注入的時候,它怎麼知道我該呼叫哪個構造方法呢?

修改Person類,兩個不同的構造方法

class Person {    var context: Context? = null
    var name: String? = null
    constructor(context: Context) {        this.context = context
        Log.i("dagger2: ", "a person created with context")
    }    constructor(name: String) {        this.name = name
        Log.i("dagger2: ", "a person created with name")
    }
}

有兩種方法可以解決這個問題:

@Named(“…”)和@Qualifier自定義標籤

使用@Named 會使用到 字串 ,如果兩邊都必須寫對才能成功,並且字串總是不那麼優雅的,容易出錯,所以我們可以自定義標籤來解決上面的問題。
下面是2者的區別使用,@Named的使用同步註釋

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class PersonWithContext@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class PersonWithName
@Moduleclass MainModule {
    var context: Context    constructor(context: Context){        this.context = context
    }    @Provides
    fun providersContext(): Context {        return this.context
    }//    @Named("context")
    @PersonWithContext
    @Singleton
    @Provides
    fun providesPersonWithContext(context: Context): Person {
        Log.i("dagger2: ","a person created from WithContext")        return Person(context)
    }//   @Named("string")
    @PersonWithName
    @Singleton
    @Provides
    fun providersPersonWithName(): Person {
        Log.i("dagger2: ","a person created from WithName")        return Person("ghp")
    }
}

分別在兩個提供Person的provides方法上新增 @Named標籤或者自定義標籤,並指定。

然後在要依賴注入的地方,同樣新增 @Name 或自定義標註表示要注入時使用哪一種

 //   @field:Named("context")
    @field:PersonWithContext    @Inject
    @JvmField
    var person: Person? = null//    @field:Named("string")
    @field:PersonWithName    @Inject
    @JvmField
    var person1: Person? = null

從上面程式碼可以看出,在使用標籤時@field:,不然會報錯:

cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method

變數編譯為 Java 位元組碼的時候會對應三個目標元素,一個是變數本身、還有 getter 和 setter,Kotlin 不知道這個變數的註解應該使用到那個目標上。
要解決這個方式,需要使用 Kotlin 提供的註解目標關鍵字來告訴 Kotlin 所註解的目標是那個,上面示例中需要註解應用到 變數上,所以使用 field 關鍵字

懶載入Lazy和強制重新載入Provider

在注入時分別使用 Lazy 和 Provider 修飾要注入的物件:

    @Inject
    var lazyPerson: Lazy<Person>? = null

     @Inject
    var providerPerson: Provider<Person>? = null

在使用的地方:

        var person: Person? = lazyPerson?.value        var person2: Person? = providerPerson?.get()

lazyPerson 多次get 的是同一個物件,
providerPerson多次get,每次get都會嘗試建立新的物件。

@Scope 自定義生命週期

透過前面的例子,我們遇到了 @Singleton 這個標籤,它可以保證在同一個Component中,一個物件是單例物件。其實可以跟進去看程式碼:
Singleton.java

@Scope@Documented@Retention(RUNTIME)public @interface Singleton {}

利用單例和元件間依賴的關係,我們也可以定義生命週期來滿足我們的需求呢,比如Activity 這樣的生命週期

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

除了名字,其他都和 @Singleton 是一樣的。
然後用ActivityScope 修飾 ActivityModule和ActivityComponent

@Moduleclass ActivityModule {
    @ActivityScope
    @Provides//    @Singleton
    fun providePerson2(context: Context): Person2 {        return Person2(context)
    }
}

@ActivityScope//@Singleton@Component(dependencies = [(AppComponent::class)], modules = [(ActivityModule::class)])interface ActivityComponent {
    fun inject(main2Activity: Main2Activity)
}



作者:_九卿_
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2508/viewspace-2817507/,如需轉載,請註明出處,否則將追究法律責任。

相關文章