架構設計知識梳理(1) Dagger2

澤毛發表於2017-12-21

一、概述

Dagger2依賴注入框架的好處:

  • 依賴的注入和配置獨立於元件之外
  • 依賴物件是在一個獨立、不耦合的地方初始化,當初始化方式改變的時候修改的程式碼少。
  • 依賴注入使得單元測試更加簡單。

Dagger2相對於其它框架的優點:

  • 編譯期生成程式碼,有錯誤會在編譯期報出。
  • 錯誤可追蹤。
  • 易於除錯。

Dagger2的缺點:

  • 缺少靈活性。
  • 沒有動態機制。

二、Dagger2的註解

Dagger2的註解主要有以下七類:

  • @Inject:這個註解有兩個作用:在目標類中標記成員變數告訴Dagger這個型別的變數需要一個例項物件;標記依賴類中的構造方法,告訴Dagger我可以提供這種型別的依賴例項。
  • @Component:用來標記介面或者抽象類,也被稱為注入器,是@Inject@Module的橋樑,所有的Component都可以通過它的modules知道它所提供的依賴範圍,一個Componet可以依賴一個或多個Component,並拿到被依賴Component暴露出來的例項,Componenetdependencies屬性就是確定依賴關係的實現。
  • @Module:用來標記類,一般類名以Module結尾,Module的主要作用是用來集中管理@Provides標記的方法,我們定義一個被@Module註解的類,Dagger就會知道在哪裡找到依賴來滿足建立類的例項,Module的一個重要特徵是被設計成區塊並可以組合在一起。
  • @Provides:對方法進行註解,並且這些方法都是有返回型別的,告訴Dagger我們向如何建立並提供該型別的依賴例項(一般會在方法中new出例項),用@Provides標記的方法,推薦用provide作為字首。
  • @Qualifier:限定符,當一個類的型別不足以標示一個依賴的時候,我們就可以用這個註解,它會呼叫DataModule中方法來返回合適的依賴類例項。
  • @Scope:通過自定義註解來限定作用域,所有的物件都不再需要知道怎麼管理它的例項,Dagger2中有一個預設的作用域註解@Singleton,通常用來標記在App整個生命週期記憶體活的例項,也可以定義一個@PerActivity註解,用來表明生命週期要與Activity一致。
  • @SubComponent:如果我們需要父元件全部的提供物件,我們就可以用包含方式,而不是用依賴方式,包含方式不需要父元件顯示顯露物件,就可以拿到父元件全部物件,且SubComponent只需要在父Component接扣中宣告就可以了。

三、Dagger2的簡單應用 - @Inject@Component

第一步:基礎配置,在build.gradle中新增相應的依賴:

//新增(1)
apply plugin: 'com.neenbedankt.android-apt'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        //新增(2)
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.0"

    defaultConfig {
        applicationId "com.demo.zejun.repodragger2"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.0.0'
    //新增(3)
    apt 'com.google.dagger:dagger-compiler:2.0'
    //新增(4)
    compile 'com.google.dagger:dagger:2.0'
}
複製程式碼

第二步:User作為目標類中需要例項化的成員物件,給其建構函式新增@Inject標籤:

public class User {

    public String name;

    @Inject
    public User() {
        name = "lizejun";
    }

    public String getName() {
        return name;
    }
}
複製程式碼

第三步:宣告Component

@Component()
public interface OnlyInjectComponent {
    void inject(AnnotationActivity annotationActivity);
}
複製程式碼

第四步:在目標類中新增註解@Inject,並根據我們第3步中宣告的Component,呼叫DaggerXXX方法來進行注入:

public class AnnotationActivity extends AppCompatActivity {

    @Inject
    public User mUser;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dragger2);
        //在第3步宣告的Component介面或者抽象類的基礎上,新增Dagger字首。
        DaggerOnlyInjectComponent.builder().build().inject(this);
    }

}
複製程式碼

上面這個例子有兩個缺點:

  • 只能標記一個構造方法,因為如果標記兩個以上,不知道要用哪一個構造提供例項。
  • 不能標記其它我們不能修改的類,例如第三方庫。
  • 如果用@Inject標記的建構函式如果有引數,那麼這個引數也需要其它地方提供依賴,而類似於String這些我們不能修改的類,只能用@Module中的@Provides來提供例項了。

四、採用@Module來提供依賴

採用@Module標記的類提供依賴是常規套路,@Module標記的類起管理作用,真正提供依賴例項靠的是@Provides標記的帶返回型別的方法。 第一步:和上面類似,我們定義一個依賴類,但是它的構造方法並不需要用@Inject標記:

public class Person {

    private String name;

    public Person() {
        this.name = "lizejun";
    }

    public String getName() {
        return name;
    }
}
複製程式碼

第二步:我們需要定義一個@Module來管理這些依賴類的例項:

@Module
public class PersonDataModule {

    @Provides
    public Person providePerson() {
        return new Person();
    }
}
複製程式碼

第三步:定義一個@Component,它指向上面定義的@Module

@Component(modules = {PersonDataModule.class})
public interface PersonInjectComponent {
    void inject(PersonInjectActivity injectActivity);
}
複製程式碼

第四步:在目標類中進行依賴注入

public class PersonInjectActivity extends Activity {

    @Inject
    Person mPerson;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DaggerPersonInjectComponent.create().inject(this);
        System.out.println("Person name=" + mPerson.getName());
    }
}
複製程式碼

這裡注入的方式有兩種,一種是像上面這樣的,它適合於PersonDataModule中只有一個無參的構造方法,否則我們需要這樣呼叫:

DaggerPersonInjectComponent.builder().personDataModule(new PersonDataModule()).build().inject(this);

複製程式碼

五、初始化依賴例項的步驟

  • 查詢Module中是否存在建立該型別的方法(即@Component標記的介面中包含了@Module標記的Module類,如果沒有則直接查詢@Inject對應的構造方法)。

  • 如果存在建立類方法,則檢視該方法是否有引數

  • 如果不存在引數,直接初始化該類的例項,一次依賴注入到此結束。

  • 如果存在引數,則從步驟1開始初始化每個引數。

  • 如果不存在建立類方法,則查詢該型別的類中有@Inject標記的構造方法,檢視構造方法是否有引數:

  • 如果不存在引數,則直接初始化該類例項,一次依賴注入到此結束。

  • 如果存在引數,則從步驟1開始初始化每個引數。

六、@Qualifier限定符

Dagger中,有一個已經定義好的限定符,@Name,下面我們也自己定義一個限定符:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface PeopleThreeQualifier {}
複製程式碼

第一步:和前面類似,我們先定義一個需要例項化的依賴類:

public class People {

    private int count;

    public People() {
        count = 0;
    }

    public People(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }
}
複製程式碼

第二步:我定義一個DataModule,和前面不同的是,在它的provideXXX方法的註解中,我們新增了@Name(xxx)和自定義的註解PeopleThreePeople

@Module
public class PeopleDataModule {

    @Provides
    @Named("Five People")
    People provideFivePeople() {
        return new People(5);
    }

    @Provides
    @Named("Ten People")
    People provideTenPeople() {
        return new People(10);
    }

    @Provides
    @PeopleThreeQualifier
    People provideThreePeople() {
        return new People(3);
    }
}
複製程式碼

第三步:定義Component

@Component(modules = PeopleDataModule.class)
public interface PeopleInjectComponent {
    void inject(PeopleInjectActivity peopleInjectActivity);
}
複製程式碼

第四步:在目標類中進行依賴注入,在提供@Inject註解時,我們還需要宣告和PeopleDataModule中對應的限定符,這樣Dagger就知道該用那個函式來生成目標類中的依賴類例項:

public class PeopleInjectActivity extends Activity {

    @Inject
    @Named("Five People")
    People mFivePeople;

    @Inject
    @Named("Ten People")
    People mTenPeople;

    @Inject
    @PeopleThreeQualifier
    People mThreePeople;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DaggerPeopleInjectComponent.builder().peopleDataModule(new PeopleDataModule()).build().inject(this);
        System.out.println("Five People=" + mFivePeople.getCount() + ",Ten People=" + mTenPeople.getCount() + ", Three People=" + mThreePeople.getCount());
    }
}
複製程式碼

七、@Scope

@Scope的作用主要是在組織ComponentModule的時候起到一個提醒和管理的作用,在Dagger中,有一個預設的作用域@Singleton@Scope的作用是:Dagger2可以通過自定義Scope註解,來限定通過ModuleInject方式建立的類的例項的生命週期能夠與目標類的生命週期相同。Scope的真正作用在與Component的組織:

  • 更好的管理Component之間的組織方式,不管是依賴方式還是包含方式,都有必要用自定的Scope註解標註這些Component,而且編譯器會檢查有依賴關係或包含關係的Component,若發現有Component沒有用自定義Scope註解,則會報錯。
  • 更好地管理ComponentModule之間地關係,編譯器會檢查Component管理的Module,若發現Component的自定義Scope註解與Module中的標註建立類例項方法的註解不一樣,就會報錯。
  • 提高程式的可讀性。

下面是一個使用@Singleton的例子: 第一步:定義需要例項化的類:

public class AnSingleObject {

    private String objectId;

    public AnSingleObject() {
        objectId = toString();
    }

    public String getObjectId() {
        return objectId;
    }
}
複製程式碼

第二步:定義DataModule,在它的provideXXX方法,提供了@Singletion註解:

@Module
public class AnSingleObjectDataModule {

    @Provides
    @Singleton
    AnSingleObject provideAnSingleObject() {
        return new AnSingleObject();
    }
}
複製程式碼

第三步:定義Component,和前面不同的是,需要給這個Component新增@Singleton註解:

@Component(modules = {AnSingleObjectDataModule.class})
@Singleton
public abstract class AnSingleObjectInjectComponent {

    private static AnSingleObjectInjectComponent sInstance;

    public abstract void inject(AnSingleObjectInjectActivity anSingleObjectInjectActivity);

    public static AnSingleObjectInjectComponent getInstance() {
        if (sInstance == null) {
            sInstance = DaggerAnSingleObjectInjectComponent.create();
        }
        return sInstance;
    }
}
複製程式碼

第四步:在目標類中進行依賴注入,每次啟動Activity的時候,我們可以發現列印出來的hash值都是相同的:

public class AnSingleObjectInjectActivity extends Activity {

    @Inject
    AnSingleObject object;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AnSingleObjectInjectComponent.getInstance().inject(this);
        System.out.println("AnSingleObject id=" + object.getObjectId());
    }
}
複製程式碼

八、組織Component

Component有三種組織方式:

  • 依賴:一個Component依賴一個或多個Component,採用的是@Componentdependencies屬性。
  • 包含:這裡就用到了@SubComponent註解,用它來標記介面或者抽象類,表示它可以被包乾。一個Component可以包含一個或多個Component,而且被包含的Component還可以繼續包含其它的Component
  • 繼承:用一個Component繼承另外一個Component

九、Google官方框架分析

下面是Google官方框架的目錄結構:

Paste_Image.png
可以看出,它把每個介面都作為一個獨立的包(addedittask、statistics、taskdetail、tasks),而資料、依賴類是其它的兩個包(data、util),我們先從ToDoApplication開始分析:

public class ToDoApplication extends Application {

    private TasksRepositoryComponent mRepositoryComponent;
    
    @Override
    public void onCreate() {
        super.onCreate();
        mRepositoryComponent = DaggerTasksRepositoryComponent.builder()
                .applicationModule(new ApplicationModule((getApplicationContext())))
                .build();
    }
    
    public TasksRepositoryComponent getTasksRepositoryComponent() {
        return mRepositoryComponent;
    }
}
複製程式碼

ToDoApplication中,我們例項化了一個變數TasksRepositoryComponent,它相當於是專案中所有其它Component的管理者,它被宣告為@Singleton的,即在App的生命週期中只存在一個,同時用@Component表明它和TaskRepositoyModuleApplicationModule這兩個Module關聯。

@Singleton
@Component(modules = {TasksRepositoryModule.class, ApplicationModule.class})
public interface TasksRepositoryComponent {
    TasksRepository getTasksRepository();
}
複製程式碼

TaskRpositotyModule提供了兩種型別的資料來源物件,它們是用@Local@Remote來區分的:

@Module
public class TasksRepositoryModule {

    @Singleton
    @Provides
    @Local
    TasksDataSource provideTasksLocalDataSource(Context context) {
        return new TasksLocalDataSource(context);
    }

    @Singleton
    @Provides
    @Remote
    TasksDataSource provideTasksRemoteDataSource() {
        return new FakeTasksRemoteDataSource();
    }

}
複製程式碼

接下來再看一下ApplicationModule

@Module
public final class ApplicationModule {

    private final Context mContext;

    ApplicationModule(Context context) {
        mContext = context;
    }

    @Provides
    Context provideContext() {
        return mContext;
    }
}
複製程式碼

下面我們用一個比較簡單的介面來看一下TasksRepositoryComponent是怎麼和其它的Component關聯起來的,首先看StatisticsActivity

public class StatisticsActivity extends AppCompatActivity {

    private DrawerLayout mDrawerLayout;

    @Inject 
    StatisticsPresenter mStatiticsPresenter; //依靠Dagger例項化的物件。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.statistics_act);
        //初始化Fragment介面。
        StatisticsFragment statisticsFragment = (StatisticsFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
        if (statisticsFragment == null) {
            statisticsFragment = StatisticsFragment.newInstance();
            ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
                    statisticsFragment, R.id.contentFrame);
        }

        DaggerStatisticsComponent.builder()
            .statisticsPresenterModule(new StatisticsPresenterModule(statisticsFragment))
            .tasksRepositoryComponent(((ToDoApplication) getApplication())
            .getTasksRepositoryComponent())
            .build().inject(this);
    }

}
複製程式碼

相關文章