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();
}