Google官方MVP+Dagger2架構詳解
1 前言
google官方示例架構專案
在我的前一篇文章分享的時候,當時todo-mvp-dagger/ 這個分支也還沒有開發完畢。最近的專案中也在用到Dagger2
作為依賴注入,所以通過這個專案一起來學習下,mvp+Dagger2 的實現吧。
參考實際專案,請使用命令“git clone
https://github.com/googlesamples/android-architecture.git” 將專案clone到本地,當前是master分支,需要使用“git checkout todo-mvp-dagger” 切換到todo-mvp-dagger分支。
2 Dagger2基礎
以下Dagger2基礎部分主要是對參考資料裡面的幾篇外文連結的知識點的整合,所以翻譯的語句可能有些生硬,在適當的地方會出現英文原文。
原文章連結(70%來自於下面的原文,做出了適當修改):
Dependency Injection with Dagger 2
2.1 什麼是Dagger2
安卓應用在初始化物件的時候經常需要處理各種依賴關係。比如說網路訪問中使用Retrofit,Gson,本地儲存中使用shared preference。無一例外,我們都都需要在使用它們的地方進行例項物件構建,物件之間可能還存在著各種各樣的依賴關係。
依賴注入(Dependency Injection,簡稱DI)是用於削減計算機程式的耦合問題的一個法則。物件在被建立的時候,由一個調控系統內所有物件的外界實體將其所依賴的物件的引用傳遞給它。也可以說,依賴被注入到物件中。
Dagger2 正是一個依賴注入框架,使用程式碼自動生成建立依賴關係需要的程式碼。減少很多模板化的程式碼,更易於測試,降低耦合,建立可複用可互換的模組。
2.2 Dagger2的優點
-
全域性物件例項的簡單訪問方式
和ButterKnife 庫定義了view,事件處理以及資源的引用一樣,Dagger2 提供全域性物件引用的簡易訪問方式。聲明瞭單例的例項都可以使用@inject
進行訪問。比如下面的MyTwitterApiClient 和SharedPreferences 的例項:public class MainActivity extends Activity { @Inject MyTwitterApiClient mTwitterApiClient; @Inject SharedPreferences sharedPreferences; public void onCreate(Bundle
-
複雜的依賴關係只需要簡單的配置
Dagger2 會通過依賴關係並且生成易懂易分析的程式碼。以前通過手寫的大量模板程式碼中的物件引用將會由它給你建立並傳遞到相應物件中。因此你可以更多的關注模組中構建的內容而不是模組中的物件例項的建立順序。 - 讓單元測試和整合測試更加方便
因為依賴關係已經為我們獨立出來,所以我們可以輕鬆的抽取出不同的模組進行測試。依賴的注入和配置獨立於元件之外。因為物件是在一個獨立、不耦合的地方初始化,所以當注入抽象方法的時候,我們只需要修改物件的實現方法,而不用大改程式碼庫。依賴可以注入到一個元件中:我們可以注入這些依賴的模擬實現,這樣使得測試更加簡單。 - 作用域例項(Scoped instances)
我們不僅可以輕鬆的管理全域性例項物件,也可以使用Dagger2中的scope定義不同的作用域。(比如根據user session,activity的生命週期)
2.3 Dagger2的引用
- 在整個專案的
build.gradle
中加入:dependencies { // other classpath definitions here classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }
- 在
app/build.gradle
中分別加入:// add after applying plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt'
dependencies { // apt command comes from the android-apt plugin apt 'com.google.dagger:dagger-compiler:2.2' compile 'com.google.dagger:dagger:2.2' provided 'javax.annotation:jsr250-api:1.0' }
需要注意的是
provided
代表編譯時需要的依賴,Dagger的編譯器生成依賴關係的程式碼,並在編譯時新增到IDE 的class path中,只參與編譯,並不會打包到最終的apk中。apt
是由android-apt外掛提供,它並不會新增這些類到class path中,這些類只用於註解解析,編寫程式碼的時候應當避免使用這些類。
2.4 建立單例(singleton)
接下來一步一步的分析Dagger2的使用,先來一張表和一張圖把Dagger2中的註解講解一下。如果有點不清晰,請接著往下看,然後再回來看一遍。
註解 | 用法 |
---|---|
@Module | Modules類裡面的方法專門提供依賴,所以我們定義一個類,用@Module註解,這樣Dagger在構造類的例項的時候,就知道從哪裡去找到需要的 依賴。modules的一個重要特徵是它們設計為分割槽並組合在一起(比如說,在我們的app中可以有多個組成在一起的modules) |
@Provide | 在modules中,我們定義的方法是用這個註解,以此來告訴Dagger我們想要構造物件並提供這些依賴。 |
@Singleton |
當前提供的物件將是單例模式 ,一般配合@Provides 一起出現 |
@Component | 用於介面,這個介面被Dagger2用於生成用於模組注入的程式碼 |
@Inject | 在需要依賴的地方使用這個註解。(你用它告訴Dagger這個 構造方法,成員變數或者函式方法需要依賴注入。這樣,Dagger就會構造一個這個類的例項並滿足他們的依賴。) |
@Scope | Scopes可是非常的有用,Dagger2可以通過自定義註解限定註解作用域。 |
接著往下看
看看Dagger2 的流程:
Dagger2 流程
首先看看下面這段程式碼,我們需要使用Okhttp,Gson,Retrofit和Gson做一個Twitter 客戶端的網路訪問。
OkHttpClient client = new OkHttpClient();
// Enable caching for OkHttp
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(getApplication().getCacheDir(), cacheSize);
client.setCache(cache);
// Used for caching authentication tokens
SharedPreferences sharedPrefeences = PreferenceManager.getDefaultSharedPreferences(this);
// Instantiate Gson
Gson gson = new GsonBuilder().create();
GsonConverterFactory converterFactory = GsonConverterFactory.create(Gson);
// Build Retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(converterFactory)
.client(client) // custom client
.build();
可以看到上面使用快取cache用到了Application的context,這也是在android中使用非常多的上下文物件。
我們的第一個Dagger模組(Module)AppModule.java
(使用@Module
進行類註解),將會提供Application
的context引用。我們使用@Provides
註解告訴Dagger providesApplication()這個方法是Application的例項的提供者。使用@Singleton
註解告訴Dagger整個生命週期中只會被初始化一次。
@Module
public class AppModule {
Application mApplication;
public AppModule(Application application) {
mApplication = application;
}
@Provides
@Singleton
Application providesApplication() {
return mApplication;
}
}
和上面類似,下面這段程式碼我們進行了了Gson,Cache,OkHttpClient以及Retrofit 的例項化,這些方法的返回型別都會在定義到依賴關係(依賴表 dependency graph)中。在這裡我們需要關注的是三個註解的@Module
,@Provides
,@Singleton
的定義位置。
@Module
public class NetModule {
String mBaseUrl;
// Constructor needs one parameter to instantiate.
public NetModule(String baseUrl) {
this.mBaseUrl = baseUrl;
}
// Dagger will only look for methods annotated with @Provides
@Provides
@Singleton
// Application reference must come from AppModule.class
SharedPreferences providesSharedPreferences(Application application) {
return PreferenceManager.getDefaultSharedPreferences(application);
}
@Provides
@Singleton
Cache provideOkHttpCache(Application application) {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(application.getCacheDir(), cacheSize);
return cache;
}
@Provides
@Singleton
Gson provideGson() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
return gsonBuilder.create();
}
@Provides
@Singleton
OkHttpClient provideOkHttpClient(Cache cache) {
OkHttpClient client = new OkHttpClient();
client.setCache(cache);
return client;
}
@Provides
@Singleton
Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) {
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl(mBaseUrl)
.client(okHttpClient)
.build();
return retrofit;
}
}
可以看到我們通過@Module
標註類,@Provide
和@Singleton
標註方法完成了這些物件例項的建立。那麼我們怎麼獲取這些物件例項呢?
Dagger2通過@inject
註解提供了例項的獲取,通過呼叫@inject
會讓Dagger2
在依賴關係(依賴表 dependency graph)中找到對應的例項物件並賦值給該欄位。比如下面的例子就會返回MyTwitterApiClient
,SharedPreferences
的例項物件。
public class MainActivity extends Activity {
@Inject MyTwitterApiClient mTwitterApiClient;
@Inject SharedPreferences sharedPreferences;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
InjectorClass.inject(this);
}
上面的Module類都會需要一個context,有的時候是Activity context,有的時候是Application context,所以上面完成了提供 和使用例項 。
這讓我想起了小時候最怕的打針。就好像打針過程一樣,我們有了藥物(提供的例項),你的身體生病了需要藥物(使用這個例項),我們需要注射器把藥物注入你的身體裡面。(關聯這個例項)
提供<->關聯<->使用
可以看到上面通過InjectorClass.inject(this)把當前activity物件注入到InjectorClass,那麼InjectorClass是什麼呢?正是這個關聯過程。
在Dagger2 中 ,注入類(injector class)被稱作元件(Component),我們通過inject方法傳遞activity,service或者fragment物件到注入類component中。比如下面這個類。我們通過@Component
註解當前類,並且把之前的兩個模組AppModule.class,
NetModule.class
也新增到component中。( Components從根本上來說就是一個注入器,也可以說是@Inject和@Module的橋樑。)
@Singleton
@Component(modules={AppModule.class, NetModule.class})
public interface NetComponent {
void inject(MainActivity activity);
// void inject(MyFragment fragment);
// void inject(MyService service);
}
到這裡我們就把Dagger2 的大致流程梳理了一遍。
那麼你就會好奇這個註解類是怎麼完成整個注入的呢?(也就是說這個關聯過程)
Dagger2中很重要的一點就是它會為@Component
註解的類生成程式碼。它會在類的前面新增上Dagger字首(比如上面的類就會生成DaggerNetComponent
.java
),也就是這個類負責初始化依賴關係(依賴表 dependency graph)中的例項,併為註解了@Inject
的欄位執行注入操作。接著往下看。
2.5 初始化元件(Instantiating the component)
初始化元件操作應當在Application中進行操作,因為這些例項在整個application生命週期中只會被例項化一次。
public class MyApp extends Application {
private NetComponent mNetComponent;
@Override
public void onCreate() {
super.onCreate();
// Dagger%COMPONENT_NAME%
mNetComponent = DaggerNetComponent.builder()
// list of modules that are part of this component need to be created here too
.appModule(new AppModule(this)) // This also corresponds to the name of your module: %component_name%Module
.netModule(new NetModule("https://api.github.com"))
.build();
// If a Dagger 2 component does not have any constructor arguments for any of its modules,
// then we can use .create() as a shortcut instead:
// mAppComponent = com.codepath.dagger.components.DaggerNetComponent.create();
}
public NetComponent getNetComponent() {
return mNetComponent;
}
}
可以看到的是我們直接使用NetComponent
生成的類DaggerNetComponent
並且生成的方法appModule
和netModule
完成了兩個對應module的初始化。
因為這裡我們繼承了
Application
並作出了修改,所以需要在AndroidManifest.xml
中作出修改如下。
<application android:allowBackup="true" android:name=".MyApp">
在activity中,我們需要獲取component並且呼叫inject()方法。注意需要將獲取的Application
強制轉換為MyApp
。這也完成了上面InjectorClass.inject(this);
程式碼的替換。
public class MyActivity extends Activity {
@Inject OkHttpClient mOkHttpClient;
@Inject SharedPreferences sharedPreferences;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
// We need to cast to `MyApp` in order to get the right method
((MyApp) getApplication()).getNetComponent().inject(this);
}
到這裡就完成了整個Dagger2的依賴注入流程.
Dagger2的使用還有一些注意點。包括下面的限定型別,作用域,組建依賴,以及子元件。
2.6 限定型別(Qualified types)
Dagger 修飾符
如果對於不同的物件有同樣的返回型別,我們可以使用@Named
修飾符註解。你需要在提供單例的地方(@Provides
註解)和注入的地方(@Inject
註解)都使用@Named
註解。
比如,對於同樣的返回OkHttpClient ,這裡提供不同的方法,和java中多型一樣,只不過這裡需要額外通過@Named
註解來標註:
@Provides @Named("cached")
@Singleton
OkHttpClient provideOkHttpClient(Cache cache) {
OkHttpClient client = new OkHttpClient();
client.setCache(cache);
return client;
}
@Provides @Named("non_cached")
@Singleton
OkHttpClient provideOkHttpClient() {
OkHttpClient client = new OkHttpClient();
return client;
}
@Inject @Named("cached") OkHttpClient client;
@Inject @Named("non_cached") OkHttpClient client2;
如下,@Named
是在Dagger中預先定義好的修飾符,你也可以建立自己的修飾符註解。關於自定義註解,我之前的一篇文章【譯】從java註解分析ButterKnife工作流程有所提及。
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface DefaultPreferences {
}
2.7 作用域(Scopes)
dagger 作用域
Scopes可是非常的有用,Dagger2可以通過自定義註解限定註解作用域。@Singleton
是被Dagger預先定義的作用域註解(
scope annotation )。沒有指定作用域的@Provides
方法將會在每次注入的時候都建立新的物件。同樣的,你也可以定義自己的Scope註解。
@Scope
@Documented
@Retention(value=RUNTIME)
public @interface MyActivityScope
你可以在官方文件中找到這樣一段文字
/** * In Dagger, an unscoped component cannot depend on a scoped component. As * {@link edu.com.app.injection.component.ApplicationComponent} is a scoped component ({@code @Singleton}, we create a custom * scope to be used by all fragment components. Additionally, a component with a specific scope * cannot have a sub component with the same scope. */
也就是說一個沒有scope的元件component不可以以來一個有scope的元件component。子元件和父元件的scope不能相同。我們通常的
ApplicationComponent
都會使用Singleton
註解,也就會是說我們如果自定義component必須有自己的scope。在下面元件依賴中會再次提及。
2.8 元件依賴(Component Dependencies)
dagger 依賴
上面的例子我們建立了application的全域性單例.如果我們想在記憶體中總是擁有多個元件(例如在activity和fragment生命週期,使用者登入註冊建立的component),我們可以使用元件依賴(Component Dependencies),使用元件依賴有下面幾個考慮點:
- 兩個依賴的元件不能共享作用域,比如兩個元件不能共享
@Singleton
作用域。這個限制產生的原因看這裡。依賴的元件需要定義自己的作用域。 - 儘管Dagger2 有建立作用域例項的能力,你也需要建立和刪除引用來滿足行為的一致性。Dagger2 不會知道任何底層的實現。可以看看Stack Overflow 的這個 討論
- 當建立依賴元件的時候,父元件需要顯示的暴露物件給子元件。比如子元件需要知道Retrofit 物件,也就需要顯示的暴露出來。
@Singleton
@Component(modules={AppModule.class, NetModule.class})
public interface NetComponent {
// downstream components need these exposed with the return type
// method name does not really matter
Retrofit retrofit();
}
2.9 子元件(Subcomponents)
dagger 子元件
除了依賴關係,也可以使用子元件進行物件關係(物件表/圖 object graph)繼承。和元件之間新增依賴關係一樣,子元件也有自己的生命週期,也會在所有對其應用不在的時候被垃圾回收,也有同樣的作用域限制。區別於元件依賴的不同點主要是:
- 需要在父元件的介面中宣告(在介面中定義的方法對於生成的物件是可訪問的。)。
- 能夠獲取父元件的所有元素(不僅僅是在介面中宣告的元素)。
比如下面這段程式碼:
@Module
public class MyActivityModule {
private final MyActivity activity;
public MyActivityModule(MyActivity activity) { this.activity = activity; }
@Provides @MyActivityScope @Named("my_list")
public ArrayAdapter providesMyListAdapter() {
return new ArrayAdapter<String>(activity, android.R.layout.my_list);
}
...
}
@MyActivityScope
@Subcomponent(modules={ MyActivityModule.class })
public interface MyActivitySubComponent {
@Named("my_list") ArrayAdapter myListAdapter();
}
@Singleton
@Component(modules={ ... })
public interface MyApplicationComponent {
MyActivitySubComponent newMyActivitySubcomponent(MyActivityModule activityModule);
}
在上面的例子中,子元件的例項在每次我們呼叫newMyActivitySubcomponent()
的時候都會被建立。使用子模組去注入一個activity:
public class MyActivity extends Activity {
@Inject ArrayAdapter arrayAdapter;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
// We need to cast to `MyApp` in order to get the right method
((MyApp) getApplication()).getApplicationComponent())
.newMyActivitySubcomponent(new MyActivityModule(this))
.inject(this);
}
}
最後再來梳理一下Dagger2 中的一些注意點:
- Components從根本上來說就是一個注入器,也可以說是@Inject和@Module的橋樑。 Components可以提供所有定義了的型別的例項,比如:我們必須用@Component註解一個介面然後列出所有的@Modules組成該元件,如 果缺失了任何一塊都會在編譯的時候報錯。@Component介面定義了物件提供者(module)和物件之間的聯絡,也表述了一種依賴關係。
- 由於Dagger2使用生成的程式碼去訪問欄位,所以欄位使用了Dagger2 是不允許標註為private的。
- Dagger2 基於JSR 330(為了最大程度的提高程式碼的複用性、測試性和維護性,java的依賴注入為注入類中的使用定義了一整套註解(和介面)標準。Dagger1和Dagger2(還有Guice)都是基於這套標準,給程式帶來了穩定性和標準的依賴注入方法。)
- 使用@inject註解表示依賴關係可以用於三個地方。建構函式,欄位或者方法中)。
- Dagger2會在編譯時通過apt生成程式碼進行注入。
以後的開發中,那麼多需要使用例項的地方,只需要簡簡單單地來一個@inject,而不需要關心是如何注入的。Dagger2讓你愛不釋手。
那麼接下來我們分析官方架構Dagger2 又是怎麼使用的吧?
3 google官方MVP架構回顧
上一篇文章 中,我們分析到整個專案是按照功能模組進行劃分(addedittask,statistics,taskdetail,tasks四個模組)並且將資料和工具類分別提取到data和util包中。我們對taskdetial模組進行了分析。這裡提取上一篇文章中的結論
3.1 官方MVP例項,通過協議類XXXContract來對View和Presenter的介面進行內部繼承。是對BaseView和BasePresenter的進一步封裝,所以我們實現的View和Presenter也只需要繼承XXXContract中的對應內部