Dagger 2. Часть вторая. Custom scopes, Component dependencies, Subcomponents

    Всем привет!
    Продолжаем наш цикл статей о Dagger 2. Если вы еще не ознакомились с первой частью, немедленно сделайте это :)
    Большое спасибо за отзывы и комментарии по первой части.
    В данной статье мы поговорим о custom scopes, о связывании компонентов через component dependencies и subcomponents. А также затронем такой немаловажный вопрос, как архитектура мобильного приложения, и как Dagger 2 помогает нам выстраивать более правильную, модульнонезависимую архитектуру.
    Всем заинтересовавшихся прошу под кат!


    Архитектура и custom scopes


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


    1. Clean Architecture от дядюшки Боба
    2. Clean Architecture в Android
    3. Перевод на русский

    Подход Clean Architecture при построении архитектуры мне очень нравится. Он позволяет производить четкое вертикальное и горизонтальное построение всех модулей, где каждый класс делает только то, что он должен делать. Например, Fragment ответственнен только за отображение UI, а не осуществление запросов в сеть, БД, реализации бизнес-логики и прочего, что делало Fragment просто огромным куском запутанного кода. Думаю, многим это знакомо..


    Рассмотрим пример. Есть приложение. В приложении есть несколько модулей, один из которых модуль чата. Модуль чата включает в себя три экрана: экран одиночного чата, группового чата и настройки.
    Вспоминая Clean architecture, выделяем три горизонтальных уровня:


    1. Уровень всего приложения. Здесь находятся объекты, которые необходимы на протяжении всего жизненного цикла приложения, то есть "глобальные синглтоны". Пускай это будут объекты: Context (глобальный контекст), RxUtilsAbs (класс-утилита), NetworkUtils (класс-утилита) и IDataRepository (класс, отвечающий за запросы к серверу).
    2. Уровень чата. Объекты, которые нужны для всех трех экранов Чата: IChatInteractor (класс, реализующий конкретные бизнес-кейсы Чата) и IChatStateController (класс, отвечающий за состояние Чата).
    3. Уровень каждого экрана чата. У каждого экрана будет свой Presenter, устойчивый к переориентации, то есть чей жизненный цикл будет отличаться от жизненного цикла фрагмента/активити.

    Схематично жизненные циклы будут выглядеть следующим образом:
    image


    Помните, в прошлой статье мы упоминали о "локальных" синглтонах? Так вот, объекты уровней чата и каждого экрана чата и представляют собой "локальные синглтоны", то есть объекты, чей жизненный цикл больше жизненного цикла стандартного активити/фрагмента, но меньше жизненного цикла всего приложения.
    А вот теперь в дело вступает Dagger 2, у которого есть замечательный механизм Scopes. Данный механизм берет на себя создание и хранение единственного экземпляра необходимого класса до тех пор, пока соответствующий scope существует. Уверен, что фраза "пока существующий scope существует" несколько смущает и порождает вопросы. Не бойтесь, все прояснится ниже.
    В прошлой статье мы помечали "глобальные синглтоны" scope @Singleton. Этот scope существовал все время жизни приложения. Но также мы можем создавать и свои custom scope-аннотации. Например:


    @Scope
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ChatScope {
    }

    А создание аннотации @Singleton в Dagger 2 выглядит так:


    @Scope
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Singleton {
    }

    То есть @Singleton от @ChatScope ничем не отличается, просто аннотация @Singleton предоставляется библиотекой по-умолчанию. И назначение этих аннотаций одно — указать Даггеру, провайдить "scope" или " unscoped" объекты. Но, снова повторюсь, за жизненный цикл "scope" объектов отвечаем мы.
    Возвращаемся к нашему примеру. По текущей архитектуре у нас получаются три группы объектов, у которых своя "длина жизни". Таким образом, нам необходима три scope-аннотации:


    1. @Singleton — для глобальных синглтонов.
    2. @ChatScope — для объектов Чата.
    3. @ChatScreenScope — для объектов конкретного экрана Чата.

    При этом отметим, что @ChatScope объекты должны иметь доступ к @Singleton объектам, а @ChatScreenScope — к @Singleton и @ChatScope объектам.
    Схематично:
    image
    Далее напрашивается создание и соответствующих Компонент Даггера:


    1. AppComponent, который предоставляет "глобальные синглтоны".
    2. ChatComponent, предоставляющий "локальные синглтоны" для всех экранов Чата.
    3. SCComponent, предоставляющий "локальные синглтоны" для конкретного экрана Чата (SingleChatFragment, то есть экрана Одиночного чата).

    И снова визуализируем вышеописанное:
    image
    В итоге получаем три компонента с тремя разными scope-аннотациями, которые связаны друг с другом по цепочке. ChatComponent зависит от AppComponent, а SCComponent — от ChatComponent.


    Но теперь встает вопрос, как нам правильно связать эти компоненты? Существует два способа.


    Component dependencies


    Данный способ связи перекачивал из Dagger 1.
    Отметим сразу особенности Component dependencies:


    1. Два зависимых компонента не могут иметь одинаковый scope. Подобнее тут.
    2. Родительский компонент в своем интерфейсе должен явно задавать объекты, которыми могут пользоваться зависимые компоненты.
    3. Компонент может зависеть от нескольких компонент.

    Как в нашем примере будет выглядеть диаграмма зависимостей с component dependencies:
    image
    Теперь рассмотрим каждый компонент с его модулями по отдельности.


    AppComponent
    image
    Обратим внимание, что в интерфейсе компонента мы явно задаем те объекты, которые будут доступны для дочерних компонент (но не для дочек дочерних компонент, об этой ситуации поговорим чуть позже). Например, если дочерний компонент захочет NetworkUtils, то Даггер выдаст соответствующую ошибку.
    В интерфейсе мы также можем по-прежнему задавать и цели инъекций. То есть у вас не должно создастся заблуждение, что если компонент имеет дочерние компоненты, то он не может инъецировать свои зависимости в необходимые классы (активити/фрагменты/другое).


    ChatComponent
    image
    В аннотации у ChatComponent мы явно прописываем, от какого компонента должен зависеть ChatComponent (зависит от AppComponent). Да, как уже отмечалось ранее, родителей у компонента может быть несколько (достаточно просто добавить в аннотацию новые компоненты-родители). А вот scope-аннотации компонент должны отличаться. И также в интерфейсе явно прописываем те объекты, к которым могут иметь доступ дочерние компоненты.
    Обратили внимание на зеленую стрелочку? Как мы уже говорили, ChatComponent может использовать зависимости от AppComponent, которые тот явно указал. Но вот дочерние компоненты ChatComponent уже не смогут использовать зависимости от AppComponent, если только мы эти зависимости явно не пропишем в ChatComponent, что собственно и сделали для Context.


    SCComponent
    image
    SCComponent является зависимым от ChatComponent, и он инъецирует зависимости в SingleChatFragment. При этом в SingleChatFragment данный компонент может инъецировать как SCPresenter, так и другие объекты родительских компонент, явно прописанные в соответствующих интерфейсах.


    Остался последний шаг. Это проинициализировать компоненты:
    image


    По сравнению с обычным компонентом, при инициализации зависимого компонента в билдерах DaggerChatComponent и DaggerSCComponent появляется еще один метод — appComponent(...) (для DaggerChatComponent) и chatComponent(...) (для DaggerSCComponent), в которые мы указываем проинициализированные родительские компоненты.
    Кстати, если у компонента два родителя, то в билдере появляются два соответствующих метода. Если три родителя, то и три метода и т.д.
    Так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в Application файле. Пример Application классе рассмотрим в конце.


    Subcomponents


    Фича уже Dagger2.
    Особенности:


    1. Необходимо прописывать в интерфейсе родителя метод получения Сабкомпонента (упрощенное название Subcomponent)
    2. Для Сабкомпонента доступны все объекты родителя
    3. Родитель может быть только один

    Да, у Subcomponents есть некоторые отличия от Component dependencies. Рассмотрим схему и код, чтобы лучше понять различия.


    image


    По схеме видим, что для дочернего компонента доступны все объекты родителя, и так по всему дереву зависимостей компонент. Например, для SCComponent доступен NetworkUtils.


    AppComponent


    image


    Следующее отличие Subcomponents. В интерфейсе AppComponent создаем метод для последующей инициализации ChatComponent. Опять-таки главное в этом методе — возвращаемое значение (ChatComponent) и аргументы (ChatModule).
    Замечу, что в конструктор нашего ChatModule ничего передавать не нужно (конструктор по-умолчанию), поэтому в методе plusChatComponent можно и опустить данный аргумент. Однако для более ясной картины зависимостей и в образовательных целях пока оставим все максимально подробно.


    ChatComponent
    image
    ChatComponent — является одновременно и дочерним и родительским компонентом. То, что он родительский, указывает метод создания SCComponent в интерфейсе. А то, что компонент является дочерним, указывает аннотация @Subcomponent.


    SCComponent
    image


    Как мы отмечали ранее, так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в Application файле:


    public class MyApp extends Application {
    
        protected static MyApp instance;
    
        public static MyApp get() {
            return instance;
        }
    
        // Dagger 2 components
        private AppComponent appComponent;
        private ChatComponent chatComponent;
        private SCComponent scComponent;
    
        @Override
        public void onCreate() {
            super.onCreate();
            instance = this;
            // init AppComponent on start of the Application
            appComponent = DaggerAppComponent.builder()
                    .appModule(new AppModule(instance))
                    .build();
        }
    
        public ChatComponent plusChatComponent() {
            // always get only one instance
            if (chatComponent == null) {
                // start lifecycle of chatComponent
                chatComponent = appComponent.plusChatComponent(new ChatModule());
            }
            return chatComponent;
        }
    
        public void clearChatComponent() {
            // end lifecycle of chatComponent
            chatComponent = null;
        }
    
        public SCComponent plusSCComponent() {
            // always get only one instance
            if (scComponent == null) {
                // start lifecycle of scComponent
                scComponent = chatComponent.plusSComponent(new SCModule());
            }
            return scComponent;
        }
    
        public void clearSCComponent() {
            // end lifecycle of scComponent
            scComponent = null;
        }
    
    }

    А вот теперь мы наконец-то можем увидеть жизненный цикл компонент в коде. Про AppComponent все понятно, мы его проинициализировали при старте приложения и больше не трогаем. А вот ChatComponent и SCComponent мы инициализируем по мере необходимости с помощью методов plusChatComponent() и plusSCComponent. Эти методы также отвечают за возврат единственных экземпляров компонент.
    Так при повторном вызове, например,
    scComponent = chatComponent.plusSComponent(new SCModule());
    формируется новый экземпляр SCComponent со своим графом зависимостей.
    С помощью методов clearChatComponent() и clearSCComponent() мы можем прекратить жизнь соответствующих компонент с их графами. Да, обычным занулением ссылок. Если снова необходимы ChatComponent и SCComponent, то мы просто вызываем методы plusChatComponent() и plusSCComponent, которые создают новые экземпляры.
    На всякий случай уточню, что в данном примере инициализировать SCComponent, когда не проинициализирован ChatComponent мы не сможем, выхватим NullPointerException.
    Также отмечу, что если у вас много компонентов и сабкомпонентов, то лучше вынести весь этот код с MyApp в специальный синглтон (например, Injector), который как раз будет ответственен за создание, уничтожение и предоставление необходимых Даггеровских компонент.


    На этом все. Как вы увидели, custom scopes, component dependencies и subcomponent — крайне важные элементы Dagger 2, с помощью которых разработчик может создавать более структурированную и правильную архитектуру.
    Дополнительно к прочтению рекомендую следующие статьи:


    1. Очень хорошая статья про Dagger 2 в общем
    2. Про custom scopes того же автора
    3. Отличия component dependencies от subcomponents

    Буду рад вашим комментариям, замечаниям, вопросам и лайкам :)
    В следующей статье мы рассмотрим применение Dagger 2 в тестировании, а также дополнительные, но от этого не менее важные и функциональные фичи библиотеки.

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 19
    • +1
      Огромное спасибо за диаграммы и стили изложения. Очень полезно для начинающих с DI. Продолжайте в том же духе!
      • +1
        отлично! и диаграммы тоже
        • +1
          Прекрасный цикл статей, очень не хватало когда начал использовать дагер! Огромное спасибо за труд.
          • +1
            Кстати, может быть стоит для удобства восприятия на диаграммах с компонентами где-то рядом с именем класса указывать имя модуля, в котором создаётся его экземпляр? То есть не AppComponent { Context, IDataRepository,… }, а что-нибудь вида
            AppComponent { Context (AppModule), IDataRepository (DataModule),… }

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

            И спасибо за статьи, просто отличные.
            • 0
              Если вам так будет удобнее понимать, то добавлю, конечно.
              • 0
                Добавил имена модулей
              • +1
                Насчёт инициализации screen-related компонентов из Application: меня смущает что получится, если экранов (активити, фрагменты) будет с несколько десятков: будет куча кода, к Application отношения не имеющего. Нарушается Single Responsibility Principle и прочая.

                Возможно стоит вынести ответственность за создание таких компонент и за управление их временем жизни в соответствующие Presenters.
                • 0
                  В Application я их инициализирую для удобства. Так то, конечно, инициализации разнесется по экранам. Все зависит еще от вашей обработки жизненного цикла.
                  Вы предлагаете инициализировать компоненты и контролировать их жизненный цикл в Презентерах?
                  • 0
                    Да, мне кажется, что там им самое место — ведь activity-related-компонент кроме как в контексте данной активити ведь и не нужен никому?

                    При этом разные DBManager-ы нужны именно Presenter-у, а не самой Activity — он из них что-то загрузил и потом результат в activity отправил.

                    С другой стороны, если используется простое приложение без MVP, с парой экранов — можно их, компоненты, там в Application и оставить.
                    Я и предположил, что в Application как раз и приведено создание, чтобы избыточную сложность не добавлять в пример.
                    • 0
                      Распространена реализация, когда прямо во фрагменте создается компонент и там хранится. Если нужна обработка ЖЦ, то фрагмент делается setRetainInstance(true). Как раз для screen-orientated.
                      Просто не представляю, как это через Presenter будет выглядеть. Плюс на Presenter ложиться новая ответственность — создание и контроль ЖЦ компонент.
                      • 0
                        А смотрите, в MVP ведь P как раз и отвечает за общение с внешним миром, выполнение запросов, сохранение результатов в БД, обработку ошибок — и проч., поэтому на мой взгляд логично ему и отдать компонент на хранение.
                        Практик обработки смены ориентации для активити, при которой presenter остаётся жив, несколько, лично мне вот этот подход нравится: http://engineering.remind.com/android-code-that-scales/

                        С точки зрения реализации особых проблем мне не видится, например так:
                        1. activity создалась, создался presenter со своими компонентами и зависимостями (или был взят уже ранее созданный в случае смены ориентации)
                        2. activity говорит: presenter.setView(this), ОК

                        теперь нужно в активити что-то заинжектить.
                        Вопрос: а зачем теперь в неё что-то инжектить? Ведь за связь с внешним миром отвечает сам presenter.

                        Нужно отметить, что правильный презентер сам выполнить inject для активити не сможет: ведь у него есть только интерфейс для активити, а для inject нужен конкретный тип.

                        Но если вдруг надо, то активити всегда может сделать так

                        presenter.setView(this);
                        presenter.getComponent.inject(this);

                        Как-то вот так.
                        • 0
                          Неплохая обработка сохранения/воспроизведения Presenters.
                          Но как-то по мне непривычная немного схема. Надо будет пообдумать.
                    • 0
                      Я поиграл, и пришёл к выводу, что цепочку инициализации всё-таки лучше с Activity начинать.

                      Если этим занимается presenter, ему нужно дать возможность создать ActivityScoped-компонент.
                      Для этого ему наверняка понадобятся другие (ApplicationScope) компоненты, уже созданные в Application.
                      Presenter про Application ничего не знает, значит Activity должна ему передать этот самый Application — либо необходимые компоненты.
                      Короче, получается некрасиво.

                      Ну а сам Presenter, как описано где-то в переписке, удобно между пересозданиями Activity в PresenterManager сохранять.
                      То есть при первом создании активити она создаёт presenter через dagger, а при пересозданиях этой активити — берёт его уже из presenterManager.
                      • 0
                        Либо же создание компонент можно вынести с activity и тому подобных. И все компоненты инициализировать через Application (или вспомогательный класс для Application).
                        Тогда нам вообще не нужно будет подстраиваться под ЖЦ, и все компоненты будут сосредоточены в специальном классе.
                        Например, в Application будет такое поле:
                        private ComponentStorage componentStorage;
                        

                        А доступ к этому полю будет открыт из любого класса, и примерное обращение к конкретному компоненту будет такое:
                        MyApplication.getInstance().getComponentStorage().plusSomeComponent(...);
                        

                        Но на нас ложиться полностью\контроль ЖЦ компонент. Хотя этот контроль на нас в любом случае.
                  • 0
                    Про Qualifier сказано совсем мало. Только что оно надо «если необходимы разные объекты одного типа».
                    Пример использования, мне кажется, не был бы лишним.
                    Тут https://github.com/googlesamples/android-architecture/tree/todo-mvp-dagger показано как можно использовать два «квалифайра» Local и Remote для работы с локальным хранилищем и удаленным.

                    @Qualifier
                    @Retention(RetentionPolicy.RUNTIME)
                    public @interface Local {
                    }

                    @Qualifier
                    @Retention(RetentionPolicy.RUNTIME)
                    public @interface Remote {
                    }

                    @Singleton
                    public class TasksRepository implements TasksDataSource {

                    private final TasksDataSource mTasksRemoteDataSource;
                    private final TasksDataSource mTasksLocalDataSource;

                    @Inject
                    TasksRepository(@Remote TasksDataSource tasksRemoteDataSource,
                    @Local TasksDataSource tasksLocalDataSource) {

                    mTasksRemoteDataSource = tasksRemoteDataSource;
                    mTasksLocalDataSource = tasksLocalDataSource;

                    }

                    @Override
                    public void getTasks(@NonNull final LoadTasksCallback callback) {
                    checkNotNull(callback);

                    if (mCacheIsDirty) {
                    // If the cache is dirty we need to fetch new data from the network.
                    getTasksFromRemoteDataSource(callback);
                    } else {
                    // Query the local storage if available. If not, query the network.
                    mTasksLocalDataSource.getTasks(callback);
                    }
                    }

                    //…
                    }
                    • 0
                      сказано в третьей части
                    • 0
                      SCComponent является зависимым от ChatComponent, и он инъецирует зависимости в SingleChatFragment. При этом в SingleChatFragment данный компонент может инъецировать как SCPresenter, так и другие объекты родительских компонент, явно прописанные в соответствующих интерфейсах.


                      Провайдер из SsModule принимает Context Из AppComponent — а это не возможно, Context должен предоставить ChatComponent, так как зависимости в SsModule зависят от ChatComponent
                      • 0
                        Все верно, поправил это дело.
                        Спасибо за замечание!
                      • 0
                        Объясните пожалуйста, а как Dagger «чистит» за собой ресурсы, процесс создания графа зависимостей понятен, что делать когда Activity/Application будет уничтожено системой?

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.