Dagger 2 для начинающих Android разработчиков. Dagger 2. Продвинутый уровень. Часть 1

в 9:49, , рубрики: android development, dagger 2, dependency injection, java, перевод, Разработка под android

Данная статья является шестой частью серии статей, предназначенных, по словам автора, для тех, кто не может разобраться с внедрением зависимостей и фреймворком Dagger 2, либо только собирается это сделать. Оригинал написан 23 декабря 2017 года. Перевод вольный.

Dagger 2 advanced part 1 image

Это шестая статья цикла «Dagger 2 для начинающих Android разработчиков.». Если вы не читали предыдущие, то вам сюда.

Серия статей

Ранее в цикле статей

Мы проанализировали генерируемый Dagger 2 класс и посмотрели на то, как Dagger 2 использует шаблон Builder для предоставления необходимых зависимостей.
После разобрали простой пример использования аннотаций @Module и @Provides.

Предисловие

Эта статья может показаться вам немного большой. Обычно мои статьи не превышают 800 символов. Я хотел разбить её на более мелкие части, но причина, по которой статья настолько большая, заключается в том, что если при решении проблемы сильных связей (hard dependencies) надолго остановиться посередине, то возникает шанс потеряться.

Но я включил в статью контрольные точки (Сheckpoint). В этих местах вы можете взять небольшой перерыв и отвлечься. Думаю, это будет полезным для новичков в Dagger 2 и внедрении зависимостей (DI).

Дом Android

House Android image

До сих пор мы рассматривали обычные 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.javaAdapter для RecyclerView

Зависимости

Для реализации функций проекта будут задействованы следующие библиотеки:

  • Retrofit — для вызовов API
  • GsonBuilder и Gson — для работы с JSON
  • HttpLoggingInterceptor  — для логирования сетевых операций
  • 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)
...

Граф зависимостей

Граф зависимостей — это не что иное, как диаграмма, объясняющая зависимости между классами. Формирование такого графа делает реализацию более понятной (вы убедитесь в этом ближе к концу). Посмотрите на граф зависимостей для нашего проекта.
DI graph image
Зеленым отмечены верхнеуровневые зависимости, это означает, что они не нужны никаким другим зависимостям, но им нужны некоторые из зависимостей.

Как читать эту диаграмму? Например, у 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 в другие модули? Нам нужно связать модули, которые зависят друг от друга.
modules and  component image
Для реализации связи между модулями требуется атрибут 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 { ... }

Таким образом мы соединили все модули.
linjed modules image

Шаг 5. Связывание Component и модулей

На данный момент все модули соединены и общаются друг с другом. Сейчас время сказать Component или обучить его обращаться к модулям, которые предоставят ему требуемые зависимости.

Как мы связывали модули между собой с помощью атрибута includes, подобным образом мы можем связать компонент и модули с помощью атрибута modules.

Учитывая потребности созданного компонента (методы getRandomUserService() и getPicasso()) включим в компонент ссылки на модули RandomUsersModule и PicassoModule, используя атрибут modules.

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

component and modules are connectes image

Шаг 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)
...

GIF

image

Но есть проблема

Что? Какая проблема?

Каждый раз, когда вы вызываете 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();
    }
    ...
}

Вот как мы создаем единственный экземпляр.

Теперь, ещё одна проблема!

GIF

image

Как правило, в каждом приложении мы используем два вида контекста — 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

Источник

Поделиться

* - обязательные к заполнению поля