出來混遲早要還的,技術債Dagger2:Android篇(上)

鹹魚正翻身發表於2019-03-07

前言

因為工作需求,所以最近補了補之前沒了解過的Dagger2的內容,基礎篇已經發布。接下來就是Dagger2在Android中的應用了。當然,和我一樣剛接觸Dagger2的朋友,可以先看一下之前的基礎文章:

出來混遲早要還的,技術債Dagger2:基礎篇

正文

這篇文章的Demo實在是太好了。所以我就厚顏無恥的把他的程式碼拿過來用...這是一個外國哥們的文章,我猜他應該不會怪我的,哈哈...

原文地址:Dagger 2 for Android Beginners — Advanced part I

進入正文之前,我們先看一下背景。程式碼需求很簡單,從一個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:Android篇(上)

綠色框表示它們是依賴關係中的頂級的(任何模組都不想要依賴它),它們只會被依賴。 結合我們寫過的初始化程式碼,這個圖很好理解吧?我猜肯定有朋友這張圖都已經在寫下程式碼之時出現在腦海中了...

走到這一步,其實問題就已經顯現出來了。這麼龐大的初始化的過程,任誰都不會想再寫第二遍。因此重構迫在眉睫。既然我們都已經捋清楚了我們所需要模組的依賴關係,那麼接下來就是讓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的元件並對外暴露RandomUsersApiPicasso

@Component
public interface RandomUserComponent {
    RandomUsersApi getRandomUserService();
    Picasso getPicasso();
}
複製程式碼

Component將提供最頂層的依賴:RandomUsersApi和Picasso。

第2步:建立模組(Module)

我們現在需要將MainActivity中的程式碼移動到不同的模組去。所以接下來,我們需要基於依賴圖去設計我們需要哪些模組。

首先是,RandomUsersModule

出來混遲早要還的,技術債Dagger2:Android篇(上)
通過圖,我們可以基本設計出來RandomUsersModule,構建它我們需要提供RandomUsersApiGsonConverterFactoryGsonRetrofit以及一個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,它提供OkHttpClientCacheHttpLoggingInterceptorFile以及一個Context

出來混遲早要還的,技術債Dagger2:Android篇(上)

@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

出來混遲早要還的,技術債Dagger2:Android篇(上)

沒錯,就是Picasso,讓我們建立一個PicassoModule,並且為它提供:PicassoOkHttp3Downloader

@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,我們的高層模組已準備就緒,不過大家還記不記得一個遺留問題:PicassoModuleOkHttpClientModule需求ContextContext的地位不言而喻,因此我們一定會遇到其他模組也需要Context的情形。那麼為什麼不給它一個模組呢?

@Module
public class ContextModule {
    Context context;

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

    @Provides
    public Context context(){ return context.getApplicationContext(); }
}
複製程式碼

到此,我們第2步的準備工作就完成了,接下來我們需要讓它們需要互相提供依賴!

第3步:連線所有模組

現在,我們已經準備好所有模組和元件:

出來混遲早要還的,技術債Dagger2:Android篇(上)

但是我們讓彼此獨立的模組,互相依賴呢?這是includes屬性發揮作用的地方。includes屬性包括當前模組中涉及的其他模組的依賴關係。

什麼模組需要包括在內?

  • RandomUsersModule需要OkHttpClientModule
  • OkHttpClientModule需要ContextModule
  • PicassoModule需要OkHttpClientModuleContextModule。但由於已經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 { ... }
複製程式碼

通過提供上述內容,我們已經連結了所有模組。

出來混遲早要還的,技術債Dagger2:Android篇(上)

第4步:連通元件

現在,我們所有的模組(Module)都已連線,可以相互依賴了。那麼接下來,我們只需告訴我們的頂層元件RandomUserComponent,需要依賴哪些模組來使自己能夠正常工作。

有了第3步的基礎,這裡應該很容易捋清關係吧?只不過這裡不用includes,而是用modules。

@Component(modules = {RandomUsersModule.class, PicassoModule.class})
public interface RandomUserComponent {
    RandomUsersApi getRandomUserService();
    Picasso getPicasso();
}
複製程式碼

出來混遲早要還的,技術債Dagger2:Android篇(上)

第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更新啦,學不動啦~

個人公眾號:鹹魚正翻身

相關文章