【Android】我的Dagger2學習歷程:從一頭霧水到恍然大悟

再見理想2017發表於2017-01-09

前言

關於Dagger2的教程在網上已經有很多很多了,對於使用和原理都講得比較明白,但是對於第一次接觸的人們來說(比如我),難免會看得一頭霧水,所以這裡我就記錄一下我學習Dagger2的過程,分享最快速的學習方法給大家。

介紹

Dagger2是一個依賴注入的框架,什麼是依賴注入?簡單的來說就是類中依賴的物件只要宣告就可以使用了,它的建立由框架來管理。如下程式碼所示,application直接就可以拿來用了。

public class LoginActivity extends Activity{

    @Inject
    Application application;
}複製程式碼

開始

剛開始接觸Dagger2的時候大量閱讀了網上的教程,主要是一些概念性的東西,篇幅長了一下就看暈了,所以這裡推薦大家直接看程式碼。把程式碼執行起來,結合文章和程式碼一起看,相信你很快就能上手了。
gitHub地址

在主專案的build.gradle中新增

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'複製程式碼

在Module的build.gradle中新增

compile 'com.google.dagger:dagger:2.6'
apt 'com.google.dagger:dagger-compiler:2.6'複製程式碼

@Component

首先這個先定義一個全域性的AppComponent,為什麼需要全域性的AppComponent呢?因為這裡放得都是一些公共的物件,它們的生命週期是和Application一致的。通常情況下一個專案定義一個AppComponent,其他每個Activity或Fragment會對應一個Component。

@Singleton
@Component(modules = {AppModule.class, HttpModule.class, ApiServiceModule.class, DBModule.class})
public interface AppComponent {

    Application application();

    Gson gson();
    //網路訪問Service
    ServiceManager serviceManager();
    //資料庫訪問
    DBManager DBManager();
}複製程式碼

AppComponent是一個介面,裡面定義了提供依賴的方法宣告,這個AppComponent提供了Application、Gson、ServiceManager、DBManager依賴(方法名沒有限制),dagger框架會根據物件型別把它們注入到需要使用它們的地方。
那這些提供的物件是從哪裡來的呢?總不能憑空產生吧,這就要看module了,AppComponent中引用的AppModule、HttpModule、ApiServiceModule、DBModule,現在我們進入AppModule中看看。

@Module & @Provides

@Module
public class AppModule {

    public AppModule(HuiApplication application) {
        this.mApplication = application;
    }

    private Application mApplication;

    @Singleton
    @Provides
    public Application provideApplication() {
        return mApplication;
    }

    @Singleton
    @Provides
    public Gson provideGson() {
        return new Gson();
    }

}複製程式碼

可以看到Module就是提供這些依賴的地方,dagger會根據@Provides標記的方法返回依賴物件,這個AppModule中提供了Application和Gson物件的建立。可能大家都注意到@Singleton這個註解了吧,如果需要讓依賴物件是單例的話標註一下就可以,後面還會提到。

如果你的provide方法裡需要用到其他provide提供的物件,可以直接通過方法引數傳進來,如下所示,provideRetrofit()方法中需要用到OkHttpClient和HttpUrl,直接傳進來就可以了。

@Module
public class HttpModule {
    @Singleton
    @Provides
    Retrofit provideRetrofit(OkHttpClient client, HttpUrl baseUrl) {
        return new Retrofit.Builder()
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .baseUrl(baseUrl)
                .build();
    }

    @Singleton
    @Provides
    OkHttpClient provideOkHttpClient() {

        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS)
                .readTimeout(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(DEFAULT_WRITE_TIMEOUT, TimeUnit.SECONDS);

        return builder.build();
    }

}複製程式碼

AppComponent

現在需要建立AppComponent,因為這是全域性的Component,自然是在Application中建立了:

public class HuiApplication extends Application{

    private AppComponent mAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        mAppComponent = DaggerAppComponent
                .builder()
                .appModule(new AppModule(this))
                .apiServiceModule(new ApiServiceModule())
                .dBModule(new DBModule())
                .httpModule(new HttpModule())
                .build();
    }


    public AppComponent getAppComponent() {
        return mAppComponent;
    }
}複製程式碼

bulid一下專案,dagger2會為每個component建立Dagger+Component名的類,該類中會提供建立和設定每一個module例項的方法,可以看到這裡的module是我們自己new了傳進去的,我們可以為各個module做一些初始化的處理。這裡為AppComponent建立一個get方法,接下來就要到Activity了。上面說了通常情況下每個Activity會對應一個Component,那現在就為LoginActivity建立一個LoginComponent:

@dependencies

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity activity);
}複製程式碼

首先看到@dependencies,這裡就把AppComponent中提供的一些物件依賴了過來,實現了全域性共用。同時宣告一個inject方法,引數是你要注入到的類(方法名詞不限,這裡用inject比較形象)。現在就看看LoginActivity是如何注入的:

@Inject

public class LoginActivity extends Activity {

    @Inject
    ServiceManager serviceManager;
    @Inject
    DBManager DBManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        HuiApplication mApplication = (HuiApplication) getApplication();
        setupActivityComponent(mApplication.getAppComponent());
        initData();
    }


    @Override
    protected void setupActivityComponent(AppComponent appComponent) {
        DaggerLoginComponent
                .builder()
                .appComponent(appComponent)
                .build()
                .inject(this);
    }
}複製程式碼

同樣定義了LoginComponent後會自動生成DaggerLoginComponent,這裡從Application中獲取之前建立的AppComponent,最後呼叫inject,注入就完成了。此時使用@Inject來標記成員變數就可以使用了,有沒有感覺很神奇?此時你可能會覺得疑惑,接下來就開始對上面使用的一些註解和方法進行講解。

概念

dagger2是什麼?

Dagger2是一款基於Java註解來實現的完全在編譯階段完成依賴注入的開源庫,主要用於模組間解耦、提高程式碼的健壯性和可維護性。Dagger2在編譯階段通過apt利用Java註解自動生成Java程式碼,然後結合手寫的程式碼來自動幫我們完成依賴注入的工作。

下圖能很好地展示Dagger2這幾個註解的作用:

【Android】我的Dagger2學習歷程:從一頭霧水到恍然大悟

註解介紹

@Component
用於標註介面,是依賴需求方和依賴提供方之間的橋樑。被Component標註的介面在編譯時會生成該介面的實現類(Dagger+Component名字),我們通過呼叫這個實現類的方法完成注入;Component介面中主要定義一些提供依賴的宣告

@Inject有三個作用
一是用來標記需要依賴的變數,以此告訴Dagger2為它提供依賴;
二是用來標記建構函式,Dagger2通過@Inject註解可以在需要這個類例項的時候來找到這個建構函式並把相關例項構造出來,以此來為被@Inject標記了的變數提供依賴,例如:

public class Student {

    private String name;
    private int age;

    @Inject
    public Student() {

    }
}複製程式碼

三是用來標記普通方法,該方法會在物件注入完成之後呼叫,可以根據這一特性因此做一些初始化的工作。

@Module
@Module用於標註提供依賴的類。你可能會有點困惑,上面不是提到用@Inject標記建構函式就可以提供依賴了麼,為什麼還需要@Module?很多時候我們需要提供依賴的建構函式是第三方庫的,我們沒法給它加上@Inject註解,又比如說提供以來的建構函式是帶引數的,如果我們之所簡單的使用@Inject標記它,那麼他的引數又怎麼來呢?@Module正是幫我們解決這些問題的。

@Provides
@Provides用於標註Module所標註的類中的方法,該方法在需要提供依賴時被呼叫,從而把預先提供好的物件當做依賴給標註了@Inject的變數賦值;

@Scope
@Scope同樣用於自定義註解,我能可以通過@Scope自定義的註解來限定註解作用域,實現區域性的單例;比如我們前面使用到的@ActivityScope:

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

如果需要提供區域性單例支援,則需要在Component中和@provides註解的方法上@ActivityScope,這裡說的區域性單例的意思是在該Component中是唯一的,如果Component是全域性唯一的話就是全域性單例了,比如AppComponent。

@Singleton
@Singleton其實就是一個通過@Scope定義的註解,我們一般通過它來標記全域性單例(AppComponent)。

我們提到@Inject和@Module都可以提供依賴,那如果我們即在建構函式上通過標記@Inject提供依賴,有通過@Module提供依賴Dagger2會如何選擇呢?具體規則如下:
步驟1:首先查詢@Module標註的類中是否存在提供依賴的方法。
步驟2:若存在提供依賴的方法,檢視該方法是否存在引數。
a:若存在引數,則按從步驟1開始依次初始化每個引數;
b:若不存在,則直接初始化該類例項,完成一次依賴注入。
步驟3:若不存在提供依賴的方法,則查詢@Inject標註的建構函式,看建構函式是否存在引數。
a:若存在引數,則從步驟1開始依次初始化每一個引數
b:若不存在,則直接初始化該類例項,完成一次依賴注入。

Dagger2遇上MVP

如果你的專案是採用MVP架構的,那麼結合Dagger2將會是一件非常棒的體驗,它讓M-V-P進一步解藕,架構更清晰。
在上面的LoginActivity基礎上實現MVP模式。

LoginContract

MVP介面契約類,定義view和model的介面

public interface LoginContract {

    interface View extends BaseView {

        /**
         * 登入成功
         * @param result
         */
        void loginSuccess(String result);
    }

    interface Model extends IModel {

        /**
         * 登入
         * @param mobile
         * @param password
         */
        Observable<String> login(String mobile, String password);

    }
}複製程式碼

LoginModule

定義Module,只要提供view和model的依賴,可以看到LoginModel是通過方法引數注入進來的,這樣model和view就解耦了。

@Module
public class LoginModule {
    private LoginContract.View view;

    public LoginModule(LoginContract.View view) {
        this.view = view;
    }

    @ActivityScope
    @Provides
    LoginContract.View provideLoginView() {
        return this.view;
    }

    @ActivityScope
    @Provides
    LoginContract.Model provideLoginModel(LoginModel model) {
        return model;
    }
}複製程式碼

LoginComponent

在之前的loginComponent中新增LoginModule

@ActivityScope
@Component(modules = LoginModule.class, dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity activity);
}複製程式碼

LoginActivity

在LoginActivity中初始化LoginComponent,我們就從這裡開始看看MVP的依賴是怎麼行程的:

public class LoginActivity extends BaseActivity<LoginPresenter> implements LoginContract.View {

    @Override
    protected void setupActivityComponent(AppComponent appComponent) {
        DaggerLoginComponent
                .builder()
                .appComponent(appComponent)
                .loginModule(new LoginModule(this))
                .build()
                .inject(this);
    }
}複製程式碼

我們的view就是當前的Activity,所以new LoginModule(this)這裡就提供了view的依賴。
這裡定義了BaseActivity並使用了泛型,設定成當前介面的presenter,這裡可以知道注入的過程是在BaesActivity中完成的,現在看看BaseActivity:

public abstract class BaseActivity<P extends BasePresenter> extends AppCompatActivity {

    ....省略程式碼

    @Inject
    protected P mPresenter;

    ....省略程式碼
}複製程式碼

BaseActivity中其實做的操作很簡單,通過@Inject註解將對應的Presenter注入進來。這樣在LoginActivity中就可以使用該Presenter了,現在我們看看LoginPresenter的實現。

LoginPresenter

@ActivityScope
public class LoginPresenter extends BasePresenter<LoginContract.Model, LoginContract.View> {

    @Inject
    public LoginPresenter() {
    }

}複製程式碼

Presenter中和Actvity的做法基本類似,物件的注入還是放在父類裡面,通過泛型的方式確定型別。這裡可以看到這裡有一個@Inject標註的空構造方法,這個是必須的,為了就是在LoginActivity中可以依賴到該Presenter。

public class BasePresenter<M extends IModel, V extends BaseView> implements IPresenter {
    @Inject
    protected M mModel;
    @Inject
    protected V mView;
}複製程式碼

BasePresenter裡面就是View和Model的注入。
model的實現和presenter的是原理是一樣的,這裡就不一一述說了,這樣MVP的架構就簡歷起來了:

  • 在View(Activity)中注入Presenter;
  • 在Presenter中注入View 和 Model
  • 在Model中注入其他一些資料處理的物件(資料庫例項和網路請求例項)

Dagger2生成的程式碼解析

到這裡,你是不是覺得為什麼Dagger2會如此神奇?我們這裡就對生成的程式碼DaggerAppComponent來進行解析,看看它是怎麼實現依賴注入的。

DaggerAppComponent
    .builder()
    .appModule(new AppModule(this))
    .apiServiceModule(new ApiServiceModule())
    .dBModule(new DBModule())
    .httpModule(new HttpModule())
    .build();複製程式碼

那就從build方法開始:

public AppComponent build() {
      if (appModule == null) {
        throw new IllegalStateException(AppModule.class.getCanonicalName() + " must be set");
      }
      if (httpModule == null) {
        this.httpModule = new HttpModule();
      }
      if (apiServiceModule == null) {
        this.apiServiceModule = new ApiServiceModule();
      }
      if (dBModule == null) {
        this.dBModule = new DBModule();
      }
      return new DaggerAppComponent(this);
    }複製程式碼

build方法中會對我們傳入的module進行NULL檢查,可以看出來,如果我們的model的建構函式是無參的話,可以不用設定,dagger2會幫我們初始化,接著看new DaggerAppComponent():

private DaggerAppComponent(Builder builder) {
    assert builder != null;
    initialize(builder);
  }


  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {

    this.provideApplicationProvider =
        DoubleCheck.provider(AppModule_ProvideApplicationFactory.create(builder.appModule));

    this.provideOkHttpClientProvider =
        DoubleCheck.provider(HttpModule_ProvideOkHttpClientFactory.create(builder.httpModule));

    this.provideBaseUrlProvider =
        DoubleCheck.provider(
            ApiServiceModule_ProvideBaseUrlFactory.create(builder.apiServiceModule));

    this.provideRetrofitProvider =
        DoubleCheck.provider(
            HttpModule_ProvideRetrofitFactory.create(
                builder.httpModule, provideOkHttpClientProvider, provideBaseUrlProvider));

    this.provideUserServiceProvider =
        DoubleCheck.provider(
            ApiServiceModule_ProvideUserServiceFactory.create(
                builder.apiServiceModule, provideRetrofitProvider));

    this.serviceManagerProvider =
        DoubleCheck.provider(ServiceManager_Factory.create(provideUserServiceProvider));

    this.provideCommonSQLiteHelperProvider =
        DoubleCheck.provider(
            DBModule_ProvideCommonSQLiteHelperFactory.create(
                builder.dBModule, provideApplicationProvider));

    this.provideUserInfoDaoProvider =
        DoubleCheck.provider(
            DBModule_ProvideUserInfoDaoFactory.create(
                builder.dBModule, provideCommonSQLiteHelperProvider));

    this.dBManagerMembersInjector = DBManager_MembersInjector.create(provideUserInfoDaoProvider);

    this.dBManagerProvider =
        DoubleCheck.provider(
            DBManager_Factory.create(dBManagerMembersInjector, provideApplicationProvider));

    this.provideGsonProvider =
        DoubleCheck.provider(AppModule_ProvideGsonFactory.create(builder.appModule));
  }複製程式碼

在構造方法裡面呼叫了initialize(builder),這裡面對所有Provider進行初始,這個方法也是完成注入的方法,這裡涉及到兩個物件:

  • Provider & Factory
    其實就是一個包裝類,裡面提供了get方法返回對應的包裝物件,比如Provider,get方法就返回它持有的Application物件。這些Provider就是我們所有提供依賴的物件(包括在@module類中使用@Provides註解標註的物件,或者@Inject標記構造方法的物件)
  • XX_MembersInjector
    顧名思義,這個是物件注入器,哪些使用了@Inject註解的類就會生成對應的MembersInjector,通過呼叫其injectMembers()方法實現物件注入。

DoubleCheck.provider()有什麼作用呢,它是實現區域性單例的,返回一個實現單例的Provider。

這裡我們看下LoginActivity_MembersInjector 是怎麼注入LoginPresenter的:

public final class LoginActivity_MembersInjector implements MembersInjector<LoginActivity> {
  private final Provider<LoginPresenter> mPresenterProvider;

  public LoginActivity_MembersInjector(Provider<LoginPresenter> mPresenterProvider) {
    assert mPresenterProvider != null;
    this.mPresenterProvider = mPresenterProvider;
  }

  public static MembersInjector<LoginActivity> create(Provider<LoginPresenter> mPresenterProvider) {
    return new LoginActivity_MembersInjector(mPresenterProvider);
  }

  @Override
  public void injectMembers(LoginActivity instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    cn.xdeveloper.dagger2.base.mvp.BaseActivity_MembersInjector.injectMPresenter(
        instance, mPresenterProvider);
  }
}複製程式碼

在injectMembers方法中呼叫了父類的injectMembers方法,因為我們把注入的過程抽取到父類了,再看看父類的injectMembers方法:

public static <P extends BasePresenter> void injectMPresenter(
      BaseActivity<P> instance, Provider<P> mPresenterProvider) {
    instance.mPresenter = mPresenterProvider.get();
  }複製程式碼

通過簡單的物件賦值就完成了注入,那呼叫LoginActivity_MembersInjector的injectMembers方法的地方是哪裡呢?

@Override
public void inject(LoginActivity activity) {
    loginActivityMembersInjector.injectMembers(activity);
}複製程式碼

沒錯這個方法就是我們在LoginComponent中定義的inject介面的實現方法。
這裡你可能會覺得奇怪,為什麼我定義了inject方法名就會生成injectMembers的實現呢?我們再看看之前定義的LoginComponent的程式碼:

@ActivityScope
@Component(modules = LoginModule.class, dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity activity);
}複製程式碼

這裡的方法申明是有講究的:

  • 如果引數有值的,代表這個方法是注入方法,注入的類就是該引數類,框架會為其建立injectMembers的實現,這個時候是不允許有返回值的,這裡方法名是可以隨意填寫,叫inject比較形象;
  • 如果引數是沒有值的時候,則代表該component提供了依賴,返回型別就是該依賴物件,比如之前的AppComponet中定義的:
@Singleton
@Component(modules = {AppModule.class, HttpModule.class, ApiServiceModule.class, DBModule.class})
public interface AppComponent {

    Application application();

    ServiceManager serviceManager();

    DBManager DBManager();

    Gson gson();
}複製程式碼

那麼這些定義的方法有什麼用呢?最簡單的方法就是看程式碼中哪裡引用了它們就知道了:

this.applicationProvider =
        new Factory<Application>() {
          private final AppComponent appComponent = builder.appComponent;

          @Override
          public Application get() {
            return Preconditions.checkNotNull(
                appComponent.application(),
                "Cannot return null from a non-@Nullable component method");
          }
        };複製程式碼

這裡是DaggerLoginActivityComponent的初始化方法裡,因為我們的LoginActivityComponent是依賴AppComponnet的,那要怎麼引用這些AppComponent中已經初始化好的物件呢?則是通過上面定義的這些介面方法來訪問的。

後續問題

因為Dagger2是在編譯階段完成依賴注入,沒有了反射帶來的效率問題,但同時就會缺乏了靈活性。
我在重構專案的時候就遇到了這麼一個問題,由於我的專案是多資料庫的,一個使用者對應一個資料庫,這樣在依賴SQLDataBaseHelper的時候就無從下手了,因為這個在編譯期間是無法知道使用者資訊的,思前想後終於想到了一個辦法:
我們可以在AppComponent中管理一個叫DBManager的物件,在DBManager裡面含有各種Dao物件,但是這些Dao的建立是由我們自己去建立而不是靠dagger2注入的,這樣的話我們就可以在其他需要使用資料庫的地方@Inject DBManager就可以了,附上程式碼僅供參考:

@Singleton
public class DBManager {

    private Application application;

    private ContactDao contactDao;

    @Inject
    public DBManager(Application application) {
        this.application = application;
    }


    public ContactDao getContactDao(Long userId) {
        if (contactDao == null) {
            synchronized (DBManager.class) {
                if (contactDao == null)
                    contactDao = new ContactDao(PrivateDBHelper.getInstance(application, userId));
            }
        }
        return contactDao;
    }
}複製程式碼

結尾

以上就是全部我對dagger2的瞭解,最初我也是從一頭霧水,現在終算恍然大悟,還是那句老話:程式碼是最好的老師。對於瞭解dagger2的注入原理的話多看生成的程式碼,邏輯還是挺清晰的。
最後,希望大家能早日擁抱dagger2。

程式碼地址 GitHub
QQ:318531018

相關文章