В данной статье рассматривается использование специализированного модуля 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 + @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.
Пример на GitHub
Автор: Евгений Надеин