1. 程式人生 > >Android開發進階——使用Dagger2

Android開發進階——使用Dagger2

前言

關於Dagger2的學習,首先看的官方文件,確實看不懂。然後搜尋網路上的介紹博文,不乏一些講得比較好的,如這個。但終究不夠透徹,還是得回頭研究官方文件。本文不僅僅是翻譯,而是記錄了自己對官方文件的理解。

提供依賴的兩種方式

使用@Inject註解構造器

class Thermosiphon implements Pump {
    private final Heater heater;

    @Inject
    Thermosiphon(Heater heater) {
        this.heater = heater;
    }
}

使用@Module

@Module
class DripCoffeeModule {
    @Provides static Heater provideHeater() {
        return new ElectricHeater();
    }

    @Provides static Pump providePump(Thermosiphon pump) {
        return pump;
    }
}

使用@Module替代@Inject的情況有:
- 依賴是一個介面,不能使用構造器。
- 依賴是第三方類,不能新增@Inject註解。
- 依賴在使用前需要被配置。

依照慣例,@Provides註解的方法名應該使用provide作為字首,@Module註解的類名應該用Module作為字尾。

注入依賴的兩種方式

使用@Inject和@Provides註解的類物件組成了一個有向圖(graph),該圖的各頂點(vertex)由它們的依賴連線。我們可以通過由@Component註解的介面來訪問這個圖,以滿足我們的依賴。我們將Module傳遞給@Component的modules引數,Dagger2負責生成一個該介面的實現類。事實上我們構造一個Component的過程就是在構造一個graph。

@Component(modules = DripCoffeeModule.class
) interface CoffeeShop { CoffeeMaker maker(); }

Dagger2生成的實現類名由Dagger加上介面名構成。如下獲取一個實現類的例項:

CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();

任何具有預設構造器的Module都不需要顯示地設定,builder會自動建立Module。因而上面的程式碼可以簡化為:

CoffeeShop coffeeShop = DaggerCoffeeShop.builder().build();

如果所有的依賴並不需要構造Module就能提供(如DripCoffeeModule中的所有@Provides方法都是靜態的),那麼實現類會提供一個create()方法來快速地獲取一個Component例項。

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

Component可以包含兩種方法,分別是Provision method和Members-injection method,Provision方法沒有引數並且返回需要的型別,Members-injection方法一般沒有返回值,引數是需要注入成員變數的型別(一般命名為inject)。或許後者方法在Android開發中更為常見。

通過Provision方法獲取依賴

得到Component例項後,通過呼叫Component的Provision方法獲取已經準備好的物件例項:

class CoffeeMaker {
    @Inject Heater heater;
    @Inject Pump pump;

    ...
}

public class CoffeeApp {
    public static void main(String[] args) {
        CoffeeShop coffeeShop = DaggerCoffeeShop.create();
        coffeeShop.maker().brew();
    }
}

通過Members-injection方法注入依賴

class MainActivity extends Activity {
    @Inject DataSource dataSource;

    private DaggerActivityComponent component;
    ...

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

        component = DaggerActivityComponent.create();
        component.inject(this);
        ...
    }
}

事實上依賴不只兩種來源

上面說過,Component提供了訪問一個有向圖的介面,有向圖中的每個邊(也就是上面所說的依賴)稱為一個binding,binding的來源如下:
- Module中通過@Provides註解的方法,Component通過@Component.modules直接引用,如果Module中通過@Module.includes定義了子Module,也一併包括。
- 使用@Inject註解構造器的型別(沒有使用@Scope或者@Scope同Component一致)。
- 該Component依賴的Component的Provision方法。
- 該Component自身。
- subcomponent的未限定builder。
- 以上binding的Provider或者Lazy包裝類。
- 以上binding的Lazy包裝類的Provider包裝類(如Provider

單例和binding的作用域

@Singleton是一個預定義的@Scope。使用@Singleton註解@Provides方法或者可注入類。Component會使用同一個類例項(也就是單例)來進行依賴注入。

@Provides @Singleton static Heater provideHeater() {
    return new ElectricHeater();
}

可注入類上的@Singleton註解事實上也起到文件的作用,用於提醒程式碼維護者該類可能被多個執行緒共享。

@Singleton
class CoffeeMaker {
    ...
}

根據上面binding的第二條來源我們知道,Component只會識別未加作用域或者跟自己的作用域一致的依賴,所以如果@Provides和可注入類加上了作用域,那麼Component本身一定要有@Scope進行註解(預定義的或者自定義)。

@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
    CoffeeMaker maker();
}

為什麼會有Scope這個東西呢?我們知道任何物件都有生命週期,包括我們建立的Component物件本身。比如我們在Activity的onCreate中建立了一個Component,然後該Component向該Activity中注入了一個DataSource,即便該DataSource被註解為@Singleton,該Activity被銷燬並重新建立後,這次注入的DataSource和第一次並不是同一個例項。所以像這種場合我們一般使用一個更有意義的自定義@Scope註解,如@PerActivity。正如前面說過的,Component只會保證跟自己有相同Scope的類在自己的生命週期內只有一個例項。因此我們需要自己確保Component例項的作用域和它的註解的意義一致,Dagger並不會負責。

Component的dependencies

正如binding的第三條來源可知,Component不僅僅可以有modules也可以有dependencies。也就是依賴其他的Component。

@Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
@PerActivity
public interface ActivityComponent {
    void inject(MainActivity mainActivity);
}

注意,只有被依賴的Component中的Provision方法才會參與該Component代表的有向圖的構建,如上例中的ApplicationComponent中的modules提供的依賴並不能訪問。如下只有DataManager物件可以被參與ActivityComponent的注入工作。

@Component(modules = ApplicationModule.class)
@Singleton
public interface ApplicationComponent {
    void inject(DemoApplication demoApplication);
    DataManager getDataManager();
}

Component的完整建立過程如下:

public static void main(String[] args) {
     OtherComponent otherComponent = ...;
     MyComponent component = DaggerMyComponent.builder()
         //必需,component dependencies一定要設定
         .otherComponent(otherComponent)
         //必需,FlagsModule有構造器引數
         .flagsModule(new FlagsModule(args))
         //可省略,MyApplicationModule有可用無參構造器
         .myApplicationModule(new MyApplicationModule())
         .build();
   }

Component的Subcomponents

Component之間的關係有兩種,除了上面的dependencies還有Subcomponents。subcomponent可以完整的繼承父Component的binding圖,不僅僅只是訪問Provision方法提供的依賴。subcomponent可以使用兩種方式宣告,列入@Module的subcomponents引數中,或者在Component中通過工廠方法生成,方法名隨意,但是要返回一個subcomponent,引數可以包括任意數量的Module,如果Module有可用的無參構造器,那麼該Module可以省略。構建原則同Component。如下示例:

@Component
@Singleton
interface ApplicationComponent {
    // component methods...

    RequestComponent newRequestComponent(RequestModule requestModule);
}

自定義Component的Builder

@Component(modules = {BackendModule.class, FrontendModule.class})
interface MyComponent {
    MyWidget myWidget();

    @Component.Builder
    interface Builder {
        MyComponent build();
        Builder backendModule(BackendModule bm);
        Builder frontendModule(FrontendModule fm);
    }
}

可重用的Scope

簡而言之,這種Scope限制依賴的例項數量,但是又並不保證只有一個例項。看程式碼:

@Reusable //生成多少個scooper沒有關係,只是不要浪費記憶體
class CoffeeScooper {
    @Inject CoffeeScooper() {}
}

@Module
class CashRegisterModule {
    @Provides
    @Reusable //不要這樣使用,你肯定會在意現金存放在哪個特定的Register裡面
              //所以這裡應該使用一個特定的Scope
    static CashRegister badIdeaCashRegister() {
        return new CashRegister();
    }
}

@Reusable //不要這樣使用,咖啡過濾器肯定每次都要新的
          //不需要Scope
class CoffeeFilter {
    @Inject CoffeeFilter() {}
}

可釋放的引用

當一個binding使用了Scope後,Component就持有這個繫結物件的強引用。在記憶體敏感的環境下這樣做不合適,這種情況下應該定義一個使用@CanReleaseReferences註解的Scope:

@Documented
@Retention(RUNTIME)
@CanReleaseReferences
@Scope
public @interface MyScope {}

然後注入一個ReleasableReferenceManager進行記憶體管理。

@Inject 
@ForReleasableReferences(MyScope.class)
ReleasableReferences myScopeReferences;

void lowMemory() {
    myScopeReferences.releaseStrongReferences();
}

void highMemory() {
    myScopeReferences.restoreStrongReferences();
}

Lazy注入

class GridingCoffeeMaker {
    @Inject Lazy<Grinder> lazyGrinder;

    public void brew() {
        while (needsGrinding()) {
            // Grinder會在呼叫get方法時才建立,然後快取起來
            lazyGrinder.get().grind();
        }
    }
}

Provider注入

class BigCoffeeMaker {
  @Inject Provider<Filter> filterProvider;

  public void brew(int numberOfPots) {
  ...
    for (int p = 0; p < numberOfPots; p++) {
      maker.addFilter(filterProvider.get()); //每次都返回新的Filter例項
      maker.addCoffee(...);
      maker.percolate();
      ...
    }
  }
}

限定符(Qualifier)

如果有多個依賴滿足條件,如何注入呢?這時就需要Qualifier了。

class ExpensiveCoffeeMaker {
    @Inject @Named("water") Heater waterHeater;
    @Inject @Named("hot plate") Heater hotPlateHeater;
    ...
}

@Provides @Named("hot plate") 
static Heater provideHotPlateHeater() {
    return new ElectricHeater(70);
}

@Named是一個預定義的@Qualifier註解,我們同樣可以自定義Qualifier

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
    String value() default "";
}

繫結例項

假如參與繫結的資料在構建Component的時候才能得到,如命令列引數。當然你可以將這種資料通過Module的構造器引數傳遞進去,然而自定義builder確是更好的選擇。

@Component(modules = AppModule.class)
interface AppComponent {
    App app();

    @Component.Builder
    interface Builder {
        @BindsInstance Builder userName(@UserName String userName);
        AppComponent build();
    }
}

public static void main(String[] args) {
    if (args.length > 1) { exit(1); }
    App app = DaggerAppComponent.builder()
        .userName(args[0])
        .build()
        .app();
    app.run();
}