Dagger 2.11 & Android. Часть 2

  • Tutorial

В предыдущей статье мы рассмотрели, как мы можем использовать специальный модуль dagger-android для предоставления зависимостей в активити и фрагменты, а также организацию разных скоупов.


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


Модуль dagger-android позволяет заинжектить зависимости в следующие базовые компоненты андроида:


Activity, Fragment, Service, DaggerIntentService, BroadcastReceiver, ContentProvider.


Если мы используем классы из библиотеки поддержки (например AppCompatActivity, android.support.v4.app.Fragment), то нам надо использовать соответствующие классы из дополнительной даггер библиотеки поддержки (dagger-android-support).


AndroidInjector


Служит для инъекций зависимостей в наследников базовых компонентов (Activity, Fragment, и т.д.).


@ContributesAndroidInjector


Данная аннотация должна быть применена над абстрактным методом в модуле, где возвращаемый тип метода — это наследник базового компонента андроид(Activity, Fragment и т.д.). Метод не должен иметь параметров.


Данная аннотация служит для генерации AndroidInjector для возвращаемого типа метода, над которым указана аннотация. Данный AndroidInjector является сабкомпонентом. Этот сабкомпонент является дочерним того компонента (или сабкомпонента), в который данный модуль(в котором присутствует данная аннотация) будет добавлен.


Аннотация содержит параметр modules. Данный параметр указывает на то, какие модули будут добавлены к данному сгенерированному сабкомпоненту.


Над методом с аннотацией @ContributesAndroidInjector также может присутствовать аннотация скоупа. Данный скоуп будет применен к сгенерированному сабкомпоненту.


AndroidInjectionModule / AndroidSupportInjectionModule


Встроенный модуль библиотеки dagger-android. Должен быть добавлен в root компонент.
Содержит в себе мультибайндиг коллекций с фабриками для создания сабкомпонентов, которые были сгенерированы с помощью аннотации @ContributesAndroidInjector. Для каждого базового компонента андроида своя коллекция.


DispatchingAndroidInjector


Является прокси AndroidInector, содержит коллекцию фабрик для создания сабкомпонента (AndroidInjector) для определенного типа. Например DispatchingAndroidInjector<Activity> содержит все фабрики создания сабкомпонентов для наследников Activity. При инжекте ищет нужную фабрику, создает сабкомпонент(AndroidInject) и инжектит зависимости.


AndroidInjection / AndroidSupportInjection


Класс утилита, имеет перегруженный метод inject для всех базовых типов (Activity, Fragment, Service и т.д.). В зависимости от переданного типа, ищет реализацию одного из следующих интерфейсов:


  • HasActivityInjector
  • HasFragmentInjector
  • HasServiceInjector
  • HasContentProviderInjector
  • HasBroadcastReceiverInjector

dagger-android-support


  • HasSupportFragmentInjector

Поиск реализации нужного интерфейса содержащий нужный AndroidInject происходит у объекта у которого время жизни выше.
Интерфейсы HasActivityInjector, HasServiceInjector, HasContentProviderInjector, HasBroadcastReceiverInjector должны быть реализованы в application. Интерфейсы HasFragmentInjector или HasSupportFragmentInjector могут быть реализованы в фрагменте или активити или в application, поиск реализации идет в следующем порядке: родительский фрагмент, активити в котором данный фрагмент находится и application.


AndroidInjection.inject() должен быть вызван в определенном методе, до вызова супер метода:


  • Activity — onCreate
  • Fragment — onAttach
  • Service — onCreate
  • IntentService — onCreate
  • ContentProvider — onCreate
  • BroadcastReceiver — onReceive

Dagger-android имеет классы, которые мы можем использовать (эти классы уже реализуют необходимые интерфейсы и вызов метода AndroidInjection.inject()):


  • DaggerApplication (HasActivityInjector, HasFragmentInjector, HasServiceInjector, HasBroadcastReceiverInjector, HasContentProviderInjector)
  • DaggerActivity (HasFragmentInjector)
  • DaggerFragment (HasFragmentInjector)
  • DaggerBroadcastReceiver
  • DaggerContentProvider
  • DaggerService
  • DaggerIntentService

dagger-android-support


  • DaggerApplication (такие же как и в DaggerApplication + HasSupportFragmentInjector)
  • DaggerAppCompatActivity (HasFragmentInjector, HasSupportFragmentInjector)
  • DaggerFragment (HasSupportFragmentInjector).

Если по каким то причинам мы не можем расширить один из этих классов, то мы всегда можем реализовать необходимый интерфейс.


Примеры использования dagger-классов:


Определим наш основной компонент:
Наш компонент должен наследовать AndroidInjector, т.к. при использовании DaggerApplication нам необходимо будет реализовать один метод, который должен возвращать AndroidInjector. Это только необходимо делать для application, т.к. мы вручную определяем главный компонент.


@Component(modules = {AppModule.class, AndroidSupportInjectionModule.class})
@Singleton
public interface AppComponent extends AndroidInjector<App> {

    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<App> {}
}

Определим основной модуль


В данном модуле мы добавим "маппинг" для нашей активити, сервиса и бродкаст ресивера. Для них будет сгенерированы сабкомпоненты и будут добавлены к основному компоненту, т.к. этот модуль мы подключили к основному компоненту.


@Module
abstract public class AppModule {

    @Provides
    @Singleton
    public static Context context(App app) {
        return app.getApplicationContext();
    }

    @Provides
    @Singleton
    public static UserRepository userRepository(Context context) {
        return new UserRepositoryImpl(context);
    }

    @ContributesAndroidInjector(modules = {
        ActivityModule.class
    })
    @ActivityScope
    abstract MainActivity mainActivity();

    @ContributesAndroidInjector(modules = {
        ServiceModule.class
    })
    @ServiceScope
    abstract MyIntentService myIntentService();

    @ContributesAndroidInjector
    @ReceiverScope
    abstract SomeReceiver connectionReceiver();
}

Определим наш класс Application


public class App extends DaggerApplication {

 @Override
 protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
     return DaggerAppComponent.builder().create(this);
 }
}

Пример с Activity


public class MainActivity extends DaggerAppCompatActivity {
    @Inject
    UserRepository userRepository;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //userRepository уже можем использовать
    }
}

Пример с IntentService


public class MyIntentService extends DaggerIntentService {
    @Inject
    UserRepository userRepository;

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        //userRepository уже можем использовать
    }
}

Пример с Receiver


public class SomeReceiver extends DaggerBroadcastReceiver {
    @Inject
    UserRepository userRepository;

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);

         //userRepository уже можем использовать
    }
}

Component lifecycle


При использовании dagger-android, компоненты и сабкомпоненты живут на протяжении жизни андроид компонентов(Activity, Fragment, Service и т.д.) в которых они были созданы и дестроить вручную их не надо. К примеру активити сабкомпоент создается в момент вызова AndroidInjection.inject() и живет до тех пор пока активити не уничтожена.


Dynamic parameters


C использованием dagger-android нам не нужно билдить сабкомпоненты и запрос зависимостей теперь достигается путем одного вызова AndroidInjection.inject(), а при использовании готовых классов из библиотеки dagger (например DaggerAppCompatActivity), мы можем сразу использовать зависимости. Может возникнуть такой вопрос, как мы можем сделать подобное:


long userId = getArguments().getLong(USER_ID);
getAppComponent()
            .plusUserComponent(new UserModule(userId))
            .inject(this);

userId является динамическим параметром для модуля. Данный параметр к примеру может использоваться для создания UserPresenter(UserRepository userRepository, Long userId).


Варианты реализации:


Вариант 1:
Установка параметров в объект, где он используется.


public class UserPresenter {
    private UserView userView;
    private UserRepository userRepository;
    private long userId;

    @Inject
    public UserPresenter(UserView userView, UserRepository userRepository) {
        this.userView = userView;
        this.userRepository = userRepository;
    }

    public void setUserId(long userId) {
        this.userId = userId;
    }
}

public class UserFragment extends BaseFragment implements UserView {
    @Inject
    UserPresenter userPresenter;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        userId = getArguments().getLong(USER_ID);

        userPresenter.setUserId(userId);
    }
}

Данный вариант не подходит для immutable классов.


Вариант 2:


Перестроить сам класс таким образом, чтобы он не принимал динамические параметры при создании, а сам параметр использовать через методы:


public final class UserPresenter {
    private final UserView userView;
    private final UserRepository userRepository;

    @Inject
    public UserPresenter(final UserView userView, final UserRepository userRepository) {
        this.userView = userView;
        this.userRepository = userRepository;
    }

    public void loadUserInfo(long userId) {
        //
    }

}
public class UserFragment extends BaseFragment implements UserView {

    @Inject
    UserPresenter userPresenter;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        userId = getArguments().getLong(USER_ID);

        userPresenter.loadUserInfo(userId);
    }
}

Вариант 3:


Реализовать создание объекта с динамическими параметрами с помощью фабрики:


public final class UserPresenter {
    private final UserView userView;
    private final UserRepository userRepository;
    private final long userId;

    public UserPresenter(final UserView userView, final UserRepository userRepository, long userId) {
        this.userView = userView;
        this.userRepository = userRepository;
        this.userId = userId;
    }

}

public final class UserPresenterFactory {
    private final UserView userView;
    private final UserRepository userRepository;

    @Inject
    public UserPresenterFactory(UserView userView, UserRepository userRepository) {
        this.userView = userView;
        this.userRepository = userRepository;
    }

    public UserPresenter create(long userId) {
        return new UserPresenter(userView, userRepository, userId);
    }
}

public class UserFragment extends BaseFragment implements UserView {

    @Inject
    UserPresenterFactory userPresenterFactory;

    UserPresenter userPresenter;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        userId = getArguments().getLong(USER_ID);

        userPresenter = userPresenterFactory.create(userId);
    }
}

Написание подобных фабрик может оказаться не удобным или занимать некоторое время, для решение этой проблемы можно посмотреть в сторону AutoFactory.


Есть еще решение от Fernando Cejas, больше подходящее для тех, кто использует RxJava:
Dynamic Parameters in Use Cases


Вывод


Dagger-android позволяет предоставлять зависимости в базовые компоненты андроид (activity, fragment, service и т.д.) более удобным способом, избавляет нас от создания сабкомпонентов и контроля за ними. Активити, фрагменты, сервисы и т.д. выглядят более “чистыми”.


Надеюсь, данная статья помогла вам больше разобраться с возможностями dagger-android.

  • +5
  • 18,8k
  • 6
Поделиться публикацией

Похожие публикации

Комментарии 6

    0
    Большое спасибо за статью!

    Хотел бы кое-что добавить:

    1) AndroidInjectionModule нужен для пустых мап. Пустые мапы могут возникнуть когда мы не вставляем никаких сабкомпонентов Activtiy в компонент Application'a, но при этом инджектим в Application инстанс DispatchingAndroidInjector. Если же у нас есть хотя бы один IntoMap то все будет ok. Цитата из документации «You do not have to use @Multibinds for sets or maps that have at least one @IntoSet, @ElementsIntoSet, or @IntoMap binding, but you do have to declare them if they may be empty.».

    2) «К примеру активити сабкомпонент создается в момент вызова AndroidInjection.inject() и живет до тех пор пока активити не уничтожена.». Казалось бы это не совсем правда? Если заглянуть в код DispatchingAndroidInjector#maybeInject, то видно что там создается инстанс билдера, а сам он никуда не сохраняется. Но если у нас в активити инджектится DispatchingAndroidInjector (для внедрения фрагментов), то ссылка на сабкомпонент активити замкнется через билдер сабкомпонента фрагмента (так как он является inner классом). В итоге получается что сабкомпоненты самых глубоких сущностей умирают сразу после инджекта.

    3) Касательно передачи динамических параметров. 1-й вариант плох тем, что требует контракта, что мы не используем userId, пока не вызван setUserId. 2-й вариант плох тем, что вьюха управляет презентером. 3-й способ наиболее чист, но как верно подмечено, слегка громоздок.
    Добавлю еще 2 способа, которые нашел в issues даггера.
    a) Инстанс активити или фрагмента, добавляется в граф после вызова AndroidInjection.inject(this), и поэтому все необходимые аргументы можно достать оттуда (то есть активити можно передавать как аргумент в Provides методы модуля и доставать оттуда что нужно).
    b) Мы можем добавить BindsInstance методы в билдер, отнаследованный от AndroidInjector.Factory. Далее переопределяем метод seedInstance у билдера, и вызываем все методы BindsInstance, при этом достаем параметры из активити/фрагмента переданного аргументом в seedInstance. Вообще использование BindsInstance более предпочтительно чем конструкторы модуля. Из доков «Binding an instance is equivalent to passing an instance to a module constructor and providing that instance, but is often more efficient. When possible, binding object instances should be preferred to using module instances.»
      0
      Спасибо за отзыв, согласен с вашими комментариями и спасибо за дополнительные варианты)
        0
        «Если заглянуть в код DispatchingAndroidInjector#maybeInject, то видно что там создается инстанс билдера». Создается инстанс сабкомпонента, а не билдера — напутал.
          0
          «Но если у нас в активити инджектится DispatchingAndroidInjector (для внедрения фрагментов), то ссылка на сабкомпонент активити замкнется через билдер сабкомпонента фрагмента (так как он является inner классом).»
          Замкнется через провайдер билдера сабкомпонента фрагмента — напутал.
            0
            Мы можем добавить BindsInstance методы в билдер, отнаследованный от AndroidInjector.Factory. Далее переопределяем метод seedInstance у билдера, и вызываем все методы BindsInstance, при этом достаем параметры из активити/фрагмента переданного аргументом в seedInstance.

            А можно пример кода? пока что тяжело понять… Спасибо!

              0
              Пример
              @Subcomponent
              public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
              
                  @Subcomponent.Builder
                  abstract class Builder extends AndroidInjector.Builder<MainActivity> {
              
                      @Override
                      public void seedInstance(MainActivity instance) {
                          someInteger(instance.getInt());
                          someString(instance.getString());
                      }
              
                      @BindsInstance
                      public abstract Builder someInteger(Integer integer);
              
                      @BindsInstance
                      public abstract Builder someString(String string);
                  }
              
              }
              


              Таким образом в граф добавятся Integer и String из активити и их можно будет инджектить.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое