前言
因為工作需求,所以最近補了補之前沒了解過的Dagger2的內容,基礎篇已經發布。接下來就是Dagger2在Android中的應用了。當然,和我一樣剛接觸Dagger2的朋友,可以先看一下之前的基礎文章:
正文
這篇文章的Demo實在是太好了。所以我就厚顏無恥的把他的程式碼拿過來用...這是一個外國哥們的文章,我猜他應該不會怪我的,哈哈...
進入正文之前,我們先看一下背景。程式碼需求很簡單,從一個API上獲取資料,然後載入到RecycleView上,並且會涉及到圖片載入。 在這麼一個需求之下,我們如果使用Dagger2為我們的工程提供相應的依賴呢?
簡單羅列一下程式碼設計
- MainActivity.java:請求API並顯示專案 RecyclerView
- Result.java:用於API響應的POJO,使用JSON Schema建立到POJO
- RandomUsersAdapter.java:介面卡 RecyclerView
涉及以下依賴項和庫。
- Retrofit
- GsonBuilder&Gson
- HttpLoggingInterceptor
- OkHttpClient
- Picasso
以上內容不重要,都是我們們日常開發常用的東西。怎麼使用啥的,大家肯定都很屬性,所以下文demo很多初始化啥的就跳過了,我們們的關注是Dagger2在Android中的應用。
注意,這裡不是叭叭叭的貼程式碼,講API,而是從一個業務出發,以Dagger2的角度講解Daager2的依賴關係,相信你們會我和一樣,看完一定會“撥雲霧見青天”,哈哈~
一般解決方案
面對這種需求,我們的常規寫法:
public class MainActivity extends AppCompatActivity {
// 省略變數宣告
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 省略View的初始化
// 省略Gson初始化
// 省略HttpLoggingInterceptor及OkHttpClient初始化
// 不省略太多了,免得失去代入感,哈哈。
retrofit = new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://randomuser.me/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
populateUsers();
}
// 網路請求
private void populateUsers() {
Call<RandomUsers> randomUsersCall = getRandomUserService().getRandomUsers(10);
randomUsersCall.enqueue(new Callback<RandomUsers>() {
@Override
public void onResponse(Call<RandomUsers> call, @NonNull Response<RandomUsers> response) {
if(response.isSuccessful()) {
mAdapter = new RandomUserAdapter();
mAdapter.setItems(response.body().getResults());
recyclerView.setAdapter(mAdapter);
}
}
// 省略請求失敗
});
}
public RandomUsersApi getRandomUserService(){
return retrofit.create(RandomUsersApi.class);
}
}
複製程式碼
public class RandomUserAdapter extends RecyclerView.Adapter<RandomUserAdapter.RandomUserViewHolder> {
private List<Result> resultList = new ArrayList<>();
public RandomUserAdapter() {}
@Override
public RandomUserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_random_user,
parent, false);
return new RandomUserViewHolder(view);
}
@Override
public void onBindViewHolder(RandomUserViewHolder holder, int position) {
Result result = resultList.get(position);
// setText操作
holder.textView.setText(String.format("%s %s", result.getName().getFirst(),
result.getName().getLast()));
// 圖片庫載入圖片
Picasso.with(holder.imageView.getContext())
.load(result.getPicture().getLarge())
.into(holder.imageView);
}
// 省略部分程式碼
}
複製程式碼
寫完上述程式碼之後,讓我們挺一分鐘,想一想我們剛才寫的東西,是不是有明顯的依賴關係?比如,我們的Activity依賴Retrofit,我們的Retrofit又依賴OkHttp等等這種關係。
而且,所有的初始化操作都其中在Activity做了處理,如果此時我們需要更多的Activity,難道還有一遍遍寫重複的程式碼?
當然可能有朋友會說可以使用基類,或者單例等等的封裝方式。不過今天我們通通不考慮這些,今天只聊Dagger2
上述的業務其實還很多內容沒有考慮到,比如說快取...因此我們的業務如果更精細一些會發現,還有依賴更多的模組:
- File/DB: 持久化快取
- 記憶體Cache: 記憶體快取
- OkHttp3Downloader: 下載模組
所以如果完整的展開程式碼,應該是這樣的:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
// Gson依賴
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
// File持久化依賴
File cacheFile = new File(this.getCacheDir(), "HttpCache");
cacheFile.mkdirs();
// Cache記憶體依賴
Cache cache = new Cache(cacheFile, 10 * 1000 * 1000); //10 MB
// Log依賴
HttpLoggingInterceptor httpLoggingInterceptor = new
HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(@NonNull String message) {
Timber.i(message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
// OkHttp依賴
OkHttpClient okHttpClient = new OkHttpClient()
.newBuilder()
.cache(cache)
.addInterceptor(httpLoggingInterceptor)
.build();
OkHttp3Downloader okHttpDownloader = new OkHttp3Downloader(okHttpClient);
// Picasso依賴
picasso = new Picasso.Builder(this).downloader(okHttpDownloader).build();
// Retrofit依賴
retrofit = new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://randomuser.me/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
populateUsers();
}
複製程式碼
作為“久經沙場”的老司機,這些都是家常便飯,便飯之餘我們聊一些茶語飯後的話題:從這些初始化程式碼中抽象出一個依賴圖。就如果Retrofit初始化時傳了一個Gson,那就說明Retrofit依賴Gson...
因此,我們差不多能夠梳理一個依賴關係圖:
綠色框表示它們是依賴關係中的頂級的(任何模組都不想要依賴它),它們只會被依賴。 結合我們寫過的初始化程式碼,這個圖很好理解吧?我猜肯定有朋友這張圖都已經在寫下程式碼之時出現在腦海中了...
走到這一步,其實問題就已經顯現出來了。這麼龐大的初始化的過程,任誰都不會想再寫第二遍。因此重構迫在眉睫。既然我們都已經捋清楚了我們所需要模組的依賴關係,那麼接下來就是讓Dagger2大展身手的時候了...
Dagger2登場
前置準備
最開始當然是引入我們的Dagger2模組,沒啥好說的...
dependencies {
implementation 'com.google.dagger:dagger:2.13'
annotationProcessor 'com.google.dagger:dagger-compiler:2.13'
}
複製程式碼
第1步:建立元件(Component)
元件將充當整個依賴關係圖的公共介面。使用元件的最佳實踐是僅暴露出最頂級的依賴關係。
這意味著,在Component中我們只提供在依賴圖中綠色辨識的類。也就是:RandomUsersAPI和Picasso。
建立一個名為RandomUserComponent
的元件並對外暴露RandomUsersApi
和Picasso
。
@Component
public interface RandomUserComponent {
RandomUsersApi getRandomUserService();
Picasso getPicasso();
}
複製程式碼
Component將提供最頂層的依賴:RandomUsersApi和Picasso。
第2步:建立模組(Module)
我們現在需要將MainActivity
中的程式碼移動到不同的模組去。所以接下來,我們需要基於依賴圖去設計我們需要哪些模組。
首先是,RandomUsersModule
:
RandomUsersModule
,構建它我們需要提供RandomUsersApi
、GsonConverterFactory
、Gson
和Retrofit
以及一個OkHttpClient。
@Module
public class RandomUsersModule {
@Provides
public RandomUsersApi randomUsersApi(Retrofit retrofit){
return retrofit.create(RandomUsersApi.class);
}
@Provides
public Retrofit retrofit(OkHttpClient okHttpClient,
GsonConverterFactory gsonConverterFactory, Gson gson){
return new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://randomuser.me/")
.addConverterFactory(gsonConverterFactory)
.build();
}
@Provides
public Gson gson(){
GsonBuilder gsonBuilder = new GsonBuilder();
return gsonBuilder.create();
}
@Provides
public GsonConverterFactory gsonConverterFactory(Gson gson){
return GsonConverterFactory.create(gson);
}
}
複製程式碼
寫到這,我們會發現,想要提供一個OkHttpClient
需要提供太多的依賴,因此讓我們建立一個OkHttpClientModule
,它提供OkHttpClient
、Cache
、HttpLoggingInterceptor
和File
以及一個Context。
@Module
public class OkHttpClientModule {
@Provides
public OkHttpClient okHttpClient(Cache cache, HttpLoggingInterceptor httpLoggingInterceptor){
return new OkHttpClient()
.newBuilder()
.cache(cache)
.addInterceptor(httpLoggingInterceptor)
.build();
}
@Provides
public Cache cache(File cacheFile){
return new Cache(cacheFile, 10 * 1000 * 1000); //10 MB
}
@Provides
public File file(Context context){
File file = new File(context.getCacheDir(), "HttpCache");
file.mkdirs();
return file;
}
@Provides
public HttpLoggingInterceptor httpLoggingInterceptor(){
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Timber.d(message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return httpLoggingInterceptor;
}
}
複製程式碼
寫完這個,我們會發現沒辦法給它提供Context
,現在先不要著急,因為我們發現似乎還有一個模組也需要Context
:
沒錯,就是Picasso
,讓我們建立一個PicassoModule
,並且為它提供:Picasso
和OkHttp3Downloader
。
@Module
public class PicassoModule {
@Provides
public Picasso picasso(Context context, OkHttp3Downloader okHttp3Downloader){
return new Picasso.Builder(context).
downloader(okHttp3Downloader).
build();
}
@Provides
public OkHttp3Downloader okHttp3Downloader(OkHttpClient okHttpClient){
return new OkHttp3Downloader(okHttpClient);
}
}
複製程式碼
OK,我們的高層模組已準備就緒,不過大家還記不記得一個遺留問題:PicassoModule
和OkHttpClientModule
需求Context
。Context
的地位不言而喻,因此我們一定會遇到其他模組也需要Context
的情形。那麼為什麼不給它一個模組呢?
@Module
public class ContextModule {
Context context;
public ContextModule(Context context){
this.context = context;
}
@Provides
public Context context(){ return context.getApplicationContext(); }
}
複製程式碼
到此,我們第2步的準備工作就完成了,接下來我們需要讓它們需要互相提供依賴!
第3步:連線所有模組
現在,我們已經準備好所有模組和元件:
但是我們讓彼此獨立的模組,互相依賴呢?這是includes屬性發揮作用的地方。includes屬性包括當前模組中涉及的其他模組的依賴關係。
什麼模組需要包括在內?
RandomUsersModule
需要OkHttpClientModule
OkHttpClientModule
需要ContextModule
PicassoModule
需要OkHttpClientModule
和ContextModule
。但由於已經OkHttpClientModule
與之相關ContextModule
,所以我們只包括OkHttpClientModule
。
//in RandomUsersModule.java
@Module(includes = OkHttpClientModule.class)
public class RandomUsersModule { ... }
//in OkHttpClientModule.java
@Module(includes = ContextModule.class)
public class OkHttpClientModule { ... }
//in PicassoModule.java
@Module(includes = OkHttpClientModule.class)
public class PicassoModule { ... }
複製程式碼
通過提供上述內容,我們已經連結了所有模組。
第4步:連通元件
現在,我們所有的模組(Module)都已連線,可以相互依賴了。那麼接下來,我們只需告訴我們的頂層元件RandomUserComponent
,需要依賴哪些模組來使自己能夠正常工作。
有了第3步的基礎,這裡應該很容易捋清關係吧?只不過這裡不用includes,而是用modules。
@Component(modules = {RandomUsersModule.class, PicassoModule.class})
public interface RandomUserComponent {
RandomUsersApi getRandomUserService();
Picasso getPicasso();
}
複製程式碼
第5步:Build就行了
是時候Build工程了。Dagger將使用Builder模式建立RandomUserComponent
。現在,我們的MainActivity
可以很容易地獲得Picasso並RandomUsersApi
。
public class MainActivity extends AppCompatActivity {
RandomUsersApi randomUsersApi;
Picasso picasso;
....
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
RandomUserComponent daggerRandomUserComponent = DaggerRandomUserComponent.builder()
.contextModule(new ContextModule(this))
.build();
picasso = daggerRandomUserComponent.getPicasso();
randomUsersApi = daggerRandomUserComponent.getRandomUserService();
populateUsers();
...
}
...
}
複製程式碼
大功告成,我們很“魔法”的完成了依賴注入,就醬,是不是cao簡單噠?...
但是
每次呼叫<DaggerComponent>.build()
時,它都會建立所有物件或依賴項的新例項,我們都很清楚,這些內容單例就好了。為什麼Dagger2不知道我們只需要一個Picasso
單例呢?
換句話說,我們如何告訴Dagger2為我們提供單例項依賴?這就是下一篇內容所要涉及的內容~
尾聲
說實話,關於Dagger2怎麼說呢?要不是因為組裡有一個Dagger2大佬,還真沒信心沒動力去搞它。不過既然有資源,那就學一學吧。
唉,別tm更新啦,學不動啦~