Как стать автором
Обновить

Dagger 2. Часть третья. Новые грани возможного

Время на прочтение 19 мин
Количество просмотров 63K

Всем привет! Наконец-то подоспела третья часть цикла статей о Dagger 2!


Перед дальнейшим прочтением настоятельно рекомендую ознакомиться с первой и второй частями.


Большое спасибо за отзывы и комментарии. Я очень рад, что мои статьи действительно помогают разработчикам окунуться в мир Даггера. Именно это и придает силы творить для вас дальше.
В третьей части мы с вами рассмотрим различные интересные и немаловажные фичи библиотеки, которые могут вам очень пригодиться.


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


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


Итак, хватит разглагольствовать, и вперед к новым знаниям!


Qualifier annotation


В прошлой статье в комментариях попросили осветить данный вопрос. Не будем откладывать в долгий ящик.


Часто бывает, что нам необходимо провайдить несколько объектов одного типа. Например, мы хотим иметь в системе два Executor: один однопоточный, другой с CachedThreadPool. В этом случае нам приходит на помощь "qualifier annotation". Это кастомная аннотация, которая имеет в себе аннотацию @Qualifier. Звучит немного как масло масляное, но на примере все гораздо проще.


В общем, Dagger2 предоставляет нам уже одну готовую "qualifier annotation", которой, пожалуй, вполне достаточно в повседневной жизни:


@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {

    /** The name. */
    String value() default "";
}

А теперь посмотрим, как это все выглядит в бою:


Qualifier annotation пример
@Module
public class AppModule {

    @Provides
    @Singleton
    @Named("SingleThread")
    public Executor provideSingleThreadExecutor() {
        return Executors.newSingleThreadExecutor();
    }

    @Provides
    @Singleton
    @Named("MultiThread")
    public Executor provideMultiThreadExecutor() {
        return Executors.newCachedThreadPool();
    }

}

public class MainActivity extends AppCompatActivity {

    @Inject
    @Named("SingleThread")
    Executor singleExecutor;

    @Inject
    @Named("MultiThread")
    Executor multiExecutor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication.getInstance().getAppComponent().inject(this);
        setContentView(R.layout.activity_main);
    }
}

В итоге у нас два разных экземпляра (singleExecutor, multiExecutor) одного класса (Executor). То, что нам и нужно! Замечу, что объекты одного класса с аннотацией @Named могут провайдиться также как с абсолютно разных и независимых компонентов, так и c зависимых друг от друга.


Отложенная инициализация


Одна из распространенных наших разработческих проблем — это долгий старт приложения. Обычно причина в одном — мы слишком много всего грузим и инициализируем при старте. Кроме того, Dagger2 строит граф зависимостей в основном потоке. И часто далеко не все конструируемые Даггером объекты нужны сразу же. Поэтому библиотека дает нам возможность отложить инициализацию объекта до первого вызова с помощью интерфейсов Provider<> и Lazy<>.


Сразу же обратим наш взор на пример:


Пример отложенной инициализации
@Module
public class AppModule {

    @Provides
    @Named("SingleThread")
    public Executor provideSingleThreadExecutor() {
        return Executors.newSingleThreadExecutor();
    }

    @Provides
    @Named("MultiThread")
    public Executor provideMultiThreadExecutor() {
        return Executors.newCachedThreadPool();
    }

}

public class MainActivity extends AppCompatActivity {

    @Inject
    @Named("SingleThread")
    Provider<Executor> singleExecutorProvider;

    @Inject
    @Named("MultiThread")
    Lazy<Executor> multiExecutorLazy;

    @Inject
    @Named("MultiThread")
    Lazy<Executor> multiExecutorLazyCopy;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication.getInstance().getAppComponent().inject(this);
        setContentView(R.layout.activity_main);
        //
        Executor singleExecutor = singleExecutorProvider.get();
        Executor singleExecutor2 = singleExecutorProvider.get();
        //
        Executor multiExecutor = multiExecutorLazy.get();
        Executor multiExecutor2 = multiExecutorLazy.get();
        Executor multiExecutor3 = multiExecutorLazyCopy.get();
    }
}

Начнем с Provider<Executor> singleExecutorProvider. До первого вызова singleExecutorProvider.get() Даггер не инициализирует соответствующий Executor. Но при каждом последующем вызове singleExecutorProvider.get() будет создаваться новый экземпляр. Таким образом singleExecutor и singleExecutor2 — это два разных объекта. Такое поведение по сути идентично поведению unscoped объекта.


В каких вообще ситуациях уместен Provider? Он пригождается, когда мы провайдим какую-то мутабельную зависимость, меняющую свое состояние в течении времени, и при каждом обращении нам необходимо получать актуальное состояние. "Что за кривая архитектура?" — скажите вы, и я с вами соглашусь. Но при работе с legacy кодом и не такое увидишь.


Отмечу, что авторы библиотеки тоже не советуют злоупотреблять интерфейсом Provider в тех местах, где достаточно обойтись обычным unscope, так как это чревато "кривой архитектурой", как говорилось выше, и трудно отлавливаемыми багами.


Теперь Lazy<Executor> multiExecutorLazy и Lazy<Executor> multiExecutorLazyCopy. Dagger2 инициализирует соответствующие Executor только при первом вызове multiExecutorLazy.get() и multiExecutorLazyCopy.get(). Далее Даггер кэширует проинициализированные значения для каждого Lazy<> и при втором вызове multiExecutorLazy.get() и multiExecutorLazyCopy.get() выдает закэшированные объекты.


Таким образом multiExecutor и multiExecutor2 ссылаются на один объект, а multiExecutor3 на второй объект.


Но, если мы в AppModule к методу provideMultiThreadExecutor() добавим аннотацию @Singleton, то объект будет кешироваться для всего дерева зависимостей, и multiExecutor, multiExecutor2, multiExecutor3 будут ссылаться на один объект.


Будьте внимательны.


Асинхронная загрузка


Мы подошли с вами к весьма нетривиальной задаче. А что, если мы хотим, чтобы конструирование графа зависимостей проходило в бэкграунде? Звучит многообещающе? Да-да, я про Producers.


Честно скажу, это тема заслуживает вообще отдельного рассмотрения. Там много особенностей и нюансов. По ней достаточно хорошего материала. Сейчас же я коснусь только плюсов и минусов Producers.


Плюсы. Ну самый главный плюс — это загрузка в бэкграунде и возможность управлять этим процессом загрузки.


Минусы. Producers "тащат" за собой Guava, а это плюс 15 тысяч методов к апк. Но самое плохое, что применение Producers немного "портят" общую архитектуру и делают код более запутанным. Если у вас уже был Даггер, а потом вы решили перенести инициализацию объектов в бэкграунд, вам придется хорошенько постараться.


В официальной документации данная темы выделена в специальный раздел. Но я очень рекомендую статьи Miroslaw Stanek. У него вообще очень хороший блог, и там много статей про Dagger2. Собственно, некоторые даже макеты картинок с прошлых статей я заимствовал у него.
Про Producers он пишет в этой статье.


А вот в следующей предлагает очень интересную альтернативу для загрузки дерева зависимостей в бэкграунде. На помощь приходит родная RxJava. Мне очень нравится его решение, так как оно полностью лишено недостатков использования Producers, но при этом решает вопрос асинхронной загрузки.


Один только минус: Мирослав не совсем верно применяет Observable.create(...). Но я об этом написал в комментарии к статье, так что обратите внимание обязательно.


А теперь посмотрим, как будет выглядеть тогда код для scope объекта (с "правильной" RxJava):


Пример со scope
@Module
public class AppModule {

    @Provides
    @Singleton // or custom scope for "local" singletons
    HeavyExternalLibrary provideHeavyExternalLibrary() {
        HeavyExternalLibrary heavyExternalLibrary = new HeavyExternalLibrary();
        heavyExternalLibrary.init(); //This method takes about 500ms
        return heavyExternalLibrary;
    }

    @Provides 
    @Singleton // or custom scope for "local" singletons
    Observable<HeavyExternalLibrary> provideHeavyExternalLibraryObservable(
final Lazy<HeavyExternalLibrary> heavyExternalLibraryLazy) {
        return Observable.fromCallable(heavyExternalLibraryLazy::get)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

}

public class MainActivity extends AppCompatActivity {

    @Inject
    Observable<HeavyExternalLibrary> heavyExternalLibraryObservable;

    //This will be injected asynchronously
    HeavyExternalLibrary heavyExternalLibrary;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication.getInstance().getAppComponent().inject(this);
        setContentView(R.layout.activity_main);
        // init HeavyExternalLibrary in background thread!
        heavyExternalLibraryObservable.subscribe(
                heavyExternalLibrary1 -> heavyExternalLibrary = heavyExternalLibrary1,
                throwable -> {}
        );
    }
}

Обратите внимание на @Singleton и интерфейс Lazy в AppModule. Lazy как раз и гарантирует, что тяжеловесный объект будет проинициализирован, когда мы запросим, а затем закеширован.


А как нам быть, если мы хотим каждый раз получать новый экземпляр этого "тяжелого" объекта? Тогда стоит немного поменять AppModule:


Пример с unscope
@Module
public class AppModule {

    @Provides
    // No scope!
    HeavyExternalLibrary provideHeavyExternalLibrary() {
        HeavyExternalLibrary heavyExternalLibrary = new HeavyExternalLibrary();
        heavyExternalLibrary.init(); //This method takes about 500ms
        return heavyExternalLibrary;
    }

    @Provides
    @Singleton // or custom scope for "local" singletons
    Observable<HeavyExternalLibrary> provideHeavyExternalLibraryObservable(
final Provider<HeavyExternalLibrary> heavyExternalLibraryLazy) {
        return Observable.fromCallable(heavyExternalLibraryLazy::get)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

}

public class MainActivity extends AppCompatActivity {

    @Inject
    Observable<HeavyExternalLibrary> heavyExternalLibraryObservable;

    //This will be injected asynchronously
    HeavyExternalLibrary heavyExternalLibrary;
    HeavyExternalLibrary heavyExternalLibraryCopy;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication.getInstance().getAppComponent().inject(this);
        setContentView(R.layout.activity_main);
        // init HeavyExternalLibrary and heavyExternalLibraryCopy in background thread!
        heavyExternalLibraryObservable.subscribe(
                heavyExternalLibrary1 -> heavyExternalLibrary = heavyExternalLibrary1,
                throwable -> {}
        );
        heavyExternalLibraryObservable.subscribe(
                heavyExternalLibrary1 -> heavyExternalLibraryCopy = heavyExternalLibrary1,
                throwable -> {}
        );
    }
}

Для метода provideHeavyExternalLibrary() мы убрали scope, а в provideHeavyExternalLibraryObservable(final Provider<HeavyExternalLibrary> heavyExternalLibraryLazy) используем Provider вместо Lazy. Таким образом heavyExternalLibrary и heavyExternalLibraryCopy в MainActivity — это разные объекты.


А можно еще вообще весь процесс инициализации дерева зависимостей вынести в бэкграунд. Вы спросите, как? Очень даже легко. Сначала посмотрим на то, как было:


SplashActivity со статьи Мирослава
public class SplashActivity extends BaseActivity {

    @Inject
    SplashActivityPresenter presenter;
    @Inject
    AnalyticsManager analyticsManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setupActivityComponent();
    }

    @Override
    protected void setupActivityComponent() {
        final SplashActivityComponent splashActivityComponent = 
            GithubClientApplication.get(SplashActivity.this)
                .getAppComponent()
                .plus(new SplashActivityModule(SplashActivity.this));
        splashActivityComponent.inject(SplashActivity.this);
    }

}

А теперь взглянем на обновленный метод void setupActivityComponent()(с моими правками по RxJava):


void setupActivityComponent()
@Override
    protected void setupActivityComponent() {
        Completable.fromAction(() -> {
                final SplashActivityComponent splashActivityComponent = 
                    GithubClientApplication.get(SplashActivity.this)
                        .getAppComponent()
                        .plus(new SplashActivityModule(SplashActivity.this));
                splashActivityComponent.inject(SplashActivity.this);
            })
            .doOnCompleted(() -> {
                //Here is the moment when injection is done.
                analyticsManager.logScreenView(getClass().getName());
                presenter.callAnyMethod();
            })
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(() -> {}, throwable -> {});
    }

Замеры


В прошлом разделе мы говорили про производительность при старте приложения. Однако мы знаем, что, если вопрос касается производительности и скорости, мы должны замерять! Полагаться на интуицию и чувство "вроде бы стало быстрее" нельзя. И с этим нам снова поможет Мирослав в этой и этой статьях. Чтобы мы все делали без него, вообще не представляю.


Новые интересные возможности


У Даггера появляются новые интересные фичи, обещающие нам облегчить жизнь. Но вот понять, как все работает и что же нам это все дает, — было задачей не из легких. Ну что же, начнем!


@Reusable scope


Интересная аннотация. Позволяет экономить память, но при этом по сути не ограничена никаким scope, что делает очень удобным переиспользование зависимостей в любых компонентах. То есть это нечто среднее между scope и unscope.


В доках пишут очень важный момент, который как-то не бросается в глаза с первого раза: "Для каждого компонента, который использует @Reusable зависимость, данная зависимость кешируется отдельно". И мое дополнение: "В отличии от scope аннотации, где объект кешируется при создании и его экземпляр используется дочерними и зависимыми компонентами".


А теперь сразу пример, чтобы все понять:


Длинный пример с разъяснениями

Наш главный компонент.


@Component(modules = {AppModule.class, UtilsModule.class})
@Singleton
public interface AppComponent {

    FirstComponent.Builder firstComponentBuilder();
    SecondComponent.Builder secondComponentBuilder();

}

У AppComponent есть два Subcomponent. Обратили внимание на эту конструкцию — FirstComponent.Builder? О ней мы чуть позже.
Теперь посмотрим на UtilsModule.


@Module
public class UtilsModule {

    @Provides
    @NonNull
    @Reusable
    public NumberUtils provideNumberUtils() {
        return new NumberUtils();
    }

    @Provides
    @NonNull
    public StringUtils provideStringUtils() {
        return new StringUtils();
    }

}

NumberUtils с аннотацией @Reusable, а StringUtils оставим unscoped.
Далее у нас два Subcomponents.


@FirstScope
@Subcomponent(modules = FirstModule.class)
public interface FirstComponent {

    @Subcomponent.Builder
    interface Builder {
        FirstComponent.Builder firstModule(FirstModule firstModule);
        FirstComponent build();
    }

    void inject(MainActivity mainActivity);

}

@SecondScope
@Subcomponent(modules = {SecondModule.class})
public interface SecondComponent {

    @Subcomponent.Builder
    interface Builder {
        SecondComponent.Builder secondModule(SecondModule secondModule);
        SecondComponent build();
    }

    void inject(SecondActivity secondActivity);
    void inject(ThirdActivity thirdActivity);

}

Как мы видим, FirstComponent инжектирует только в MainActivity, а SecondComponent — в SecondActivity и ThirdActivity.
Посмотрим код.


public class MainActivity extends AppCompatActivity {

    @Inject
    NumberUtils numberUtils;

    @Inject
    StringUtils stringUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyApplication.getInstance().getFirstComponent()
                .inject(this);
        // other...
    }

}

public class SecondActivity extends AppCompatActivity {

    @Inject
    NumberUtils numberUtils;

    @Inject
    StringUtils stringUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        MyApplication.getInstance().getSecondComponent()
                .inject(this);
        // other...
    }

}

public class ThirdActivity extends AppCompatActivity {

    @Inject
    NumberUtils numberUtils;

    @Inject
    StringUtils stringUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        MyApplication.getInstance().getSecondComponent()
                .inject(this);
        // other...
    }

}

Коротко про навигацию. Из MainActivity мы попадаем в SecondActivity, а затем в ThirdActivity. А теперь вопрос. Когда мы будем уже на третьем экране, сколько объектов NumberUtils и StringUtils будет создано?


Так как StringUtilsunscoped, то будет создано три экземпляра, то есть при каждой инъекции создается новый объект. Это мы знаем.


А вот объектов NumberUtils будет два — один для FirstComponent, а другой для SecondComponent. И здесь я снова приведу основную мысль про @Reusable с документации: "Для каждого компонента, который использует @Reusable зависимость, данная зависимость кешируется отдельно!", в отличии от scope аннотации, где объект кешируется при создании и его экземпляр используется дочерними и зависимыми компонентами.


Но сами гугловцы предупреждают, что если вам необходим уникальный объект, который может быть еще и mutable, то используйте только scoped аннотации.


Еще приведу ссылку на вопрос про сравнение @Singleton и @Reusable со SO.


@Subcomponent.Builder


Фича, которая делает код красивее. Раньше, чтобы создать @Subcomponent нам приходилось писать нечто такое:


Как было
@Component(modules = {AppModule.class, UtilsModule.class})
@Singleton
public interface AppComponent {
    FirstComponent plusFirstComponent(FirstModule firstModule, SpecialModule specialModule);
}

@FirstScope
@Subcomponent(modules = {FirstModule.class, SpecialModule.class})
public interface FirstComponent {
    void inject(MainActivity mainActivity);
}

Создание FirstComponent:


appComponent
    .plusFirstComponent(new FirstModule(), new SpecialModule());

Мне не нравилось в этом подходе то, что родительский компонент был загружен ненужными знаниями о модулях, которые используют дочерние сабкомпоненты. Ну и плюс передача большого количества аргументов выглядит не очень красиво, ведь для этого есть паттерн Builder. Теперь стало красивее:


Как стало
@Component(modules = {AppModule.class, UtilsModule.class})
@Singleton
public interface AppComponent {
    FirstComponent.Builder firstComponentBuilder();
}

@FirstScope
@Subcomponent(modules = {FirstModule.class, SpecialModule.class})
public interface FirstComponent {

    @Subcomponent.Builder
    interface Builder {
        FirstComponent.Builder firstModule(FirstModule firstModule);
        FirstComponent.Builder specialModule(SpecialModule specialModule);
        FirstComponent build();
    }

    void inject(MainActivity mainActivity);

}

Создание FirstComponent теперь выглядит следующим образом:


appComponent
       .firstComponentBuilder()
       .firstModule(new FirstModule())
       .specialModule(new SpecialModule())
       .build();

Другое дело =)


static


Теперь у нас есть возможность делать вот так:


@Provides static User currentUser(AuthManager authManager) {
    return authManager.currentUser();
}

То есть методы, отвечающие за провайдинг зависимостей в модулях, мы можем делать статическими. Я сначала не совсем понимал, а зачем это вообще нужно то? А оказывается, запрос на такую фичу существовал довольно давно, и есть ситуации, когда это выгодно.


На SO задали хороший вопрос на эту тему, мол, а чем собственно отличаются @Singleton от @Provide static. Чтобы хорошо понять эту разницу, нужно читать ответ на вопрос, параллельно экспериментируя и смотря сгенерированный код.


Итак, у нас есть вводная. Мы имеем три варианта одного и того же метода в модуле:


@Provides User currentUser(AuthManager authManager) {
    return authManager.currentUser();
}

@Provides @Singleton User currentUser(AuthManager authManager) {
    return authManager.currentUser();
}

@Provides static User currentUser(AuthManager authManager) {
    return authManager.currentUser();
}

При этом authManager.currentUser() в разные моменты времени может отдавать разные экземпляры.
Логичный вопрос: а чем эти методы отличаются.


В первом случае у нас классический unscope. При каждом запросе будет отдаваться новый экземпляр authManager.currentUser() (точнее новая ссылка на currentUser).


Во втором случае при первом запросе будет закеширована ссылка на currentUser, и при каждом новом запросе будет отдаваться эта ссылка. То есть, если поменялся currentUser в AuthManager, то отдаваться то будет старая ссылка на невалидный уже экземпляр.


Третий случай уже интереснее. Данный метод по поведению аналогичен unscope, то есть при каждом запросе будет отдаваться новая ссылка. Это первое отличие от @Singleton, который кеширует объекты. Таким образом размещать в @Provide static методе инициализацию объекта не совсем уместно.


Но в чем тогда @Provide static отличается от unscope? Допустим у нас есть такой модуль:


@Module
public class AuthModule {
    @Provides
    User currentUser(AuthManager authManager) {
        return authManager.currentUser();
    }
}

AuthManager поставляется из другого модуля в качестве Singleton. Теперь быстро окинем взглядом сгенерированный код AuthModule_CurrentUserFactory (в студии просто поставьте курсор на currentUser и нажмите Ctrl+B):


Unscope
@Generated(
  value = "dagger.internal.codegen.ComponentProcessor",
  comments = "https://google.github.io/dagger"
)
public final class AuthModule_CurrentUserFactory implements Factory<User> {
  private final AuthModule module;

  private final Provider<AuthManager> authManagerProvider;

  public AuthModule_CurrentUserFactory(
      AuthModule module, Provider<AuthManager> authManagerProvider) {
    assert module != null;
    this.module = module;
    assert authManagerProvider != null;
    this.authManagerProvider = authManagerProvider;
  }

  @Override
  public User get() {
    return Preconditions.checkNotNull(
        module.currentUser(authManagerProvider.get()),
        "Cannot return null from a non-@Nullable @Provides method");
  }

  public static Factory<User> create(AuthModule module, Provider<AuthManager> authManagerProvider) {
    return new AuthModule_CurrentUserFactory(module, authManagerProvider);
  }

  /** Proxies {@link AuthModule#currentUser(AuthManager)}. */
  public static User proxyCurrentUser(AuthModule instance, AuthManager authManager) {
    return instance.currentUser(authManager);
  }
}

А если добавить static к currentUser:


@Module
public class AuthModule {
    @Provides
    static User currentUser(AuthManager authManager) {
        return authManager.currentUser();
    }
}

То получим:


static
@Generated(
  value = "dagger.internal.codegen.ComponentProcessor",
  comments = "https://google.github.io/dagger"
)
public final class AuthModule_CurrentUserFactory implements Factory<User> {
  private final Provider<AuthManager> authManagerProvider;

  public AuthModule_CurrentUserFactory(Provider<AuthManager> authManagerProvider) {
    assert authManagerProvider != null;
    this.authManagerProvider = authManagerProvider;
  }

  @Override
  public User get() {
    return Preconditions.checkNotNull(
        AuthModule.currentUser(authManagerProvider.get()),
        "Cannot return null from a non-@Nullable @Provides method");
  }

  public static Factory<User> create(Provider<AuthManager> authManagerProvider) {
    return new AuthModule_CurrentUserFactory(authManagerProvider);
  }

  /** Proxies {@link AuthModule#currentUser(AuthManager)}. */
  public static User proxyCurrentUser(AuthManager authManager) {
    return AuthModule.currentUser(authManager);
  }
}

Обратите внимание, что в варианте со static нет AuthModule. Таким образом, статический метод дергается компонентом напрямую, минуя модуль. А если в модуле только одни статические методы, то экземпляр модуля даже не создается.


Экономия и минус лишние вызовы. Собственно у нас выигрыш по производительности. Также пишут, что вызов статического метода на 15-20% быстрее вызова аналогичного нестатического метода. Если я ошибаюсь, iamironz поправит меня. Уж он то точно знает, а если нужно, и замерит.


@Binds + Inject конструктора


Мегаудобная связка, которая значительно уменьшает boilerplate-code. На заре изучения Даггера я не понимал, зачем нужны инъекции конструктора. Что и откуда берется. А тут еще появился @Binds. Но все на самом деле довольно просто. Спасибо за помощь Владимиру Тагакову и вот этой статье.


Рассмотрим типичную ситуацию. Есть интерфейс Презентера и его реализация:


public interface IFirstPresenter {
    void foo();
}

public class FirstPresenter implements IFirstPresenter {

    public FirstPresenter() {}

    @Override 
    public void foo() {}

}

Мы, как белые люди, провайдим все это дело в модуле и инжектим интерфейс Презентера в активити:


@Module
public class FirstModule {

    @Provides
    @FirstScope
    public IFirstPresenter provideFirstPresenter() {
        return new FirstPresenter();
    }

}

public class MainActivity extends AppCompatActivity {

    @Inject
    IFirstPresenter firstPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyApplication.getInstance().getFirstComponent()
                .inject(this);
        // others
    }

}

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


Модуль будет такой:


@Module
public class FirstModule {

    @Provides
    @FirstScope
    public HelperClass1 provideHelperClass1() {
        return new HelperClass1();
    }

    @Provides
    @FirstScope
    public HelperClass2 provideHelperClass2() {
        return new HelperClass2();
    }

    @Provides
    @FirstScope
    public IFirstPresenter provideFirstPresenter(
                HelperClass1 helperClass1, HelperClass2 helperClass2) {
        return new FirstPresenter(helperClass1, helperClass2);
    }

}

И так вот каждый раз, если нужно добавить какой-то класс и "расшарить" его другим. Модуль "загрязняется" очень быстро. И как-то слишком много кода, не находите? Но есть решение, которое существенно уменьшает код.


Во-первых, если нам необходимо создать зависимость и отдавать готовый класс, а не интерфейс (HelperClass1 и HelperClass2), мы можем прибегнуть к инъекции конструктора. Выглядеть это будет следующим образом:


@FirstScope
public class HelperClass1 {
    @Inject
    public HelperClass1() {
    }
}

@FirstScope
public class HelperClass2{
    @Inject
    public HelperClass2() {
    }
}

Обратите внимание, что к классам была добавлена аннотация @FirstScope, таким образом Даггер понимает, в какое дерево зависимостей отнести данные классы.


Теперь с модуля мы можем смело убирать провайдинг HelperClass1 и HelperClass2:


@Module
public class FirstModule {

    @Provides
    @FirstScope
    public IFirstPresenter provideFirstPresenter(
               HelperClass1 helperClass1, HelperClass2 helperClass2) {
        return new FirstPresenter(helperClass1, helperClass2);
    }

}

Как можно еще уменьшить код в модуле? Вот здесь применим @Binds:


@Module
public abstract class FirstModule {

    @FirstScope
    @Binds
    public abstract IFirstPresenter provideFirstPresenter(FirstPresenter firstPresenter);

}

А в FirstPresenter сделаем инъекцию конструктора:


@FirstScope
public class FirstPresenter implements IFirstPresenter {

    private HelperClass1 helperClass1;
    private HelperClass2 helperClass2;

    @Inject
    public FirstPresenter(HelperClass1 helperClass1, HelperClass2 helperClass2) {
        this.helperClass1 = helperClass1;
        this.helperClass2 = helperClass2;
    }

    @Override 
    public void foo() {}

}

Какие здесь новшества? FirstModule стал у нас абстрактным, как и метод provideFirstPresenter. У provideFirstPresenter убрали аннотацию @Provide, зато добавили @Binds. А в аргументы передаем не необходимые зависимости, а конкретную реализацию!
У FirstPresenter добавилась scope аннотация — @FirstScope, по которой Даггер понимает, куда отнести данный класс. Также к конструктору добавили аннотацию @Inject. Стало намного чище, и добавлять новые зависимости стало еще проще!


Пару ценных дополнений по абстрактным модулям от Mujahit.
Давайте вспомним, что FirstModule относится к FirstComponent, который в свою очередь является сабкомпонентом от AppComponent. И чтобы создать FirstComponent мы делали вот так:


appComponent
       .firstComponentBuilder()
       .firstModule(new FirstModule())
       .specialModule(new SpecialModule())
       .build();

Но как нам создать то экземпляр FirstModule, если он является абстрактным? В прошлых статья я упоминал, что если мы в конструктор модулей ничего не передаем, то есть используем конструкторы по умолчанию, то при создании компонента инициализацию этих модулей можно опустить:


appComponent
       .firstComponentBuilder()
       .build();

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


Также замечу, что если у модуля одни абстрактные методы, то модуль можно реализовать через интерфейс:


@Module
public interface FirstModule {

    @FirstScope
    @Binds
    IFirstPresenter provideFirstPresenter(FirstPresenter firstPresenter);

}

Кроме того в абстрактный модуль мы можем также добавить только статические методы. "Обычные" методы добавить не можем:


@Module
public abstract class FirstModule {

    @FirstScope
    @Binds
    public abstract IFirstPresenter provideFirstPresenter(FirstPresenter firstPresenter);

    @FirstScope
    @Provides
    public HelperClass3 provideHelperClass3() {  // <- Incorrect!
        return new HelperClass3();
    }

    @FirstScope
    @Provides
    public static HelperClass3 provideHelperClass3() {  // <- Ok!
        return new HelperClass3();
    }

}

О чем еще не сказано


Далее я приведу еще список фич с коротким описанием и ссылками на качественное объяснение:


  1. Muitibindings. Позволяет "байндить" объекты в коллекции (Set и Map). Подходит для реализации архитектуры расширения ("plugin architecture"). Крайне рекомендую вот это очень подробное описание с азов. Более интересные примеры применения Muitibindings можно найти в статьях Мирослава тут и тут. И еще в придачу ссылка на официальную документацию. Так что мне даже нечего добавить по данному вопросу.


  2. Releasable references. Если уж с памятью совсем беда. С помощью соответствующих аннотаций мы помечаем объекты, которыми можем пожертвовать при недостатке памяти. Вот такой вот хак.
    В доках (подраздел Releasable references) вполне все понятно описано, как ни странно.


  3. Тестирование. Конечно же, для Unit-тестирования Даггер не нужен. А вот для функциональных, интеграционных и UI тестов может пригодиться возможность подмены определенных модулей. Очень здорово эту тему раскрывает Artem_zin в своей статье и примере. В документации выделен раздел по вопросу тестирования. Но опять-таки гугловцы не могут нормально описать, как именно подменить компонент. Как правильно создать фэйковые модули и подставить их. Для подмены компонента (отдельных модулей) я пользуюсь способом Артема. Да, хотелось бы, чтобы можно было создать отдельным классом тестовый компонент и отдельными классами тестовые модули, и красиво все это подключить в тестовом Application файле. Может кто знает?


  4. @BindsOptionalOf. Работает вместе с Optional от Java 8 или Guava, что делает данную фичу уже труднодоступной для нас. Если интересно, в конце документации можно найти описание.


  5. @BindsInstance. К сожалению, в dagger 2.8 мне данная фича оказалась недоступной. Основной посыл ее в том, что хватит передавать какие-либо объекты через конструктор модуля. Очень распространенный пример, когда через конструктор AppComponent передается глобальный Context. Так вот с этой аннотацией такого делать станет не нужно. В конце документации есть пример.

Ну вот и все! Вроде все моменты удалось осветить. Если что-то пропустил или недостаточно описал, пишите! Исправим. Также рекомендую группу по Dagger2 в Телеграме, где ваши вопросы не останутся без ответов.


Кроме того, правильное применение библиотеки очень связано с чистой архитектурой. Поэтому вот вам и группа по архитектуре. И да, скоро на AndroidDevPodcast планируется выпуск, посвященный Даггеру. Следите за новостями!

Теги:
Хабы:
+16
Комментарии 30
Комментарии Комментарии 30

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн