В данной статье рассматривается использование специализированного модуля Dagger 2 под android и подразумевается, что у вас есть наличие базовых знаний по Dagger 2.
В Dagger 2.10 был представлен новый модуль специально для Android. Данный модуль поставляется как дополнение, состоящий из дополнительной библиотеки и компилятора.
В версии 2.11 были некоторые небольшие изменения, в частности некоторые классы, были переименованы, поэтому будет использоваться именно эта версия.
Базовая теория
Рассмотрим некоторые особенности Dagger 2, которые будут применяться в примерах.
static @Provides методы
У нас появилась возможность писать статические @Provides методы:
@Module public class RepositoryModule { @Provides public static NewsRepository newsRepository(SQLiteDatabase db) { return new NewsRepositoryImpl(db); } }
Основное отличие статического @Provides метода от обычного в том, что он дергается компонентом напрямую, а не через инстанс модуля. Статические @Provides методы можно использовать как в абстрактном, так и в обычном классе модуля. Статические методы могут быть scope и unscope.
@Binds
Dagger 2 позволяет нам предоставлять зависимости без наличия @Provides методов. Это достигается путем наличия @Inject над конструктором у класса, который нам необходимо создать.
Рассмотрим пример:
public class NewsRepositoryImpl implements NewsRepository { private SQLiteDatabase database; @Inject public NewsRepositoryImpl(SQLiteDatabase database) { this.database = database; } //.. } ... public class MyActivty extends BaseActivity { @Inject NewsRepositoryImpl newsRepo; @Override protected void onCreate(Bundle savedInstanceState) { getAppComponent.inject(this); } }
При таком подходе мы можем писать в качестве типа конкретный класс, мы не можем запросить зависимость по интерфейсу NewsRepository. Dagger 2 не сможет найти нужную реализацию для данного интерфейса.
Для того чтобы обойти это ограничение, нам необходим @Binds, для того чтобы забайндить(привязать) реализацию к интерфейсу.
Особенности @Binds:
- Применятся над абстрактным методом или над методом интерфейса модуля.
- Возвращаемый тип метода — это интерфейс, на который мы байндим реализацию.
- В качестве параметра метода указывается не зависимости, а тип конкретной реализации.
- Можно применять к методу
@Qualifier/@Named. - Можно указывать scope.
Пример:
@Module public abstract class RepositoryModule { @Binds @Singleton public abstract NewsRepository newsRepository(NewsRepositoryImpl repo); }
Теперь мы можем смело писать следующее:
public class MyActivty extends BaseActivity { @Inject NewsRepository newsRepo; ...
Модули представленные в виде абстрактных классов имеют следующие особенности:
- Могут содержать абстрактные методы с аннотацией
@Binds - Могут содержать только static
@Provideметоды. Не статические провайд методы не поддерживаются. - Могут содержать абстрактные методы с аннотацией
@Multibinds
При использовании @Binds + @Inject над конструктором у нас нет необходимости писать и реализовать полностью @Provides методы.
Если в модуле методы только для байндинга (@Binds), то имеет смысл сделать этот модуль в виде интерфейса:
@Module public interface AppModule { @Binds @Singleton NewsRepository newsRepository(NewsRepositoryImpl newsRepository); }
Dagger-Android
Типичное android приложение использующая Dagger 2 выглядит примерно так:
public class SomeActivity { @Inject Api api; @Override protected void onCreate(Bundle savedInstanceState) { DaggerAppComponent .builder() .app(getApplication()) .build() .inject(this); } }
Также могут быть получение саб компонентов для разных скоупов (например Activity scope, Fragment scope).
Отсюда вытекают такие проблемы:
- Копипаст подобного блока. Даже если вынесли данный код в базовый класс, мы все равно вынуждены будем вызывать метод inject’a.
- Компонент/Subcomponent должен иметь метод для каждого класса, где он должен инжектить.
- Если у нас многоуровневая структура скоупов, то нам надо “пробрасывать” саб компоненты по уровням.
Эту проблему решает новый модуль для android.
Подключение зависимостей
//Dagger 2 compile 'com.google.dagger:dagger:2.11' annotationProcessor 'com.google.dagger:dagger-compiler:2.11' //Dagger-Android compile 'com.google.dagger:dagger-android:2.11' annotationProcessor 'com.google.dagger:dagger-android-processor:2.11' //Если мы собираемся использовать support библиотеку compile 'com.google.dagger:dagger-android-support:2.11'
Нельзя просто подключить зависимости только относящиеся к android. Они идут как дополнение.
Реализация
Как известно аннотацией @Scope и её наследниками помечаются методы в модулях, а также компоненты/сабкомпоенты, которые предоставляют необходимые нам зависимости.
@Scope определяет время жизни создаваемых(представляемых) объектов, тем самым представляют эффективное управление памятью.
Рассмотрим пример структуры приложения по скоупам:
@Singletone— уровень приложения (Application), корневой скоуп, живущий дольше всех. (Составляющие: Context, Database, Repository). То что нам может понадобится чаще всего.@ActivityScope— уровень жизни на протяжении жизни активити. Могут жить не долго из за частого перехода с одного экрана на другой. (Составляющие: Router, Facade). Имеет смысл подчистить все, что не используется на конкретной активити.@FragmentScope— уровень жизнь на протяжении жизни фрагмента. Живет меньше всех, смена фрагментов может быть организована внутри одной активити. Та же не имеет смысла хранить то, что уже не используется на конкретном фрагменте и необходимо подчистить. (Составляющие: Presenter)
В данном примере под Facade подразумевается аналог UseCase/Interactor. Приложение имеет структуру состоящую из 3х скопов для демонстрации как это можно применить с помощью нового модуля Dagger 2. Также здесь рассмотрен вариант с использованием исключительно с аннотацией @ContributesAndroidInjector.
Приступаем к реализации:
1. Определим наш главный модуль.
@Module(includes = {AndroidSupportInjectionModule.class}) public interface AppModule { @Singleton @Binds Repository repository(RepositoryImpl repository); @ActivityScope @ContributesAndroidInjector(modules = {MainActivityModule.class}) MainActivity mainActivityInjector(); }
В данный модуль было добавлено следующее:
AndroidSupportInjectionModule— это встроенный модуль dagger-android, который согласно документации, должен быть обязательно добавлен в root компонент, для обеспечения доступности всехAndroidInjector. Он необходим для того чтобы заинжектить DispatchingAndroidInjector(см. ниже).@ContributesAndroidInjector— Данная аннотация сгенерируетAndroidInjectorдля возвращаемого типа, таким образом даггер сможет инжектить зависимости в данную активити. Так же будет сгенерирован сабкомпонент дляMainActivityсо скоупом@ActvitiyScope.AndroidInjection— это по сути сабкомпонент для конкретной активити. Также мы можем указать какие модули будут относится только к этому конкретному активити. ДанныйAndroidInjectorбудет иметь все зависимости этого модуля(AppModule) плюс зависимости которые указаны в модулях аннотацииContributesAndroidInjector.
@ContributesAndroidInjector— применяется над абстрактными методами или над методами интерфейса.
2. MainActivityModule
@Module public interface MainActivityModule { @ActivityScope @Binds Facade facade(FacadeImpl facade); @ActivityScope @Binds MainRouter router(MainRouterImpl mainRouter); }
При использовании AndroidInector, нам будет доступен инстанс активити, т.к. данная активити является частью графа.Происходит это потому, что мы вызываем AndroidInjection.inject(this), тем самым передаем инстанс активити (см. ниже).
Пример получение инстанса активити в качестве зависимости.
public class MainRouterImpl extends BaseRouterImpl<MainActivity> implements MainRouter { @Inject public MainRouterImpl(MainActivity activity) { super(activity); } @Override public void showSomeScreen() { replaceFragment(R.id.content, new MyFragment()); } }
3. Напишем наш root компонент, который будет содержать наш AppModule, а также единственный инжект в Application.
@Singleton @Component(modules = { AppModule.class }) public interface AppComponent { @Component.Builder interface Builder { @BindsInstance Builder context(Context context); AppComponent build(); } void inject(App app); }
4. Необходимо реализовать интерфейс HasActivityInjector в Application и заинжектить диспечер AndroidInector’ов.
public class App extends Application implements HasActivityInjector { @Inject DispatchingAndroidInjector<Activity> dispatchingAndroidInjector; @Override public void onCreate() { super.onCreate(); DaggerAppComponent .builder() .context(this) .build() .inject(this); } @Override public AndroidInjector<Activity> activityInjector() { return dispatchingAndroidInjector; } }
DispatchingAndroidInjector нужен для поиска AndroidInector для конкретного Activity.
5. Теперь мы можем всем этим воспользоватся
public class MainActivity extends Activity { @Inject Repository repository; //Singleton scope @Inject Facade facade; //activity scope @Inject MainRouter router; //activity scope public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); // Должен быть первым вызовом super.onCreate(savedInstanceState); } }
Как мы видим здесь нет никаких компонентов и сабкомпонентов, но при этом у нас есть возможность получение зависимостей с разными скоупами. К тому же у нас есть роутер, для создания которого необходим(в качестве зависимости) сам инстанс активити.
Конструкцию AndroidInjection.inject(this) можно вынести в базовый класс и так же все будет работать.
Как это работает? При вызове AndroidInjection.inject(this), Dagger 2 получается доступ к Application который реализует интерфейс HasActivityInjector, где через диспетчер находит нужный AndroidInector (сабкомпонент активити) по классу активити и затем производит инициализацию зависимостей с нужным скоупом.
6. Перейдем к реализации @FragemntScope.
Нам необходимо обновить наш MainActivityModule:
@Module public interface MainActivityModule { @ActivityScope @Binds Facade facade(FacadeImpl facade); @ActivityScope @Binds MainRouter router(MainRouterImpl mainRouter); @FragmentScope @ContributesAndroidInjector(modules = {MyFragmentModule.class}) MyFragment myFragment(); }
Мы добавили аналогичную конструкцию AndroidInject для фрагмента, как мы делали для активити.
Таким образом для нас будет сгенерирован AndroidInject (сабкомпонент) для конкретного фрагмента с скоупом @FragmentScope. Нам будут доступны зависимости @Singleton, @ActivityScope которые указаны в данном модуле и те что указаны в качестве модулей для данного фрагмента.
7. Добавим базовую активити и реализацию интерфейса HasSupportFragmentInjector.
abstract public class BaseActivity extends AppCompatActivity implements HasSupportFragmentInjector { @Inject DispatchingAndroidInjector<Fragment> fragmentInjector; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); } @Override public AndroidInjector<Fragment> supportFragmentInjector() { return fragmentInjector; } }
По аналогии с Application, активити у нас будет выступать диспетчером AndroidInjector, который будет нам предоставлять нужный нам для фрагмента AndroidInjector (сабкомпонент).
8. MyFragmentModule
@Module public interface MyFragmentModule { @Binds MyView myView(MyFragment myFragment); }
Точно так же, как с активити, мы используем AndroidInjection (AndroidSupportInjection, если используем библиотеку поддержки), нам доступен инстанс фрагмента, т.к. он является частью графа, мы можем передавать ее в качестве зависимости, а также забайндить на какой нибудь интерфейс.
Пример презентера:
public class MyPresenter { private MyView view; //будет наш фрагмент, т.к. Мы на него забайндили private Facade facade; //@ActivityScope private MainRouter router; //@ActivityScope @Inject public MyPresenter(MyView view, Facade facade, MainRouter router) { this.view = view; this.facade = facade; this.router = router; } //... }
9. Инджектим в фрагмент
public class MyFragment extends Fragment implements MyView { @Inject MyPresenter presenter; //@FragmentScope @Override public void onAttach(Context context) { AndroidSupportInjection.inject(this); super.onAttach(context); presenter.doSomething(); } @Override public void onResult(String result) { //Todo } }
Конструкцию AndroidSupportInjection можно вынести в базовый класс.
Вывод
По моему мнению, новый модуль android-dagger предоставляет более правильное предоставление зависимостей для android. Мы можем вынести в базовые классы методы инъекции, получили бол��е удобное разделение по скоупам, нам не надо пробрасывать сабкомпоненты и у нас стали доступны в графе зависимости объекты активити и фрагмента, которые мы можем использовать в качестве внешней зависимости, например в presenter'e.
