初心者のAndroid開発者向けの短剣2。 ダガー2.アドバンス。 パート1

この記事は、著者によると、 依存関係Dagger 2フレームワークの実装を理解できないか、それを実行しようとしている人向けの一連の記事の第6部です。 オリジナルは2017年12月23日に書かれました。 無料翻訳。

ダガー2アドバンストパート1画像

これは、 初心者のAndroid開発者向けのDagger 2シリーズの6番目の記事です。 前のものを読んでいない場合、 ここにいます。

一連の記事



一連の記事の前半


生成されたDagger 2クラスを分析し、Dagger 2がBuilderパターンを使用して必要な依存関係を提供する方法を調べました。
その後、 @Moduleおよび@Providesアノテーションを使用する簡単な例@Provides

まえがき


この記事はあなたにとって少し大きいように思えるかもしれません。 通常、私の記事は800文字を超えません。 私はそれをより小さな部分に分解したかったのですが、記事が非常に大きい理由は、途中で長時間ハード接続(ハード依存関係)の問題を解決すると、迷子になる可能性があるからです。

しかし、記事にはチェックポイント(チェックポイント)を含めました。 これらの場所では、少し休憩して気を散らすことができます。 これはDagger 2と依存性注入(DI)の初心者にとって便利だと思います。

Androidホーム


家のAndroidイメージ

これまで、例で通常のJavaプロジェクトを見てきました。 皆さんのほとんどがDIについて、そしてDagger 2を使用してDIを実装する方法を理解していることを願っています。 ここで、Androidアプリケーションの実際の例に飛び込み、このプロジェクトでDagger 2を使用してみてください。

Googleコードラボのようにすべてを1か所に集めるために、キックスタートブランチを作成しまし 。 私たちの目標は、このプロジェクトの強いつながりをなくすことです。 ソリューションの一部は、このプロジェクトの別々のブランチに配置されます。

プロジェクトの説明


これは非常に単純なプロジェクトです。 その中で、 Random Users APIを使用してランダムユーザーを受け取り、 RecyclerView表示します。 プロジェクトの説明にあまり時間をかけず、抽象的に説明します。 ただし、プロジェクトでのDagger 2の実装が可能な限り明確でシンプルになるように、コードを慎重に逆アセンブルしてください。

#クラスとパッケージ



依存関係


プロジェクト機能を実装するには、次のライブラリを使用します。


前の例で見たように、 MainActivityには依存関係がMainActivityます。 MainActivityを作成するMainActivity 、依存関係インスタンスがMainActivity度も作成されます。

 public class MainActivity extends AppCompatActivity { Retrofit retrofit; RecyclerView recyclerView; RandomUserAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); GsonBuilder gsonBuilder = new GsonBuilder(); Gson gson = gsonBuilder.create(); Timber.plant(new Timber.DebugTree()); HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { @Override public void log(@NonNull String message) { Timber.i(message); } }); httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient okHttpClient = new OkHttpClient() .newBuilder() .addInterceptor(httpLoggingInterceptor) .build(); retrofit = new Retrofit.Builder() .client(okHttpClient) .baseUrl("https://randomuser.me/") .addConverterFactory(GsonConverterFactory.create(gson)) .build(); populateUsers(); } private void initViews() { recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); } 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); } } @Override public void onFailure(Call<RandomUsers> call, Throwable t) { Timber.i(t.getMessage()); } }); } public RandomUsersApi getRandomUserService(){ return retrofit.create(RandomUsersApi.class); } } 

(チェックポイント)
...

既存の問題


MainActivityを見ると、次の問題に気づくでしょう。

#オブジェクトの厄介な初期化


onCreate()メソッドを見ると、その中に厄介な初期化が見つかることがあります。 もちろん、この方法でこのメソッドのオブジェクトを引き続き初期化することはできますが、問題を解決する正しい方法を見つけた方がよいでしょう。

#テスト容易性


また、コードをテストする方法を見つける必要があります。 また、 Adapter内のPicassoもテスト機能を妨害します。 この依存関係をコンストラクターに渡すと便利です。

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

例を少し複雑にしましょう


MainActivityクラスの上記の依存関係は、プロジェクトを掘り下げて快適に感じるためにのみ必要でした。 さらに深く進むと、実際のプロジェクトと同様に、依存関係が増えます。 さらにいくつか追加しましょう。

以前に考慮された依存関係に加えて、次を追加します。


コードは次のようになります( 別のブランチで完全な例表示できます)。

 public class MainActivity extends AppCompatActivity { Retrofit retrofit; RecyclerView recyclerView; RandomUserAdapter mAdapter; Picasso picasso; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); GsonBuilder gsonBuilder = new GsonBuilder(); Gson gson = gsonBuilder.create(); Timber.plant(new Timber.DebugTree()); File cacheFile = new File(this.getCacheDir(), "HttpCache"); cacheFile.mkdirs(); Cache cache = new Cache(cacheFile, 10 * 1000 * 1000); //10 MB HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { @Override public void log(@NonNull String message) { Timber.i(message); } }); httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient okHttpClient = new OkHttpClient() .newBuilder() .cache(cache) .addInterceptor(httpLoggingInterceptor) .build(); OkHttp3Downloader okHttpDownloader = new OkHttp3Downloader(okHttpClient); picasso = new Picasso.Builder(this).downloader(okHttpDownloader).build(); retrofit = new Retrofit.Builder() .client(okHttpClient) .baseUrl("https://randomuser.me/") .addConverterFactory(GsonConverterFactory.create(gson)) .build(); populateUsers(); } private void initViews() { recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); } 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(picasso); mAdapter.setItems(response.body().getResults()); recyclerView.setAdapter(mAdapter); } } @Override public void onFailure(Call<RandomUsers> call, Throwable t) { Timber.i(t.getMessage()); } }); } public RandomUsersApi getRandomUserService(){ return retrofit.create(RandomUsersApi.class); } } 

(チェックポイント)
...

依存グラフ


依存関係グラフは、クラス間の依存関係を説明する図にすぎません。 このようなグラフの形成により、実装がより理解しやすくなります(これは終わり近くでわかります)。 プロジェクトの依存関係グラフを見てください。
DIグラフ画像
最上位の依存関係は緑色でマークされています。つまり、他の依存関係では必要ありませんが、いくつかの依存関係が必要です。

このチャートの読み方 たとえば、 Picasso OkHttp3DownloaderContext 2つの依存関係があります。

APIを使用してカジュアルユーザーを獲得するには、 Retrofitが必要です。 彼は、 GsonConvertFactoryOkHttpClientなどの2つの依存関係を必要とします。

MainActivityのコードを見て、理解を深めるためにチャートと比較してください。
(チェックポイント)
...

Dagger 2を使用した依存関係の展開


完全なコードは、別のプロジェクトブランチにあります。

注:



ステップ1. Dagger 2のインストール


build.gradleファイルに数行を追加するだけです。

 dependencies { implementation 'com.google.dagger:dagger:2.13' annotationProcessor 'com.google.dagger:dagger-compiler:2.13' } 

ステップ2.コンポーネントの作成


コンポーネントは、依存関係グラフ全体のインターフェイスになります。 コンポーネントを使用するためのベストプラクティスは、その最上位の依存関係のみを宣言し、残りの依存関係を非表示にすることです。

これは、依存関係グラフで緑色でマークされている依存関係のみがコンポーネント、つまりRandomUsersAPIPicasso存在することをRandomUsersAPIします。

 @Component public interface RandomUserComponent { RandomUsersApi getRandomUserService(); Picasso getPicasso(); } 

Component自身は、 RandomUsersAPIPicasso依存関係を取得する場所をどのように理解しますか? モジュールを使用します。

ステップ3.モジュールの作成


ここで、コードをMainActivityからさまざまなモジュールに移動する必要があります。 依存関係グラフを見ると、必要なモジュールを決定できます。

1つ目はRandomUsersModuleRandomUsersApiGsonConverterFactoryGson 、およびGsonConverterFactory依存関係を提供します。

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

2つ目はPicassoModulePicassoおよび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); } } 

RandomUsersModuleモジュールでは、 RandomUsersModule Retrofit必要OkHttpClient 。 順番に、他の依存関係が必要です。 このために別のモジュールを作成しませんか?

OkHttpClientCacheHttpLoggingInterceptorおよびFileを提供するOkHttpClientModuleを作成しFile

 @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; } } 

モジュールの準備はほぼ完了していますが、 PicassoModuleOkHttpClientModuleContext必要Contextあり、他の場所で役立つかもしれません。 これらの目的のためにモジュールを作成します。

 @Module public class ContextModule { Context context; public ContextModule(Context context){ this.context = context; } @Provides public Context context(){ return context.getApplicationContext(); } } 

ステップ4.モジュールを接続する


これで、下の図のように、すべてのモジュールとコンポーネントができました。 しかし、 Contextを他のモジュールに渡す方法は? 互いに依存するモジュールをバインドする必要があります。
モジュールおよびコンポーネントイメージ
モジュール間の通信を実装するには、 includes属性が必要です。 この属性には、参照されるモジュールの現在のモジュール依存関係が含まれます。

どのモジュールを接続する必要がありますか?


 //  RandomUsersModule.java @Module(includes = OkHttpClientModule.class) public class RandomUsersModule { ... } //  OkHttpClientModule.java @Module(includes = ContextModule.class) public class OkHttpClientModule { ... } //  PicassoModule.java @Module(includes = OkHttpClientModule.class) public class PicassoModule { ... } 

そこで、すべてのモジュールを接続しました。
linjed modules image

ステップ5.コンポーネントとモジュールのリンク


現時点では、すべてのモジュールが接続されており、互いに通信しています。 今こそ、必要な依存関係を提供するモジュールを使用するようにComponentに伝えるか、トレーニングするときです。

includes属性を使用してモジュールを相互にリンクしたため、同様に、 modules属性を使用してコンポーネントとモジュールをリンクできます。

作成されたコンポーネント( getRandomUserService()およびgetPicasso()メソッド)のニーズを考慮して、 modules属性を使用してコンポーネントにRandomUsersModuleおよびPicassoModuleモジュールへのリンクを含めます。

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

コンポーネントとモジュールは接続イメージです

ステップ6.プロジェクトの組み立て


すべてを正しく行った場合、Dagger 2は、必要な依存関係を提供する、作成したコンポーネントに基づいてクラスを生成します。

MainActivityでは、 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(); ... } ... } 


ステップ7.おめでとうございます!


やった! AndroidアプリでDagger 2を使用しました。 祝福して休憩
(チェックポイント)
...
GIF
画像

しかし、問題があります


なに? 問題は何ですか?

DaggerComponent.build()を呼び出すDaggerComponent.build() 、構成したすべてのオブジェクトまたは依存関係の新しいインスタンスが作成されます。 この場合、Dagger 2がPicassoインスタンスを1つだけ必要とすることを知らないのはなぜですか? 言い換えると、シングルトンとして依存関係を提供するようにDagger 2に指示するにはどうすればよいですか?

@Scope


@Scopeは、 DaggerComponent.build()が複数回呼び出された場合でも、Dagger 2に単一のインスタンスのみを作成するように指示します。 これにより、依存関係がシングルトンとして機能します。 目的の領域(スコープ)を構成するには、独自の注釈を作成する必要があります。

 @Scope @Retention(RetentionPolicy.CLASS) public @interface RandomUserApplicationScope { } 

@Retentionは、注釈の使用の拒否点を示す注釈です。 彼女は、いつ注釈を使用できるかについて話します。 たとえば、マークSOURCEを使用すると、注釈はソースコードでのみ使用でき、コンパイル中に破棄されます。マークCLASSを使用すると、注釈はコンパイル時に使用できますが、プログラム操作中は使用できません。マークRUNTIMEでは、注釈は実行時に使用できます。

スコープを使用する


作成された領域を使用するには、コンポーネントから開始し、作成された注釈にマークを付けてから、必要な各メソッドをシングルトンとしてマークする必要があります。

 @RandomUserApplicationScope @Component(modules = {RandomUsersModule.class, PicassoModule.class}) public interface RandomUserComponent { ...} @Module(includes = OkHttpClientModule.class) public class PicassoModule { ... @RandomUserApplicationScope @Provides public Picasso picasso(Context context, OkHttp3Downloader okHttp3Downloader){ return new Picasso.Builder(context). downloader(okHttp3Downloader). build(); } ... } @Module(includes = OkHttpClientModule.class) public class RandomUsersModule { ... @RandomUserApplicationScope @Provides public Retrofit retrofit(OkHttpClient okHttpClient, GsonConverterFactory gsonConverterFactory, Gson gson){ return new Retrofit.Builder() .client(okHttpClient) .baseUrl("https://randomuser.me/") .addConverterFactory(gsonConverterFactory) .build(); } ... } 

これは、単一のインスタンスを作成する方法です。

別の問題!


GIF
画像

原則として、各アプリケーションでは、 ApplicationContextActivityコンテキストの2種類のコンテキストを使用します。 それらを提供するには? ContextModuleを使用してApplicationContextを提供できます。 別のモジュールを作成しましょう。

 @Module public class ActivityModule { private final Context context; ActivityModule(Activity context){ this.context = context; } @RandomUserApplicationScope @Provides public Context context(){ return context; } } 

しかし、作成されたクラスは問題を解決しません。 ここで、 Context型の2つの依存関係を提供します。Dagger2はどちらを使用するかを判断できず、エラーが発生します。

@Named


この注釈は、コンテキストを区別するのに役立ちます。 属性を忘れずに、この注釈をメソッドに追加するだけです。

 @Module public class ActivityModule { .... @Named("activity_context") @RandomUserApplicationScope @Provides public Context context(){ return context; } } @Module public class ContextModule { .... @Named("application_context") @RandomUserApplicationScope @Provides public Context context(){ return context.getApplicationContext(); } } 

次に、適切な場所で適切なコンテキストを使用するようにDagger 2に指示します。

 @Module(includes = ContextModule.class) public class OkHttpClientModule { .... @Provides @RandomUserApplicationScope public File file(@Named("application_context") Context context){ File file = new File(context.getCacheDir(), "HttpCache"); file.mkdirs(); return file; } .... } @Module(includes = OkHttpClientModule.class) public class PicassoModule { ... @RandomUserApplicationScope @Provides public Picasso picasso(@Named("application_context")Context context, OkHttp3Downloader okHttp3Downloader){ return new Picasso.Builder(context). downloader(okHttp3Downloader). build(); ... } 

@Named注釈の@Named - @Qualifier


@Named注釈を@Qualifier置き換えるには、別個の注釈を作成し、必要に応じて使用する必要があります。

 @Qualifier public @interface ApplicationContext {} 

次に、対応する依存関係を提供するメソッドに注釈を付けます。

 @Module public class ContextModule { .... @ApplicationContext @RandomUserApplicationScope @Provides public Context context(){ return context.getApplicationContext(); } } 

次に、注釈によって作成されたApplicationContextが必要なすべてのメソッドのパラメーターに注目します。

 @Module(includes = ContextModule.class) public class OkHttpClientModule { ... @Provides @RandomUserApplicationScope public File file(@ApplicationContext Context context){ File file = new File(context.getCacheDir(), "HttpCache"); file.mkdirs(); return file; } .... } @Module(includes = OkHttpClientModule.class) public class PicassoModule { @RandomUserApplicationScope @Provides public Picasso picasso(@ApplicationContext Context context, OkHttp3Downloader okHttp3Downloader){ return new Picasso.Builder(context). downloader(okHttp3Downloader). build(); } .... } 

対応するコミットを見て、 @Namedアノテーションを@Qualifier置き換える方法を確認してください。

まとめ


現時点では、単純なプロジェクトを作成し、Dagger 2と注釈を使用して依存関係を実装しました。

また、3つの新しい注釈についても調査しました。 1つは、単一インスタンスで依存関係を取得するための@Scopeです。 2つ目は@Namedで、同じタイプの依存関係を提供するメソッドを分離します。 3つ目は@Qualifierの代替としての@Namedです。

次は?


現時点では、アプリケーションレベルの依存関係のみを考慮しています。 次の記事では、 Activityレベルの依存関係を調べ、いくつかのコンポーネントを作成し、それらを操作する方法を学習します。 次の記事は1週間で公開されます。

Source: https://habr.com/ru/post/J345372/


All Articles