Статья предполагает, что читатель уже знаком с Dagger 2 и понимает что такое компонент, модуль, инжектирование и граф объектов и как все это вместе работает. Здесь же мы, в первую очередь, сконцентрируемся на создании ActivityScope и на том, как его увязать с фрагментами.
Итак, поехали… Что же такое scope?
![](https://habrastorage.org/files/2af/a00/4fd/2afa004fd331431186e6ed8348d370b8.jpg)
Скоуп — это механизм Dagger 2, позволяющий сохранять некоторое множество объектов, которое имеет свой жизненный цикл. Иными словами скоуп — это граф объектов имеющий свое время жизни, которое зависит от разработчика.
По умолчанию Dagger 2 «из коробки» предоставляет нам поддержку javax.inject.Singleton скоупа. Как правило, объекты в этом скоупе существуют ровно столько, сколько существует инстанс нашего приложения.
Кроме того, мы не ограничены в возможности создания своих дополнительных скоупов. Хорошим примером кастомного скоупа может послужить UserScope, объекты которого существуют до тех пор, пока пользователь авторизован в приложении. Как только сессия пользователя заканчивается, или пользователь явно выходит из приложения, граф объектов уничтожается и пересоздается при следующей авторизации. В таком скоупе удобно хранить объекты, связанные с конкретным пользователем и не имеющие смысла для других юзеров. Например, какой-нибудь AccountManager, позволяющий просматривать списки счетов конкретного пользователя.
![](https://habrastorage.org/files/f25/485/cfb/f25485cfbe504ec5948016beb94f5010.png)
На рисунке показан пример жизненного цикла Singleton и UserScope в приложении.
- При запуске создается Singleton скоуп, время жизни которого равняется времени жизни приложения. Иными словами, объекты принадлежащие Singleton скоупу будут существовать до тех пор, пока система не уничтожит и не выгрузит из памяти наше приложение.
- После запуска приложения, User1 авторизуется в приложении. В этот момент создается UserScope, содержащий объекты, имеющие смысл для данного пользователя.
- Через некоторое время пользователь решает «выйти» и разлогинивается из приложения.
- Теперь User2 авторизуется и этим инициирует создание объектов UserScope для второго пользователя.
- Когда сессия пользователя истекает, это приводит к уничтожению графа объектов.
- Пользователь User1 возвращается в приложение, авторизуется, тем самым создает граф объектов UserScope и отправляет приложение в бэкграунд.
- Спустя некоторое время система в ситуации нехватки ресурсов принимает решение об остановке и выгрузке из памяти нашего приложения. Это приводит к уничтожению как UserScope, так и SingletonScope.
Надеюсь, со скоупами немного разобрались.
Перейдем теперь к нашему примеру — ActivityScope. В реальных Android приложениях ActivityScope может оказаться крайне полезным. Еще бы! Достаточно представить себе какой-нибудь сложный экран, состоящий из кучи классов: пяток различных фрагментов, куча адаптеров, хелперов и презентеров. Было бы идеально в таком случае “шарить” между ними модель и/или классы бизнес логики, которые должны быть общими.
![](https://habrastorage.org/files/ca6/d6f/587/ca6d6f5874ac4058b1233c49bf61cfc2.png)
- Использовать для передачи ссылок на общие объекты самодельные синглтоны, Application класс или статические переменные. Данный подход мне однозначно не нравится, потому что нарушает принципы ООП и SOLID, делает код запутанным, трудночитаемым и неподдерживаемым.
- Самостоятельно передавать объекты из Активности в нужные классы посредством сеттеров или конструкторов. Минус данного подхода — затраты на написание рутинного кода, когда вместо этого можно было бы сфокусироваться на написании новых фич.
- Использовать Dagger 2 для инжектирования разделяемых объектов в необходимые места нашего приложения. В этом случае мы получаем все преимущества второго подхода, при этом не тратим время на написание шаблонного кода. По сути, перекладываем написание связующего кода на библиотеку.
Давайте посмотрим по шагам как с помощью Dagger 2 создать и использовать ActivityScope.
Итак, для создания кастомного скоупа необходимо:
- Объявить скоуп (создать аннотацию)
- Объявить хотя бы один компонент и соответствующий модуль для скоупа
- В нужный момент инстанцировать граф объектов и удалить его после использования
Интерфейс нашего демо-приложения будет состоять из двух экранов ActivityА и ActivityB и общего фрагмента, используемого обоими активностями SharedFragment.
![](https://habrastorage.org/files/a07/aca/547/a07aca547e884f1993613303266c97d4.png)
![](https://habrastorage.org/files/ab1/a72/bf7/ab1a72bf7c1b4d5e82338dbd9f99fcef.png)
В приложении будет 2 скоупа: Singleton и ActivityScope.
Условно все наши бины можно разделить на 3 группы:
- Синглтоны — SingletonBean
- Бины активити скоупа, которые нужные только внутри активити — BeanA и BeanB
- Бины активити скоупа, доступ к которым нужен как из самой активити, так и из других мест активити скоупа, например, фрагмента — SharedBean
Каждый бин при создании получает уникальный id. Это позволяет наглядно понять, работает ли скоуп как задумывалось, потому что каждый новый инстанс бина будет иметь id, отличный от предыдущего.
![](https://habrastorage.org/files/4d2/df5/6c1/4d2df56c1f3d416f9f31cb4b39a25bfb.png)
Таким образом, в приложении будет существовать 3 графа объектов (3 компонента)
- SingletonComponent — граф объектов, которые существуют, пока приложение запущено, и не убито системой
- ComponentActivityA — граф объектов, необходимых для работы ActivityA (в том числе ее фрагментов, адаптеров, презентеров и так далее) и существующих до тех пор, пока существует экземпляр ActivityA. При уничтожении и пересоздании активити, граф также будет уничтожен и создан заново вместе с новым экземпляром активити. Данный граф является супермножеством, включающим в себя все объекты из Singleton скоупа.
- ComponentActivityB — аналогичный граф, но для ActivityB
![](https://habrastorage.org/files/002/46a/708/00246a7086944930a716905af9d39043.png)
Перейдем к реализации. Для начала подключаем Dagger 2 к нашему проекту. Для этого подключим android-apt плагин в корневом build.gradle…
buildscript {
//...
dependencies {
//...
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
и сам Dagger 2 в app/build.gradle
dependencies {
compile 'com.google.dagger:dagger:2.7'
apt 'com.google.dagger:dagger-compiler:2.7'
}
Далее объявляем модуль, который будет провайдить синглтоны
@Module
public class SingletonModule {
@Singleton
@Provides
SingletonBean provideSingletonBean() {
return new SingletonBean();
}
}
и компонент синглтон:
@Singleton
@Component(modules = SingletonModule.class)
public interface SingletonComponent {
}
Создаем инжектор — единственный синглтон в нашем приложении, которым будем управлять мы, а не Dagger 2, и который будет держать Singleton скоуп даггера и отвечать за инжекцию.
public final class Injector {
private static final Injector INSTANCE = new Injector();
private SingletonComponent singletonComponent;
private Injector() {
singletonComponent = DaggerSingletonComponent.builder()
.singletonModule(new SingletonModule())
.build();
}
public static SingletonComponent getSingletonComponent() {
return INSTANCE.singletonComponent;
}
}
Объявляем ActivityScope. Для того, чтобы объявить свой скоуп, необходимо создать аннотацию с именем скоупа и пометить ее аннотацией javax.inject.Scope.
@Scope
public @interface ActivityScope {
}
Группируем бины в модули: разделяемый и для активностей
@Module
public class ModuleA {
@ActivityScope
@Provides
BeanA provideBeanA() {
return new BeanA();
}
}
@Module
public class ModuleB {
@ActivityScope
@Provides
BeanB provideBeanB() {
return new BeanB();
}
}
@Module
public class SharedModule {
@ActivityScope
@Provides
SharedBean provideSharedBean() {
return new SharedBean();
}
}
Объявляем соответствующие компоненты активностей. Для того чтобы реализовать компонент, который будет включать в себя объекты другого компонента, есть 2 способа: subcomponents и component dependencies. В первом случае дочерние компоненты имеют доступ ко всем объектам родительского компонента автоматически. Во втором — в родительском компоненте необходимо явно указать список объектов, которые мы хотим экспортировать в дочерние. В рамках одного приложения, на мой взгляд, удобнее использовать первый вариант.
@ActivityScope
@Subcomponent(modules = {ModuleA.class, SharedModule.class})
public interface ComponentActivityA {
void inject(ActivityA activity);
void inject(SharedFragment fragment);
}
@ActivityScope
@Subcomponent(modules = {ModuleB.class, SharedModule.class})
public interface ComponentActivityB {
void inject(ActivityB activity);
void inject(SharedFragment fragment);
}
В созданных сабкомпонентах объявляем точки инжекции. В нашем примере таких точек две: Activity и SharedFragment. Они будут иметь общие разделяемые бины SharedBean.
Инстансы сабкомпонентов получаются из родительского компонента путем добавления объектов из модуля сабкомпонента к существующему графу. В нашем примере родительским компонентом является SingletonComponent, добавим в него методы создания сабкомпонентов.
@Singleton
@Component(modules = SingletonModule.class)
public interface SingletonComponent {
ComponentActivityA newComponent(ModuleA a, SharedModule shared);
ComponentActivityB newComponent(ModuleB b, SharedModule shared);
}
Вот и всё. Вся инфраструктура готова, осталось инстанцировать объявленные компоненты и заинжектить зависимости. Начнем с фрагмента.
Фрагмент используется сразу внутри двух различных активностей, поэтому он не должен знать конкретных деталей об активности, внутри которой находится. Однако, нам необходим доступ к компоненту активити, чтобы через него получить доступ к графу объектов нашего скоупа. Чтобы решить эту «проблему», используем паттерн Inversion of Control, создав промежуточный интерфейс InjectorProvider, через который и будет строится взаимодействие с активностями.
public class SharedFragment extends Fragment {
@Inject
SharedBean shared;
@Inject
SingletonBean singleton;
//…
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof InjectorProvider) {
((InjectorProvider) context).inject(this);
} else {
throw new IllegalStateException("You should provide InjectorProvider");
}
}
public interface InjectorProvider {
void inject(SharedFragment fragment);
}
}
Осталось инстанцировать компоненты уровня ActivityScope внутри каждой из активностей и проинжектить активность и содержащийся внутри неё фрагмент
public class ActivityA extends AppCompatActivity implements SharedFragment.InjectorProvider {
@Inject
SharedBean shared;
@Inject
BeanA a;
@Inject
SingletonBean singleton;
ComponentActivityA component =
Injector.getSingletonComponent()
.newComponent(new ModuleA(), new SharedModule());
//...
@Override
public void inject(SharedFragment fragment) {
component.inject(this);
component.inject(fragment);
}
}
Озвучу еще раз основные моменты:
- Мы создали 2 различных скоупа: Singleton и ActivityScope
- ActivityScope реализуется через Subcomponent, а не component dependencies, чтобы не нужно было явно экспотировать все бины из Singleton скоупа
- Активити хранит ссылку на граф объектов соответствующего ей ActivityScop-а и выполняет инжектирование себя и всех классов, которые хотят в себя инжектировать бины из ActivityScope, например, SharedFragment
- С уничтожением активити уничтожается и граф объектов для данной активности
- Граф Singleton объектов существует до тех пор, пока существует инстанс приложения
На первый взгляд может показаться, что для реализации такой простой задачи необходимо написать достаточно много связующего кода. В демо-приложении количество классов, выполняющих «работу» (бинов, фрагментов и активностей), примерно сопоставимо с количеством «связующих» классов даггера. Однако:
- В реальном проекте количество «рабочих» классов будет значительно больше.
- Связующий код достаточно написать однажды, а потом просто добавлять нужные компоненты и модули.
- Использование DI сильно облегчает тестирование. У вас появляются дополнительные возможности по инжектированию моков и стабов вместо реальных бинов во время тестирования
- Код бизнес-логики становится более изолированным и лаконичным за счет переноса связующего и инстанциирующего кода в классы даггера. При этом в самих классах бизнес-логики остается только бизнес-логика и ничего лишнего. Такие классы опять же легче писать, поддерживать и покрывать юнит-тестами
» Демо-проект доступен на гитхабе
Всем Dagger и happy coding! :)
Комментарии (10)
k0ber
28.10.2016 13:53Возможно, стоило добавить для наглядности способ переходить между активити A и B в демо-проекте.
Давайте такую ситуацию представим — из активити A стартует активити B, инжектит в свой фрагмент SharedBean, но теперь я хочу, чтобы SharedBean был тем же самым инстансом, с которым я работал во фрагменте на активити A. Добавлять ещё один глобальный синглтон мне не хотелось бы. Как это можно разрулить с помощью даггера?
ZAit
28.10.2016 13:53Правильно ли я понимаю что старый scope чиститься при инжектировании нового или удалении активити? А что если мы сделали к примеру logout, перешли на другую активити, пока старая активити не удалиться все объекты его scope будут существовать и нам все равно руками подчищать данные?
terrakok
28.10.2016 13:53Я не понял один момент:
На схеме бинов есть Shared Bean, и создается впечатление, что он, при переходе на новое активити, не будет создаваться заново, а будет использоваться общий между двумя фрагментами разных активити.
Но далее, судя по коду, этот Shared Bean ничем не отличается от Bean A и Bean B, и будет создаваться новый инстанс для нового активити B.
Еще странный момент: на скринах двух активити ИД отличаются даже у SingletonBean!
Dimezis
То есть в вашем примере граф будет пересоздаваться при каждом повороте экрана?
Как насчет хранить его в lastCustomNonConfigurationInstance?
Lawliet666
Это уже другой вопрос, можно хранить разными способами. Эта статья о scope.
DZVang
Хранение состояние очень тесно связано со scope. Собственно хранение и обеспечивает необходимый жизненный цикл, а не scope.
Artem_zin
Что вроде как не противоречит словам предыдущего оратора :)
DZVang
Справедливо) Но, по моему, без этого момента статья кажется не полной, а то создается впечатление, что @ActivityScope какая-то магическая аннотация. которая все сделает за тебя.
Artem_zin
С этим я безоговорочно согласен!