AndroidInjection

У цьому уроці розберемося, як працює механізм 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 1 і передаємо йому екземпляр Activity. Цей метод знайде необхідний компонент і виконає інджект для Activity.

Я підготував приклад на github, у якому використовував AndroidInjection. Посилання: https://github.com/startandroid/Dagger2_AndroidInjection/tree/androidinjector 8.

У прикладі є три Activity:

  • FirstActivity - Activity із простим презентером
  • SecondActivity - Activity з презентером, що вимагає дані під час створення
  • ThirdActivity - Activity з двома фрагментами TopFragment і BottomFragment. Презентер BottomFragment вимагає дані під час створення.

Тобто я постарався охопити найпоширеніші варіанти, щоб показати, як їх можна реалізувати за допомогою AndroidInjection.

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

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

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

Отже, підемо по порядку.

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

FirstActivityComponent - сабкомпонент для FirstActivity, SecondActivityComponent - для SecondActivity. Сабкомпоненти мають успадковувати AndroidInjector 1, а білдери - 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 app).

Application клас

Інджектимо Application-клас за допомогою AppComponent, який надає нам Map з білдерами. Після інджекту App отримає не Map, а обгортку над ним - DispatchingAndroidInjector. Щоб дагер знав, як йому потім від об'єкта App отримати DispatchingAndroidInjector, необхідно реалізувати інтерфейс HasDispatchingActivityInjector з методом activityInjector. Тобто це просто get метод для DispatchingAndroidInjector.

AndroidInjectionAndroidInjection - це внутрішній клас дагера. Але корисно буде глянути його вихідні коди, щоб зрозуміти, що він робить. Я прибрав різні перевірки і залишив тут тільки робочий код. Спочатку він з 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 1, у яких уже реалізовано виклики 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.

Support

Даггер надає дві версії бібліотеки для роботи з AndroidInjector

'com.google.dagger:dagger-android:2.10'

и

'com.google.dagger:dagger-android-support:2.10'

У support версії ви знайдете:

  • DaggerAppCompatActivity, який розширює android.support.v7.app.AppCompatActivity
  • DaggerFragment, який розширює android.support.v4.app.Fragment

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

На прикладі 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 при кожному виклику буде створювати новий компонент. І я поки не знайшов, як це можна змінити.