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