Dagger 2.11 & Android

в 6:51, , рубрики: android, dagger 2, dependency injection, Разработка под android

В данной статье рассматривается использование специализированного модуля 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

Автор: Евгений Надеин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js