強行來一波Dagger2使用介紹

JYcoder發表於2018-03-31

安卓基礎開發庫,讓開發簡單點。
DevRing & Demo地址github.com/LJYcoder/De…

學習/參考地址:
https://www.jianshu.com/p/cd2c1c9f68d4
https://blog.csdn.net/lisdye2/article/details/51942511
https://www.jianshu.com/p/24af4c102f62

前言

Dagger2已經出來挺久了,網上相關的教程也很多,但我還是堅持要寫這篇,做事要有始有終嘛(這應該是本系列介紹的最後一個框架了,也是本系列最難上手的一個框架)
由於接觸Dagger2的時間不長,有些理解可能也不到位,但我還是會盡我所能把它講清楚些,不當之處也請大家指出。

什麼是Dagger2

Dagger2是一個依賴注入(Dependency Injection)框架。

什麼又是依賴注入呢?

借別人的話來說,就是“目標類中所依賴的其他類的初始化過程,不是通過在目標類中編碼的方式完成,而是通過其他手段把已經初始化好的例項自動注入到目標類中”。

再換種方式來說,就是把類例項的初始化過程都挪至一個地方統一管理,具體需要哪個例項時,就從這個地方取出(注入到目標類中)。

使用Dagger2有什麼好處

知其然,然後要知其所以然。

1. 解耦

假設有一個A類,專案中很多地方都使用到它(在很多地方通過new A()對A例項進行了初始化)。然後由於需求變動,A的建構函式增加了一個引數。
好了,牽一髮而動全身,你需要把各個new A()的地方都進行修改。
但如果是使用Dagger2進行管理,你只需在類例項的供應端進行修改即可。

2. 讓功能實現更專注於功能實現

假設現在你需要呼叫A類的x()方法來實現某功能,但是A類的構造過程相當的複雜(這樣的例子可以參考GreenDao中獲取XXXDao、Retrofit中獲取Observable請求)

public void xxx(){
    E e = new E();
    D d = new D(e);
    C c = new C();
    B b = new B(c,d);
    A a = new A(b);
    a.x();
}
複製程式碼

結果6行程式碼中,構造例項a佔了5行,呼叫x()方法實現功能卻只佔了1行。
但如果使用Dagger2進行管理,將a例項的構造過程移至例項供應端,則功能實現模組的程式碼會變成這樣

@Injcet
A a;
public void xxx(){
    a.x();
}
複製程式碼

這就是所說的讓功能實現更專注於功能實現,而不必去管a例項的構造過程。

3. 更好地管理類例項

通常我們開發中會有兩種類例項:
一種是全域性例項(單例),它們的生命週期與app保持一致。
一種是頁面例項,它們的生命週期與頁面保持一致。
通過Dagger2,我們可以使用一個元件專門管理全域性類例項(也免去了單例的寫法,不用考慮餓漢懶漢什麼的);然後各個頁面也有各自元件去管理它們的頁面例項。
這樣不管是對於例項的管理,還是專案的結構,都會變得更加的清晰明瞭。

4. 逼格高

(這點可以略過...)
當你不認識Dagger2卻看著使用Dagger2的專案程式碼,很可能會感覺到一種茫然與高大上:
各種@Inject,@Provides,@Singleton,Lazy<>,Provider<>等是什麼鬼?
為什麼沒有例項化的程式碼?
為什麼明明是null卻不會報空指標?
等你學會使用Dagger2之後,你也可以來一波"高逼格"的程式碼。

當然,這裡也不得不提一下,使用Dagger2會增加程式碼量。所以如果是小專案/獨立開發,你也可以考慮不用,因為你可能有種失大於得的感覺。如果是大專案/團隊開發,使用後就得大於失了。


角色介紹

在講用法前,先對幾個重要角色進行了解。

例項需求端

一個類中,它包含了例項成員,在使用這些例項前,需要對它們進行初始化,但前面已經說了初始化的過程挪至其他地方。所以這個類的需求是,已經完成初始化的例項物件。 暫且把這樣的類稱為“例項需求端”。

例項供應端

進行例項初始化的地方,並對外供應所需的例項。

Dagger2中,供應端有兩種方式可以供應例項:(後面會介紹) 第一種:使用Module 第二種:使用@Inject標註建構函式

橋樑

例項供應端並不知道它要供應的例項交給誰。 這個時候就需要通過橋樑,將例項需求端與例項供應端聯絡在一起。

Dagger2中,Component就負責扮演橋樑的角色。

使用介紹

1. 初步使用

1.1 新增依賴

compile 'com.google.dagger:dagger:2.14.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.14.1'
複製程式碼

1.2 處理例項需求端

在例項需求端中,使用@Inject標註需要注入的例項變數。

public class UploadActivity extends AppCompatActivity{
    @Inject
    UploadPresenter mPresenter;
}
複製程式碼

1.3 處理例項供應端

前面說了,供應端有兩種方式可以供應例項。

方式一 使用Module

  • 使用@Module標註類,表示它為供應端
  • 在類中使用@Provides標註方法,表示它為提供例項的方法。 在該方法中對例項進行初始化並返回該例項。
@Module
public class UploadActivityModule {
    @Provides
    UploadPresenter uploadPresenter() {
        return new UploadPresenter();
    }
}
複製程式碼

方式二 使用@Inject標註建構函式

public class UploadPresenter{
    @Inject
    public UploadPresenter() {
    }
}
複製程式碼

注意

方式一的優先順序高於方式二,意思就是:

  • 在供應某例項時,會先通過方式一查詢是否存在返回該例項的的方法
  • 如果存在,則獲取例項並對外供應
  • 如果不存在,再通過方式二查詢是否存在@Inject標註的建構函式
  • 如果存在,則將通過該建構函式構造例項並對外供應
  • 如果不存在,那將報錯,因為無法供應所需的例項

1.4 搭建橋樑

  • 使用@Component標註介面,表示它為橋樑。
    (如果使用1.3.1的方式供應例項,則需在@Component(modules=xxx.class)中指明module。)
    一個Component可以沒有module,也可以同時有多個module。
  • 新增 void inject(例項需求端) 方法,表明例項供應端中的例項將交給該例項需求端。
    通過這個方法,查詢例項需求端中需要注入的例項有哪些(使用@Inject標註的那些例項),然後在例項供應端中獲取所需的例項,最後注入到例項需求端中
  • 用來注入的方法,它的方法名不一定要是inject,可以隨便取,一般都取inject。但該方法的返回型別必須為void
@Component(modules = UploadActivityModule.class)
public interface UploadActivityComponent {
    void inject(UploadActivity uploadActivity);
}
複製程式碼

1.5 編譯,注入

  • 完成以上幾步後,ReBuild一下專案以生成DaggerUploadActivityComponent類。
  • 在例項需求端中呼叫inject完成例項的注入
public class UploadActivity extends AppCompatActivity{
    @Inject
    UploadPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入例項
        DaggerUploadActivityComponent.builder()
            .build()
            .inject(this);

        //注入後即可呼叫mPresenter中的方法了
        mPresenter.xxx();
    }
}
複製程式碼

2. 接收外部引數

有些時候,例項的初始化需要接收外部引數才能完成,比如MVP中的Presenter往往需要傳入IView介面以便完成資料回撥。

現在UploadPresenter的建構函式發生了變動,需傳入IUploadView。

public class UploadPresenter{
    IUploadView mIView;
    public UploadPresenter(IUploadView iview) {
          mIView = iview;
    }
}
複製程式碼

下面介紹兩種方式來接收外部引數

2.1 方式一 通過Module的建構函式傳入

對例項供應端的module進行改造

@Module
public class UploadActivityModule {
    IUploadView mIView;
    public UploadActivityModule(IUploadView iview) {
        mIView = iview;
    }

    @Provides
    IUploadView iUploadView(){
         return mIView;
    }

    @Provides
    UploadPresenter uploadPresenter(IUploadView iview) {
        return new UploadPresenter(iview);
    }
}
複製程式碼
  • 新增了建構函式以便獲取外部引數IUploadView
  • uploadPresenter()方法新增了IUploadView引數
    • 到時構建UploadPresenter例項時,會在這個Module或者同個Component下的其他Module中查詢是否存在返回IUploadView的的方法
    • 如果存在,則通過該方法獲取IUploadView以構造UploadPresenter
    • 如果不存在,則通過1.3.2方式查詢是否存在@Inject標註的建構函式(當然不會存在,因為IUploadView是介面)
  • 不直接使用mIView來構造UploadPresenter是為了降低耦合度。uploadPresenter()方法只管獲取構造Presenter所需的引數,而該引數從哪來、以後會有哪些變動,交給iUploadView()方法處理即可。

利用Module建構函式傳入外部引數

public class UploadActivity implements IUploadView{
    @Inject
    UploadPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入例項
        DaggerUploadActivityComponent.builder()
            .uploadActivityModule(new UploadActivityModule(this))//通過建構函式傳入外部引數IUploadView
            .build()
            .inject(this);

        //注入後即可呼叫mPresenter中的方法了
        mPresenter.xxx();
    }

    //實現IUploadView介面的方法
    @Override
    public void onUploadSuccess() {
        //上傳成功
    }
    @Override
    public void onUploadFail() {
        //上傳失敗
    }
}
複製程式碼

2.2 方式二 通過Component傳入

對橋樑Component進行改造

@Component(modules = UploadActivityModule.class)
public interface UploadActivityComponent {
    void inject(UploadActivity uploadActivity);

    IUploadView iUploadView();

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder iUploadView(IUploadView iUploadView);

        UploadActivityComponent build();
    }
}
複製程式碼
  • 加入iUploadView()方法返回IUploadView
  • 加入了Builder來接收IUploadView

構建Component時傳入外部引數

public class UploadActivity extends AppCompatActivity implements IUploadView{
    @Inject
    UploadPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入例項
        DaggerUploadActivityComponent.builder()
            .iUploadView(this)
            .build()
            .inject(this);

        //注入後即可呼叫mPresenter中的方法了
        mPresenter.xxx();
    }

    //實現IUploadView介面的方法
    @Override
    public void onUploadSuccess() {
        //上傳成功
    }
    @Override
    public void onUploadFail() {
        //上傳失敗
    }
}
複製程式碼

3. 限定符註解 @Qualifier

@Qualifier主要是用於解決,因供應端存在多個型別相同的例項而引起歧義的問題。

3.1 使用@Named註解

Dagger2預設提供了一個@Named註解,從程式碼可以看出屬於@Qualifier的一種實現。

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
    String value() default "";
}
複製程式碼

下面舉個例子,現在頁面需有兩個Dialog,一個用於登入,一個用於註冊。

@Module
public class TestActivityModule {
    private Context mContext;

    public TestActivityModule(Context context){
          mContext = context;
    }

    @Provides
    Context context(){
          return mContext;
    }
    
    @Named("login")
    @Provides
    Dialog loginDialog(Context context){
         Dialog dialog = new Dialog(context);
         dialog.setTitle("登入提示");
         ....
         return dialog;
    }

    @Named("register")
    @Provides
    Dialog registerDialog(Context context){
         Dialog dialog = new Dialog(context);
         dialog.setTitle("註冊提示");
         ....
         return dialog;
    }
}
複製程式碼
  • 可以看到,在例項供應端中,需在提供Dialog例項的方法上面加@Named註解以區分。如果不加則會報錯,因為Dagger2不知道要使用哪個方法來獲取Dialog例項。
@Component(modules = TestActivityModule.class)
public interface TestActivityComponent {
    void inject(TestActivity testActivity);
}
複製程式碼
public class TestActivity extends AppCompatActivity{
    @Named("login")
    @Inject
    Dialog mDialogLogin;
    
    @Named("register")
    @Inject
    Dialog mDialogRegister;      

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入例項
        DaggerTestActivityComponent.builder()
            .testActivityModule(new TestActivityModule(this))
            .build()
            .inject(this);
    }    
}
複製程式碼
  • 在例項需求端中,同樣要使用@Named註解標註要注入的例項。讓Dagger2知道,
    @Named("login")標註的mDialogLogin,需要通過@Named("login")標註的供應方法來獲取。
    @Named("register")標註的mDialogRegister,需要通過@Named("register")標註的供應方法來獲取。

3.2 自定義@Qualifier註解

使用@Named註解的話,需要加入字串來區分,這樣比較麻煩也容易出錯。所以我們可以使用自定義的限定符註解。

@Qualifier
public @interface DialogLogin {
}
複製程式碼
@Qualifier
public @interface DialogRegister {
}
複製程式碼

然後把前面涉及的@Named("login")換成@DialogLogin,@Named("register")換成@DialogRegister即可~


4. 作用域註解 @Scope

@Scope的作用是使同一個Component中供應的例項保持唯一。

4.1 使用作用域註解實現區域性單例

舉例說明:

public class UploadActivity extends AppCompatActivity{
    @Inject
    UploadPresenter mPresenter1;
    @Inject
    UploadPresenter mPresenter2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入例項
        DaggerUploadActivityComponent.builder()
            .build()
            .inject(this);
    }
}
複製程式碼
  • 如果不使用作用域註解,則程式碼中的mPresenter1,mPresenter2將會是兩個不一樣的例項,可通過列印記憶體地址檢視。
  • 而如下使用作用域註解後,則兩者將會是同一個例項,可通過列印記憶體地址檢視。

步驟1 自定義@Scope註解

@Scope
@Documented
@Retention(RUNTIME)
public @interface ActivityScope {}
複製程式碼

步驟2 橋樑Component新增作用域註解

@ActivityScope 
@Component(modules = UploadActivityModule.class)
public interface UploadActivityComponent {
    void inject(UploadActivity uploadActivity);
}
複製程式碼

步驟3 供應端中提供例項的方法新增作用域註解

@Module
public class UploadActivityModule {
    @ActivityScope
    @Provides
    UploadPresenter uploadPresenter() {
        return new UploadPresenter();
    }
}
複製程式碼

如果是使用1.3.2方式提供例項,則在類上方新增作用域註解

@ActivityScope
public class UploadPresenter{
    @Inject
    public UploadPresenter() {
    }
}
複製程式碼

經過@Scope處理後,UploadActivity中的UploadPresenter例項將保持唯一。

4.2 使用作用域註解實現全域性單例

全域性單例,相信大家就很熟悉了,就是平時用餓漢懶漢等等寫的那種單例模式。
前面已經說過@Scope作用是使同一個Component中供應的例項保持唯一。
也就是說,如果我在另一個Activity中再建立了一個新的Component,那麼它所提供的UploadPresenter例項也將是新的。
這和我們理解的全域性單例並不一樣。
所以,要想實現全域性單例,那就要確保獲取例項的Component一直都是同一個。
如何實現呢?答案是建立一個Component用於提供全域性單例的例項,然後在Application中對該Component進行初始化,以後要獲取單例時,都統一通過它來獲取。

全域性性的例項供應端

@Module
public class AppModule {
    private Application mApplication;

    public AppModule (Application application){
          mApplication = application;
    }

    @Singleton
    @Provides
    Application application(){
          return mApplication;
    }

    @Singleton
    @Provides
    ActivityStackManager activityStackManager() {
        return new ActivityStackManager();
    }
}
複製程式碼

全域性性的橋樑

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
    ActivityStackManager activityStackManager();
    Application application();
}
複製程式碼

Application中初始化

public class MyApplication extends Application {

    public static AppComponent mAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();
       
        mAppComponent= DaggerAppComponent.builder()
              .appModule(new AppModule(this))
              .build();
    }
}
複製程式碼

每次都通過Application中的AppComponent獲取某例項,即可保證全域性單例

public class UploadActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
   
        MyApplication.mAppComponent.activityStackManager().pushOneActivity(this);     
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        MyApplication.mAppComponent.activityStackManager().popOneActivity(this);   
    }
}
複製程式碼
  • 使用了Dagger2預設提供的作用域註解@Singleton,通過原始碼可以發現它的實現其實和前面的@ActivityScope是一樣的。
  • 所以真正實現全域性單例的不是並@Singleton,而是使用每次獲取例項都通過同一個Component。
  • 但由於它的字面意思為單例,所以我們通常把它應用在全域性性的橋樑和例項供應端中。
  • 全域性性的Component中沒有加入inject方法來自動注入(當然你也可以這麼做,但全域性性的比較少這麼做),而是加入了activityStackManager()方法,供外部呼叫來獲取例項。

5. Lazy 和 Provider

假如你並不希望在呼叫inject()方法後,就對@Inject標註的例項進行初始化注入,而是希望在用到該例項的時候再進行初始化,那麼我們就可以使用Lazy和Provider來實現。

舉例說明(省略例項供應端和橋樑的程式碼)

public class UploadActivity extends AppCompatActivity{
    @Inject
    Lazy<UploadPresenter> mPresenter1;
    @Inject
    Provider<UploadPresenter> mPresenter2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入例項
        DaggerUploadActivityComponent.builder()
            .build()
            .inject(this);
    }

    public void xxx(){
         //呼叫Lazy的get()方法後才開始初始化Presenter並得到該例項
         //並且後面每次呼叫get()方法得到的例項都將是同一個。
         mPresenter1.get().xxx();

         //呼叫Provider的get()方法後才開始初始化Presenter並得到該例項
         //並且後面每次呼叫get()方法,都會重新呼叫供應端的供應方法來獲取新例項。
         mPresenter2.get().xxx();
    }
}
複製程式碼

注意: 如果使用了前面介紹的作用域註解@Scope控制了例項的唯一性,那麼即使多次呼叫Provider的get()方法,得到的依然是同一個例項。


6. 依賴 和 包含

假設供應端某個例項的初始化過程,需要用到全域性的Context(即Application),那麼我們可以通過橋樑間的依賴或包含,從全域性的AppComponent中獲取。

@Module
public class TestActivityModule {
    //需要使用到Application引數
    @Provides
    DbHelper dbHelper(Application application){
          return new DbHelper(application);
    }
}
複製程式碼

6.1 通過依賴實現(TestActivityComponent依賴AppComponent)

通過dependencies = xxx.classs指定要依賴的Component :

@Component(modules = TestActivityModule.class, dependencies = AppComponent.class)
public interface TestActivityComponent {
   void inject(TestActivity testActivity);
}
複製程式碼

被依賴的Component中需定義相關例項的獲取方法 :

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {

    Application application();
    ...
}
複製程式碼

初始化TestActivityComponent時需傳入依賴的Component

public class TestActivity extends AppCompatActivity{

    @Inject
    DbHelper mDbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        DaggerTestActivityComponent.builder()
            .appComponent(MyApplication.mAppComponent) //傳入依賴的Component
            .build()
            .inject(this);
    }    
}
複製程式碼

6.2 通過包含實現(AppComponent包含TestActivityComponent)

TestActivityComponent使用@Subcomponent註解而不是@Component

@Subcomponent(modules = TestActivityModule.class)
public interface TestActivityComponent {
    void inject(TestActivity testActivity);
}
複製程式碼

AppComponent中定義相關方法,用來包含和獲取SubComponent

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {

    TestActivityComponent addSub(TestActivityModule testActivityModule);
    ...
}
複製程式碼

通過AppComponent來獲取SubComponent,然後注入

public class TestActivity extends AppCompatActivity{

    @Inject
    DbHelper mDbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TestActivityComponent testActivityComponent = MyApplication.mAppComponent.addSub(new TestActivityModule());
        testActivityComponent .inject(this);
            
    }    
}
複製程式碼

6.3 依賴和包含使用小結

  • 通過橋樑Component之間的依賴或包含,可以獲取到其他橋樑所連線的供應端提供的例項。
  • 使用依賴實現的話(假設A依賴B),A需通過dependencies指定依賴B,B中需定義A所需的相關例項的獲取方法,A構造時需傳入B。
  • 使用包含實現的話(假設B包含A),A需使用@Subcomponent標註,B中需定義方法來包含/獲取A,A是通過呼叫B的方法來獲取的。
  • 具有依賴關係的兩個Component,它們的作用域註解@Scope必須不同,否則會引起歧義。

7. 一些Tips

7.1
在1.3.1和1.3.2介紹了兩種例項供應方式:使用Module(方式一)、使用@Inject標註建構函式(方式二)。

那什麼時候應該使用哪種方式呢? 假設現在供應端需要提供A類的例項

  • 當無法在A類的建構函式上加入@Inject時(比如一些第三方庫裡的類),則使用方式一提供A例項。
  • 當你希望在A類例項初始化時,A類中被@Inject標註的變數也被自動注入,則使用方式二提供A例項。

7.2
Module類可以宣告為abstract抽象,但相關的供應方法需宣告為static靜態方法。

7.3
如果module中的供應方法宣告瞭@Scope,那麼它所屬的component必須也宣告相同的@Scope。 但如果component宣告瞭@Scope,它的module的供應方法並不一定全都要宣告@Scope。

7.4
@inject標註的例項變數不能宣告為private,也不能為static,否則會編譯報錯。


相關文章