Данная статья является шестой частью серии статей, предназначенных, по словам автора, для тех, кто не может разобраться с внедрением зависимостей и фреймворком Dagger 2, либо только собирается это сделать. Оригинал написан 23 декабря 2017 года. Перевод вольный.
Это шестая статья цикла «Dagger 2 для начинающих Android разработчиков.». Если вы не читали предыдущие, то вам сюда.
Серия статей
- Dagger 2 для начинающих Android разработчиков. Введение.
- Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей. Часть 1 .
- Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей. Часть 2.
- Dagger 2 для начинающих Android разработчиков. Dagger 2. Часть 1.
- Dagger 2 для начинающих Android разработчиков. Dagger 2. Часть 2.
- Dagger 2 для начинающих Android разработчиков. Dagger 2. Продвинутый уровень.
Часть 1 (вы здесь). - Dagger 2 для начинающих Android разработчиков. Dagger 2. Продвинутый уровень.
Часть 2.
Ранее в цикле статей
Мы проанализировали генерируемый Dagger 2 класс и посмотрели на то, как Dagger 2 использует шаблон Builder для предоставления необходимых зависимостей.
После разобрали простой пример использования аннотаций @Module
и @Provides
.
Предисловие
Эта статья может показаться вам немного большой. Обычно мои статьи не превышают 800 символов. Я хотел разбить её на более мелкие части, но причина, по которой статья настолько большая, заключается в том, что если при решении проблемы сильных связей (hard dependencies) надолго остановиться посередине, то возникает шанс потеряться.
Но я включил в статью контрольные точки (Сheckpoint). В этих местах вы можете взять небольшой перерыв и отвлечься. Думаю, это будет полезным для новичков в Dagger 2 и внедрении зависимостей (DI).
Дом Android
До сих пор мы рассматривали обычные Java проекты в примерах. Я надеюсь, что у большинства из вас теперь есть представление о DI и том как Dagger 2 позволяет реализовать DI. Теперь же погрузимся в реальный пример Android приложения и попробуем использовать в этом проекте Dagger 2.
Чтобы собрать всё в одном месте, как в Google code labs, я создал ветку kickstart. Нашей целью будет устранение сильных связей в этом проекте. Части решения будут находится в отдельных ветках этого проекта.
Описание проекта
Это очень простой проект. В нем мы будем получать случайных пользователей, используя Random Users API и показывать их в RecyclerView
. Я не буду тратить много времени на объяснение проекта, буду объяснять абстрактно. Но, пожалуйста, разбирайте код внимательно, чтобы внедрение Dagger 2 в проект проходило для вас максимально понятно и просто.
#Классы и пакеты
MainActivity.java
— Делает запросы к API и показывает полученные элементы вRecyclerView
.- Пакет
Model
— POJO для ответа от API, создан с использованием JSON Schema to POJO RandomUsersAdapter.java
—Adapter
дляRecyclerView
Зависимости
Для реализации функций проекта будут задействованы следующие библиотеки:
Retrofit
— для вызовов APIGsonBuilder
иGson
— для работы с JSONHttpLoggingInterceptor
— для логирования сетевых операцийOkHttpClient
— клиент дляRetrofit
Picasso
— работа с изображениями вAdapter
Как мы видели в предыдущих примерах, в 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);
}
}
(Checkpoint)
...
Существующие проблемы
Если посмотрите на MainActivity
, то заметите следующие проблемы:
#Неуклюжая инициализация объектов
Когда вы смотрите на метод onCreate()
, то можете найти неуклюжими инициализации внутри него. Мы, конечно, можем продолжать таким образом инициализировать объекты в этом методе, но лучше отыскать верный путь для решения проблемы.
#Тестируемость
Также нужно найти путь для того, чтобы тестировать наш код. И Picasso
внутри Adapter
тоже мешает возможности тестирования. Было бы неплохо передавать эту зависимость через конструктор.
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
, нужны были лишь для того, чтобы вы немного вникли в проект и почувствовали себя комфортно. Если углубиться, то как в любом реальном проекте зависимостей станет больше. Давайте добавим ещё несколько.
Кроме ранее рассмотренных зависимостей добавим следующие:
File
— для хранения кэшаCache
— для сетевого кешаOkHttp3Downloader
— загрузчик, использующийOkHttpClient
для загрузки изображенийPicasso
— для обработки изображений из сети
Код будет выглядеть следующим образом (полный пример вы можете просмотреть в отдельной ветке):
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);
}
}
(Checkpoint)
...
Граф зависимостей
Граф зависимостей — это не что иное, как диаграмма, объясняющая зависимости между классами. Формирование такого графа делает реализацию более понятной (вы убедитесь в этом ближе к концу). Посмотрите на граф зависимостей для нашего проекта.
Зеленым отмечены верхнеуровневые зависимости, это означает, что они не нужны никаким другим зависимостям, но им нужны некоторые из зависимостей.
Как читать эту диаграмму? Например, у Picasso
две зависимости — OkHttp3Downloader
и Context
.
Для получения случайных пользователей с помощью API вам нужен Retrofit
. Ему, в свою очередь, нужны две зависимости — GsonConvertFactory
и OkHttpClient
и так далее.
Уделите время на то, чтобы посмотреть на код в MainActivity
и сравнить его с диаграммой для лучшего понимания.
(Checkpoint)
...
Внедряем зависимости с помощью Dagger 2
Полный код вы можете найти в отдельной ветке проекта.
Заметка:
RandomUsersAPI
иRandomUsersApi
— это одно и тоже. Просто опечатка в диаграмме.RandomUsersComponent
будет отличаться в ветке выше и примерах ниже. Предлагаю вам использовать для полного соответствия с примерами ниже новую ветку, а вышеупомянутую оставить для справки.- Пожалуйста, не забудьте добавить проект в закладки на GitHub (Star), если он действительно помог вам в обучении.
Шаг 1. Установка Dagger 2
Просто добавьте несколько строк в build.gradle
файл.
dependencies {
implementation 'com.google.dagger:dagger:2.13'
annotationProcessor 'com.google.dagger:dagger-compiler:2.13'
}
Шаг 2. Создание Component
Component будет интерфейсом для всего графа зависимостей. Лучшей практикой использования Component является объявление только верхнеуровневых зависимостей в нём и скрытие остальных зависимостей.
Это означает, что в Component будут присутствовать только те зависимости, которые помечены в графе зависимостей зеленым цветом, то есть RandomUsersAPI
и Picasso
.
@Component
public interface RandomUserComponent {
RandomUsersApi getRandomUserService();
Picasso getPicasso();
}
Как сам Component поймет где взять зависимости RandomUsersAPI
и Picasso
? Воспользуемся модулями.
Шаг 3. Создание модулей
Сейчас нужно переместить код из MainActivity
в различные модули. Смотря на граф зависимостей можно решить какие модули необходимы.
Первый — RandomUsersModule
, он предоставит зависимости RandomUsersApi
, GsonConverterFactory
, Gson
и Retrofit
.
@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);
}
}
Второй — 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);
}
}
В модуле RandomUsersModule
для Retrofit
нужен OkHttpClient
. Которому, в свою очередь, нужны другие зависимости. Почему бы не сделать для этого отдельный модуль?
Создадим OkHttpClientModule
, который предоставит OkHttpCkient
, Cache
, HttpLoggingInterceptor
и 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;
}
}
Модули почти готовы, но PicassoModule
и OkHttpClientModule
требуется 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
нуждается вOkHttpClientModule
OkHttpClientModule
нуждается вContextModule
PicassoModule
нуждается вOkHttpClientModule
иContextModule
. Так какOkHttpClientModule
уже связан сContextModule
, то можно обойтись толькоOkHttpClientModule
// в 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 { ... }
Таким образом мы соединили все модули.
Шаг 5. Связывание Component и модулей
На данный момент все модули соединены и общаются друг с другом. Сейчас время сказать Component или обучить его обращаться к модулям, которые предоставят ему требуемые зависимости.
Как мы связывали модули между собой с помощью атрибута includes
, подобным образом мы можем связать компонент и модули с помощью атрибута modules
.
Учитывая потребности созданного компонента (методы getRandomUserService()
и getPicasso()
) включим в компонент ссылки на модули RandomUsersModule
и PicassoModule
, используя атрибут modules
.
@Component(modules = {RandomUsersModule.class, PicassoModule.class})
public interface RandomUserComponent {
RandomUsersApi getRandomUserService();
Picasso getPicasso();
}
Шаг 6. Сборка проекта
Если вы всё сделали верно, то Dagger 2 сгенерирует на основе созданного нами компонента класс, предоставляющий нужные зависимости.
Теперь в MainActivity
можно удобно получить зависимости Picasso
и RandomUsersApi
с помощью RandomUserComponent
.
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. Поздравьте себя!
Вы это сделали! Вы использовали Dagger 2 в Android приложении. Поздравьте себя и возьмите перерыв
(Checkpoint)
...
Но есть проблема
Что? Какая проблема?
Каждый раз, когда вы вызываете DaggerComponent.build()
создаются новые экземпляры всех объектов или зависимостей, которые вы настроили. В этом случае почему Dagger 2 не знает о том, что мне нужен только один экземпляр Picasso
? Другими словами, как мы можем сказать Dagger 2 предоставлять нам зависимость как singleton?
Аннотация @Scope
Аннотация @Scope
говорит Dagger 2 создавать только единственный экземпляр, даже если DaggerComponent.build()
вызывается многократно. Это заставляет зависимость работать как singleton. Для настройки требуемой области (Scope) необходимо создать собственную аннотацию.
@Scope
@Retention(RetentionPolicy.CLASS)
public @interface RandomUserApplicationScope {
}
@Retention
— это аннотация для обозначения точки отклонения использования аннотации. Она говорит о том, когда может быть использована аннотация. Например, с отметкой SOURCE аннотация будет доступна только в исходном коде и будет отброшена во время компиляции, с отметкой CLASS аннотация будет доступна во время компиляции, но не во время работы программы, с отметкой RUNTIME аннотация будет доступна и во время выполнения программы.
Использование областей (Scope)
Для использования созданной области нужно начать с компонента, отметив созданной аннотацией, а затем отметить каждый метод, который нам нужен как singleton.
@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();
}
...
}
Вот как мы создаем единственный экземпляр.
Теперь, ещё одна проблема!
Как правило, в каждом приложении мы используем два вида контекста — ApplicationContext
и контекст Activity
. Как их предоставить? Для предоставления ApplicationContext
можно использовать ContextModule
. Давайте создадим ещё один модуль.
@Module
public class ActivityModule {
private final Context context;
ActivityModule(Activity context){
this.context = context;
}
@RandomUserApplicationScope
@Provides
public Context context(){ return context; }
}
Но созданный класс не решает проблемы. Теперь мы предоставляем две зависимости с типом Context
и Dagger 2 не сможет понять каким воспользоваться, возникнет ошибка.
Аннотация @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
— @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();
}
....
}
Взгляните на соответствующий commit для того, чтобы увидеть как можно заменить аннотацию @Named
на @Qualifier
.
Резюме
На данный момент мы взяли простой проект и внедрили зависимости в нем с помощью Dagger 2 и аннотаций.
Также мы изучили 3 новые аннотации. Первая — @Scope
, для получения зависимостей в единственном экземпляре. Вторая — @Named
, для разделения методов, предоставляющих зависимости одинакового типа. Третья — @Qualifier
, как альтернатива @Named
.
Что дальше?
На данный момент мы рассмотрели только зависимости уровня приложения. В следующей статье посмотрим на зависимости уровня Activity
, создадим несколько компонентов и научимся работать с ними. Следующая статья выйдет через неделю.
Автор: Andrei