【android】擺正姿勢,dagger2原來如此簡單

此情此景我想吟詩一首發表於2019-01-06

##1.前言 依稀記得兩年之前就偷偷學習過dagger2的使用,當然那個時候也是雲裡霧裡,對照著部落格確實實現了物件的注入,但是後來基本上沒有再深入的學習過,雖然偶爾也會瀏覽到dagger2的文章,但是實際專案中並沒有使用,一直擱置著,最近突然腦子發熱,想學習了,剛好學到了這裡,趁著腦子熱,索性就記錄下,待以後需要使用的翻看便知。

##2.什麼是dagger2 dagger2是一款非常有名的依賴注入框架,那麼問題來了,什麼是依賴注入,很簡單,就是字面意思,當你需要建立物件的時候,依賴於其他的程式進行注入,而不是自己主動去建立。

平時我們用到的set方法其實也是一種依賴注入的方式,再比如有參建構函式傳遞過來的物件同樣屬於注入的一種。

優點:

  • 避免無用的體力勞動(重複多次new物件)
  • 更好的管理類的例項,避免耦合 #####引入dagger2:
    annotationProcessor 'com.google.dagger:dagger-compiler:2.11' 
    implementation 'com.google.dagger:dagger-android:2.11'
    implementation 'com.google.dagger:dagger-android-support:2.11'
    // if you use the support libraries
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.11'
複製程式碼

##3.最簡單的物件注入(無參)

dagger2的使用依賴於幾個比較主要的註解,下面跟隨我一切來實現一個簡單的物件注入,我們想要在MainActivity建立一個School物件:

1.首先建立School類

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

可以看到我們在School的建構函式上加上了@Inject註解,此處的作用就是 : #####標識School類可以被注入,相對於給注入的物件指定一個建立的來源,不然程式無法知曉如果建立school物件。

2.school類有了,也標明瞭它可以被注入(使用@Inject註解),下面我們就要找個東西來建立這個例項,@Component註解就是這個作用,它相當於一個注入器,負責生產類的例項,起到一個橋樑的作用,一端是生產,一端是注入。

@Component
public interface MainComponent {
    void inject(MainActivity activity);
}
複製程式碼

3.Android Studio中 build---> make project,或者單獨編譯下module也可以,編譯完成後將會自動生成dagger2注入所需的程式碼,程式碼生成位置在:

2.png

此時我們開始把school物件注入到MainActivity中,程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Inject
    School school;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //inject方法負責連線所建立的例項和mainActivity,此時例項被賦值給school變數
        DaggerMainComponent.create().inject(this);
        Log.e("daggerLog","school :【"+school+"】");
    }
}
複製程式碼

####此時執行程式,就會出現下面的列印結果:

1.png

我們發現,MainActivity並沒有new School()的操作,僅僅通過@Inject註解,並且進行inject連線之後,物件就“自動”產生了。

有些槓精可能要說了,這TM實在逗我,建立個物件一行程式碼搞定了,你給我弄這麼多花裡胡哨的不嫌麻煩嗎?我只能說你說的沒毛病,單個物件不頻繁的建立或許並不需要這麼麻煩,但是為了降低程式碼的耦合性,或者說從架構方面考慮,可能這真的很有必要。

##4.有參的物件注入

上面我們只是簡單定義了一個無參的School物件,那麼有參的物件應該如何注入呢,下面我們改造下School類。

public class School {

    private String name;

    private String address;

    @Inject
    public School(String name, String address) {
        this.name = name;
        this.address = address;
    }
}
複製程式碼

嗯,也沒幹啥,就增加了兩個引數,我們來編譯一下:

3.png

完犢子,報錯了,看錯誤提示裡面出現了@Provides註解,那麼對於有參的建構函式,該如何進行注入呢,dagger2給我們提供了兩個註解,@Module和@Provides, @Module標識我是用來提供注入例項的,@Provides用來表示具體提供例項的方法,看下面的這段程式碼:

@Module
public class MainModules {

    @Provides
    public School provideSchool(){
        return new School("南京高階中學","江蘇南京");
    }
}
複製程式碼

在class名字上方加上@Module註解,在具體提供例項的地方新增上@Provides註解,此時School就具備了被注入的能力,但前面我們說到連線例項和被注入端使用的是@Component註解,那麼如何為@Component指定使用哪個Module注入呢?

@Component(modules = MainModules.class)
public interface MainComponent {
    void inject(MainActivity activity);
}
複製程式碼

####很簡單,通過@Component中的modules關鍵字來指定提供例項的module,如果有多個module可以使用{}括起來,形如下面這樣的形式:

@Component(modules = {MainModules.class,OtherModules.class})
複製程式碼

此時我們再次編譯專案,並且修改MainActivity程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Inject
    School school;

    @Inject
    School school2;

    private Button btnJump;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerMainComponent.create().inject(this);

        Log.e("daggerLog","school name:【"+school.getName()+"】 address:【"+school.getAddress()+"】");

        Log.e("daggerLog","school add >【"+school+"】");
        Log.e("daggerLog","school2 add >【"+school2+"】");

  }
}
複製程式碼

執行後,結果列印如下:

4.png

此時School物件已經注入完畢,可以正常使用了,此處大家可能發現了我們注入了兩個School物件,但如果我們想保持School單例的話,該如何操作呢? 當然dagger2不會讓我們為這些所擔憂,它提供了@Singleton註解用於處理單例的情況;

####注意:Singleton是區域性單例,和生命週期繫結,注入activity單例範圍就是當前activity之內,注入application就是全域性單例。 下面我們來驗證一下,修改程式碼,在Module和Component中分別加入@Singleton註解,缺一不可,此時程式碼如下:

@Module
public class MainModules {

    @Singleton
    @Provides
    public School provideSchool(){
        return new School("南京高階中學","江蘇南京");
    }
}

@Singleton
@Component(modules = MainModules.class)
public interface MainComponent {
    void inject(MainActivity activity);
}

複製程式碼

執行後,結果列印如下:

5.png

可以看到此時在MainActivity中確實實現了單例,那麼我們再次修改下程式碼,新增SecordActivity,並把School物件注入到SecordActivity中,並在MainActivity中新增跳轉按鈕,點選後跳轉到SecordActivity:

@Singleton
@Component(modules = MainModules.class)
public interface MainComponent {
    void inject(MainActivity activity);
    void inject(SecordActivity activity);
}

public class SecordActivity extends AppCompatActivity {

    @Inject
    School school;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_secord);

        DaggerMainComponent.create().inject(this);

        Log.e("daggerLog","secord school add >【"+school+"】");
    }
}
複製程式碼

編譯執行後,點選跳轉後列印結果如下:

6.png
####就像我們前面說的那樣,@Singleton是區域性單例,此時的兩個Activity中的School物件已經是不同的物件了。

我們看下@Singleton的原始碼:

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

發現裡面有個@Scope註解,@Scope其實才是單例的關鍵,@Scope是用來管理生命週期的,而@Singleton是dagger2通過@Scope提供的一種預設實現。

##5.全域性單例的實現 下面我們看一下全域性單例該如何實現,首先建立MyApplicaiton:

public class MyApplicaiton extends Application {

    private AppComponent appComponent;

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

        appComponent = DaggerAppComponent.builder().build();
    }

    public AppComponent getAppComponent() {
        return appComponent;
    }
}

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    void inject(MainActivity activity);
    void inject(SecordActivity activity);
}

@Module
public class AppModule {

    @Singleton
    @Provides
    public School provideSingleSchool(){
        return new School("全域性單例的School","中國");
    }
}

public class MainActivity extends AppCompatActivity {

    @Inject
    School school;

    @Inject
    School school2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ((MyApplicaiton)getApplication()).getAppComponent().inject(this);


        Log.e("daggerLog","school name:【"+school.getName()+"】 address:【"+school.getAddress()+"】");

        Log.e("daggerLog","school add >【"+school+"】");
        Log.e("daggerLog","school2 add >【"+school2+"】");
    }
}

public class SecordActivity extends AppCompatActivity {

    @Inject
    School school;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_secord);

        ((MyApplicaiton)getApplication()).getAppComponent().inject(this);

        Log.e("daggerLog","secord school add >【"+school+"】");
    }
}
複製程式碼

然後執行,跳轉到SecordActivity,日誌列印如下:

7.png
#####通過日誌可以發現,所有的School物件都是同一物件,實現了全域性的單例!

##6.如何傳遞context.

有些時候我們在通過@Provides提供物件的時候需要context引數,形如下面的形式:

public class ShowUtils {
    private Context context;

    @Inject
    public ShowUtils(Context context) {
        this.context = context;
    }
}
複製程式碼

這個時候如果我們正常通過@Provides的方式提供的話,dagger是無法確定此處的context是哪種型別的,是Application的?還是Activity的?此時編譯就會報錯,那麼我們該如何指定Context具體是何種型別呢?

#####此時又出現了一個新的註解@Qualifier,@Qualifier的作用是當dagger無法確定型別的時候,或者說當我們@module裡有多個provides提供相同型別的返回值(dagger提供物件是根據返回值匹配的)的時候,對型別進行限定,下面舉個例子來體驗一下,首先自定義一個限定註解:

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

然後我們修改下上面例子中的MainModules:

@Module
public class MainModules {

    private Context context;

    public MainModules(Context context) {
        this.context = context;
    }

    @Singleton
    @Provides
    public School provideSchool(){
        return new School("南京高階中學","江蘇南京");
    }

    @Singleton
    @Provides
    public ShowUtils provideShowUtils(){
        return new ShowUtils(context);
    }

    //注意這裡,此處限定我們使用的context是activity的context
    @ForActivityContext
    @Provides
    public Context provideContext(){
        return context;
    }
}

複製程式碼

當然我們還需要在需要此物件的Activity上新增此註解:

@ForActivityContext
public class MainActivity extends AppCompatActivity {

    @Inject
    School school;

    @Inject
    School school2;

    @Inject
    ShowUtils showUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerMainComponent.builder().mainModules(new MainModules(this)).build()
                .inject(this);
        Log.e("daggerLog","school name:【"+school.getName()+"】 address:【"+school.getAddress()+"】");

        Log.e("daggerLog","school add >【"+school+"】");
        Log.e("daggerLog","school2 add >【"+school2+"】");

        Log.e("daggerLog","showUtils add >【"+showUtils+"】");
    }
}
複製程式碼

此時執行結果如下,可以看到showUtils物件成功注入到了MainActivity中:

8.png

####再看另外一種情況: 當我們需要在一個@module裡提供同一物件的不同例項的時候,該如何指定具體需要由哪個@Provides來提供呢? 先看下面一段程式碼:

    @Singleton
    @Provides
    public School provideSchool(){
        return new School("南京高階中學","江蘇南京");
    }

    @Singleton
    @Provides
    public School provideSchoolOther(){
        return new School("國外學校","外國");
    }
複製程式碼

其他程式碼不修改,直接編譯會報下面的錯誤:

9.png
不要慌,@Qualifier來幫你解決這個問題,首先使用@Qualifier自定義一個註解:

@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface SchoolType {
    int type() default 0;
}
複製程式碼

然後在@provides中新增此註解:

    @SchoolType
    @Singleton
    @Provides
    public School provideSchool(){
        return new School("南京高階中學","江蘇南京");
    }

    @SchoolType(type = 1)
    @Singleton
    @Provides
    public School provideSchoolOther(){
        return new School("國外學校","外國");
    }
複製程式碼

在MainActivity中@Inject處指定需要使用那種type的物件來進行注入:

@ForActivityContext
public class MainActivity extends AppCompatActivity {

    @SchoolType
    @Inject
    School school;

    @SchoolType(type = 1)
    @Inject
    School school2;

    @Inject
    Student student;

    @Inject
    ShowUtils showUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerMainComponent.builder().mainModules(new MainModules(this)).build()
                .inject(this);

        Log.e("daggerLog","school name:【"+school.getName()+"】 address:【"+school.getAddress()+"】");

        Log.e("daggerLog","school add >【"+school+"】");
        Log.e("daggerLog","school2 add >【"+school2+"】");
        Log.e("daggerLog","school2 name:【"+school2.getName()+"】 address:【"+school2.getAddress()+"】");

        Log.e("daggerLog","showUtils add >【"+showUtils+"】");
    }
}
複製程式碼

執行結果如下:

10.png
可以看到school和school2物件都成功的注入到了MainActivity中,由於我們指定了@SchoolType並且使用了不同的type,所以建立了兩個不同的物件。 #####那麼是不是我們需要限定的時候就需要自定義註解呢,當然不是,dagger2為我們提供了一個註解@Named,我們來看下@Named的原始碼:

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

是不是瞬間豁然開朗,這不就是我們剛剛定義的@SchoolType一樣嘛,只是名字不同罷了,當然把上面的@SchoolType直接替換成@Named也是一樣的,不信大家可以試試!

####說了這麼多,對於dagger基本使用大家是不是有點頭緒了,本篇我們主要講解了基本註解@Inject,@Component,@Module,@Provides,@Qualifier等的使用,當然dagger2還是很多更高階的用法,比如dependencies依賴,又比如subcomponent註解我們還沒有講到,這些內容準備放在下篇博文中進行講解,敬請期待!

本文原始碼地址: github.com/hanxiaofeng… --- > daggeruse目錄

相關文章