[startandroid] Dagger 2 - Урок 6

Урок 6. AndroidInjection

06 апреля 2017

В этом уроке разберемся как работает механизм AndroidInjection, который позволяет упростить inject для Activity и Fragment. Рассмотрим классы DaggerActivity и DaggerFragment, при использовании которых, в вашем коде вообще не будет строки с вызовом метода inject.

На прошлом уроке мы говорили о билдерах и о возможности их использования для создания сабкомпонентов. На примере приложения рассмотрели универсальную схему, в которую достаточно удобно добавлять новые сабкомпоненты.

AndroidInjection действует по похожей схеме, но более соответствует паттерну Dependency Injection в том плане, что Activity должно как можно меньше знать о том, как оно инджектится.

AndroidInjection доступен с версии 2.10. Чтобы его использовать, необходимо добавить в build.gradle зависимости:

compile 'com.google.dagger:dagger-android:2.10'
apt 'com.google.dagger:dagger-android-processor:2.10'

При использовании AndroidInjection инджект Activity выглядит так:

public class FirstActivity extends AppCompatActivity {
 
    @Inject
    FirstActivityPresenter presenter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.first_activity);
        presenter.doSomething();
    }
}

Вызываем статический метод AndroidInjection.inject и передаем ему экземпляр Activity. Этот метод найдет необходимый компонент и выполнит инджект для Activity.

Я подготовил пример на github, в котором использовал AndroidInjection.
Ссылка: https://github.com/startandroid/Dagger2_AndroidInjection/tree/androidinjector.

В примере есть три Activity:
FirstActivity - Activity с простым презентером
SecondActivity - Activity c презентером, требующим данные при создании
ThirdActivity - Activity с двумя фрагментами TopFragment и BottomFragment. Презентер BottomFragment требует данные при создании.

Т.е. я постарался охватить наиболее распространенные варианты, чтобы показать, как их можно реализовать c помощью AndroidInjection.

Для лучшего понимания работы AndroidInjection я нарисовал схему с двумя Activity (нажмите, чтобы открыть полноразмерное изображение).

На ней отображен путь, который проходит компонент, чтобы попасть в Activity и выполнить inject. Каждый следующий шаг использует элемент(ы) из предыдущего шага. Цветными рамками я выделил эти используемые элементы для наглядности. Т.е. если на каком-то шаге название класса или метода выделено цветной рамкой, то на следующем шаге ищите рамку того же цвета, чтобы увидеть, где был использован этот класс или метод.

Код на картинках немного сокращен и упрощен для уменьшения размера картинок.

Итак, пойдем по порядку.

Сабкомпоненты для Activity

FirstActivityComponent - сабкомпонент для FirstActivity, SecondActivityComponent - для SecondActivity. Сабкомпоненты должны наследовать AndroidInjector, а билдеры - AndroidInjector.Builder.

Синими рамками выделены сабкомпонент и билдер для FirstActivity, а зелеными - для SecondActivity. И на следующей картинке они выделены рамками тех же цветов, чтобы нагляднее было видно, где именно они используются.

Эти сабкомпоненты необходимо прописать в модуле, в аргументе subcomponents. Так же мы делали и в прошлом уроке.

Модуль, который будет знать про билдеры сабкомпонентов

Сабкомпоненты указываем в subcomponents, а для билдеров используем аннотацию @IntoMap, чтобы собрать их в Map. Ключом в этом Map будет класс Activity.

Т.е. модуль AppScBuilderModule теперь умеет собирать Map, который по классу Activity сможет вернуть билдер для создания сабкомпонента, соответствующего этому Activity.

Основной компонент приложения - AppComponent

Модуль AppScBuilderModule используем в основном компоненте - AppComponent. Т.е. компонент AppComponent теперь сможет предоставить Map, который по классу Activity сможет вернуть билдер для создания сабкомпонента, соответствующего этому Activity.

Нам необходимо будет предоставлять этот Map в Application-класс нашего приложения, поэтому прописываем в этом компоненте метод injectApp(App app).

Application класс

Инджектим Application-класс с помощью AppComponent, который предоставляет нам Map с билдерами. После инджекта App получит не Map, а обертку над ним - DispatchingAndroidInjector.
Чтобы даггер знал, как ему потом от объекта App получить DispatchingAndroidInjector, необходимо реализовать интерфейс HasDispatchingActivityInjector с методом activityInjector. Т.е. это просто get метод для DispatchingAndroidInjector.

AndroidInjection

AndroidInjection - это внутренний класс даггера. Но полезно будет глянуть его исходники, чтобы понять, что он делает. Я убрал различные проверки и оставил здесь только рабочий код.
Сначала он из Activity получает Application. Затем вызывает его метод activityInjector, чтобы получить DispatchingAndroidInjector. И у DispatchingAndroidInjector вызывает метод inject и передает ему Activity.

А DispatchingAndroidInjector при вызове метода inject отыщет (по классу Activity) нужный билдер в Map, создаст сабкомпонент и заинджектит переданный экземпляр Activity.

Activity

В Activity нам остается только вызвать метод AndroidInjection.inject и передать ему экземпляр Activity

Вкратце всю эту схему можно описать так:

  • создаем для Activity сабкомпоненты с билдерами
  • собираем билдеры в Map и помещаем в Application класс
  • в Activity вызываем AndroidInjection.inject, который идет в Application, достает нужный билдер, создает сабкомпонент и инджектит Activity.

Здесь важно понять, что AndroidInjection.inject ищет DispatchingAndroidInjector в Application классе. Т.е. вы должны каким-либо компонентом заинджектить DispatchingAndroidInjector в Application класс.

Фрагменты

Существует вариант метода AndroidInjection.inject и для фрагментов. Т.е. в фрагменте вы можете вызвать AndroidInjection.inject и передать ему инстанцию фрагмента. Но в отличие от Activity, в случае с фрагментом AndroidInjection.inject будет искать DispatchingAndroidInjector сначала в родительском фрагменте, затем в родительском Activity, затем в Application-классе. Где найдет, тот и использует для получения билдера и создания сабкомпонента.

Вы можете посмотреть, как это реализовано на примере этого урока на гитхабе. Там есть ThirdActivity с двумя фрагментами. И DispatchingAndroidInjector для фрагментов я поместил в их родительское Activity (ThirdActivity), а не в Application.

ThirdActivity.java:

public class ThirdActivity extends AppCompatActivity implements HasDispatchingFragmentInjector {
 
    @Inject DispatchingAndroidInjector<Fragment> fragmentInjector;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.third_activity);
 
        if (savedInstanceState == null) {
            // create fragments
        }
    }
 
    @Override
    public DispatchingAndroidInjector<Fragment> fragmentInjector() {
        return fragmentInjector;
    }
}

Activity должно получить от своего компонента DispatchingAndroidInjector. В нем будет Map с билдерами сабкомпонентов для фрагментов.

Activity реализует интерфейс HasDispatchingFragmentInjector, чтобы фрагмент знал, как ему добраться до DispatchingAndroidInjector.

Соответственно, компонент Activity должен уметь собирать билдеры фрагментов в DispatchingAndroidInjector.

ThirdActivityComponent.java:

@Subcomponent(modules = ThirdActivitySubcomponentBuildersModule.class)
public interface ThirdActivityComponent extends AndroidInjector<ThirdActivity> {
 
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<ThirdActivity> {
 
    }
 
}

Компонент использует модуль ThirdActivitySubcomponentBuildersModule:

@Module(subcomponents = {TopFragmentComponent.class, BottomFragmentComponent.class})
public abstract class ThirdActivitySubcomponentBuildersModule {
 
    @Binds
    @IntoMap
    @FragmentKey(TopFragment.class)
    abstract AndroidInjector.Factory<? extends Fragment>
    bindTopFragmentInjectorFactory(TopFragmentComponent.Builder builder);
 
    @Binds
    @IntoMap
    @FragmentKey(BottomFragment.class)
    abstract AndroidInjector.Factory<? extends Fragment>
    bindBottomFragmentInjectorFactory(BottomFragmentComponent.Builder builder);
 
}

А в модуле уже идет сборка билдеров фрагментов в Map.

DaggerActivity, DaggerFragment

Даггер предоставляет классы DaggerActivity и DaggerFragment, в которых уже реализованы вызовы AndroidInjection.inject и интерфейс HasDispatchingFragmentInjector.

ThirdActivity теперь выглядит так:

public class ThirdActivity extends DaggerActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.third_activity);
 
        if (savedInstanceState == null) {
        //create fragments
        }
    }
 
}

Сравните с кодом ThirdActivity, который приведен чуть ранее. Стало читабельнее, проще и привычнее.

В отдельной ветке вы можете найти этот же пример, переделанный на использование DaggerActivity и DaggerFragment.

Осталось еще несколько моментов, на которые я хотел бы обратить внимание.

Когда вызывать AndroidInjection.inject(this)

В случае с Activity рекомендуется это делать в onCreate перед вызовом метода super.onCreate.

В случае с Fragment рекомендуется это делать в onAttach перед вызовом метода super.onAttach.

Service, IntentService, BroadcastReceiver

Вы может использовать AndroidInjection.inject не только для Activity и Fragment, но и для Service, IntentService, BroadcastReceiver.

Также даггер предоставляет соответствующие классы: DaggerService, DaggerIntentService, DaggerBroadcastReceiver

Официальный хелп утверждает, что существует еще и DaggerApplication, но я такой класс не нашел.

Support

Даггер предоставляет две версии библиотеки для работы с AndroidInjector

‘com.google.dagger:dagger-android:2.10’
и
‘com.google.dagger:dagger-android-support:2.10’

В support версии вы найдете:

Передача данных в модуль

На примере SecondActivity вы можете посмотреть, как можно передать данные в презентер при его создании

SecondActivityPresenter при создании требует на вход String

public class SecondActivityPresenter {
 
    private final String data;
 
    public SecondActivityPresenter(String data) {
        this.data = data;
    }
 
    public String getData() {
        return data;
    }
}

Этот String находится в Intent, с которым было вызвано SecondActivity. Как передать String из Intent в презентер?

Когда мы в Activity вызываем AndroidInjection.inject, мы передаем экземпляр Activity. Этот экземпляр используется в двух целях. Во-первых, для того, чтобы определить класс, по которому будет найден билдер в Map. Во-вторых, AndroidInjection поместит этот экземпляр Activity в созданный сабкомпонент, т.е. в SecondActivityComponent.

А если объект доступен для компонента, то он доступен и для его модулей. В нашем случае - для SecondActivityModule.

@Module
public class SecondActivityModule {
 
    @Provides
    SecondActivityPresenter provideSecondActivityPresenter(SecondActivity secondActivity) {
        String data = secondActivity.getIntent().getStringExtra(Constants.EXTRA_DATA);
        return new SecondActivityPresenter(data);
    }
}

только достать Intent и строку из него, и передать в конструктор презентера.

Минус

Самый большой минус в этом решении - непонятно как управлять временем жизни компонента. В прошлом уроке мы рассматривали пример, когда мы сами создавали компонент при создании Activity, получали его же при повороте экрана и обнуляли при закрытии Activity. Это позволяло достаточно гибко обрабатывать повороты экрана.

А AndroidInjection при каждом вызове будет создавать новый компонент. И я пока не нашел, как это можно изменить.