出来混迟早要还的,技术债Dagger2:Android篇(上)

2,253 阅读8分钟

前言

因为工作需求,所以最近补了补之前没了解过的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大展身手的时候了...

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

通过图,我们可以基本设计出来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

@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,并且为它提供: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步:连接所有模块

现在,我们已经准备好所有模块和组件:

但是我们让彼此独立的模块,互相依赖呢?这是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 { ... }

通过提供上述内容,我们已经链接了所有模块。

第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更新啦,学不动啦~

个人公众号:咸鱼正翻身