Dagger 2. Subcomponents. Best practice. Part 2

    Всем привет! В прошлый раз мы разобрались с реализацией Subcomponent и случаями использования его на примере отдельно взятого экрана. Здесь будет несколько отсылок к той статье, поэтому лучше сначала ознакомиться с ней.

    Сегодня же мы обсудим создание реальной авторизованной зоны приложения и работу с соцсетями. Конечно же не без помощи Dagger’а!

    Интересно? Добро пожаловать под кат!
    image

    Абстрагируемся

    Предыдущая статья строилась на абстракции врача и ассистента, но чтобы описать заявленные во вступлении кейсы, я решил подобрать более подходящую.

    Представьте крупный железнодорожный вокзал со всеми его развилками, семафорами и длинными грузовыми составами. На платформе 3-го пути под моросящим дождём ожидает свою электричку толпа народу. Вдалеке уже слышен звонкий гудок, люди встают со своих сумок, как вдруг… Долгожданный поезд прибывает на 7-й путь! Видать, машинист не перевел стрелку (а в нашем примере именно машинист переводит стрелки).

    И, в общем-то, ничего страшного — добежать до нужной платформы через надземный переход. Страшно, что на 7-й путь с другой стороны подъезжает ТОВАРНЯК!

    СТОП


    Давайте разберемся, как так могло получиться? Причин может быть несколько:

    • Первая — это, конечно, невнимательность. Возможно, наш железнодорожный дальнобойщик увлекся написанием смс жене, чтобы та ставила воду на пельмени.
    • Нельзя отбросить и человеческий фактор, все-таки «путей много, а он один».
    • А может быть он специально?! Ох уж эти нынешние нелегкие времена...

    На самом же деле, причина здесь одна — это сама возможность машиниста поехать не туда. Он провел долгое время в пути и его не должна обременять еще и логистика каждого вокзала.

    Фух… Как же все-таки хорошо, что в жизни машинист не сам переводит стрелки, а за него это делают работники станции.

    Но причем тут авторизованная зона?

    Немного боли

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

    if (userIsAuthourized) {
        // Здесь может быть всё, что угодно. doSomething();
        showAnimation();
    } else {
        // doOtherthing();
        showOtherAnimation();
    }
    

    И увидите много раз :(
    image

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

    Возможно, вы уже догадались, к чему я веду.

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

    И тут важно понять, что такая реализация не является именно авторизованной зоной (причина, почему я выделил слово «реальной» в предисловии). «Зона» означает, что из текущего объекта (или состояния) доступен только один определенный функционал — либо пользовательский, либо анонимный. В данной же реализации объект сам решает, к чему обращаться, и, по идее, может обратиться из одной «зоны» к функционалу другой «зоны». That’s wrong.

    Да и, опять-таки, это — логика, которой не должно быть в нашем компоненте.
    image

    Время перемен

    Любой функционал, метод или кусок кода можно вынести в класс, чтобы в дальнейшем иметь возможность предоставить или подменить его объект. Вынесем анимацию в UserAnimationFunction и AnonymousAnimationFunction соответственно. Ну и, как мы это уже умеем, заинжектим.

    Преобразование 1

    @Inject
    UserAnimationFunction userAnimationFunction;
    @Inject
    AnonymousAnimationFunction anonymousAnimationFunction;
    

    От злосчастного if мы, конечно, пока не ушли.

    if (userIsAuthourized) {
        userAnimationFunction.show();
    } else {
        anonymousAnimationFunction.show();
    }
    

    Правильная мысль:

    Машинист должен просто сообщить о том, что он прибывает, а уже работники станции подготовят для него определенный путь.

    (равно как)

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

    Это — ничто иное, как интерфейс. Определим единый интерфейс для наших анимаций:

    public interface AuthDependentAnimationFunction {
        void show();
    }
    

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

    Преобразование 2

    @Inject
    AuthDependentAnimationFunction animationFunction;
    
    ...
    
    animationFunction.show();
    

    Спроси меня, «как?»

    Вся магия становится возможной с помощью Subcomponent’ов. Как вы помните, в прошлой статье мы использовали Subcomponent’ы, чтобы каждый экран имел собственный граф зависимостей. Время жизни такого графа зависело от времени жизни экрана.

    В нашем случае мы будем иметь 2 графа зависимостей: один для авторизованной зоны и другой — для анонимной. Не трудно догадаться, что время жизни будет зависеть от времени авторизованности.

    Зачастую, запросы к API включают некоторый токен пользователя, а потому такой токен можно взять за ядро нашего авторизованного графа.

    Для анонимного графа можно ядро опустить (всё на ваш вкус, можно опустить ядро и для авторизованного).

    Схематично наша система будет выглядеть так:
    image

    Вы, наверное, обратили внимание на интерфейс AuthDependentComponent. Работая с интерфейсами, в своем программном компоненте мы избавились от всех кейсов выбора логики, кроме одного. Наш выбор логики свелся к выбору Component’а для инъекции:

    if (userIsAuthourized) {
        App.getInstance().getUserComponent.inject();
    } else {
        App.getInstance().getAnonymousComponent.inject();
    }
    

    А общий интерфейс AuthDependentComponent для двух Subcomponent’ов как раз позволит избавиться и от этого кода.

    public interface AuthDependentComponent {
        void inject(SomeFragment fragment);
    }
    

    Обратите внимание. AuthDependentComponent — это просто интерфейс без каких-либо аннотаций «Component», «Subcomponent» и т.п. Он необходим нам только как общий предок для двух Component’ов. Также в нём можно описывать inject-методы — Dagger реализует их для каждого из Component’ов наследников.

    @UserScope
    @Subcomponent(modules = UserModule.class)
    public interface UserComponent extends AuthDependentComponent {
    }
    
    @Module
    public class UserModule {
    
        private String userToken;
    
        public UserModule(String userToken) {
            this.userToken = userToken;
        }
    
        @UserScope
        @Provides
        AuthDependentAnimationFunction provideAnimationFunction() {
            return new UserAnimationFunction();
        }
    }
    
    @AnonymousScope
    @Subcomponent(modules = AnonymousModule.class)
    public interface AnonymousComponent extends AuthDependentComponent {
    }
    
    @Module
    public class AnonymousModule {
    
        @AnonymousScope
        @Provides
        AuthDependentAnimationFunction provideAnimationFunction() {
            return new AnonymousAnimationFunction();
        }
    }
    
    @Singletone
    @Component(modules = AppModule.class)
    public interface AppComponent {
    
        UserComponent userComponent(UserModule userModule);
    
        AnonymousComponent anonymousComponent(AnonymousModule anonymousModule);
    }
    

    Единственным ответственным, знающим текущее состояние авторизованности, становится наш класс App, он и есть работник станции.

    public class App extends Application {
    
    private AuthDependentComponent authDependentComponent;
    
    ...
    
        private void init() {
            ...
            onUserLoggedIn();
        }
    
        public void onUserLoggedIn(String userToken) {
            authDependentComponent = appComponent.userComponent(new UserModule(userToken));
        }
    
        public void onUserLoggedOut() {
            authDependentComponent = appComponent.anonymousComponent(new AnonimousModule());
        }
    
        public AuthDependentComponent getAuthDependentComponent() {
            return authDependentComponent;
        }
    }
    

    В итоге наш экран будет выглядеть так:

    @Inject
    AuthDependentAnimationFunction animationFunction;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        App.getInstance().getAuthDependentComponent().inject(this);
    }
    
    public void showAnimation() {
        animationFunction.show();
    }
    

    Что касается перехода между зонами

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

    Если же ваша логика предусматривает незначительные различия в логике, вы можете обратить внимание на реинжект зависимостей. По сути, при изменении состояния авторизованности просто принудительно вызывайте inject() для программного компонента:

    App.getInstance().getAuthDependentComponent().inject(this);
    

    Так ранее созданные зависимости из старого компонента перепишутся зависимостями из нового.
    image

    БОНУС


    Кажется, я что-то упоминал о соцсетях?

    Проблема в том, что разработчики разных соцсетей реализовали свои API кто во что горазд. Кто пробовал поддерживать больше чем одну соцсеть одновременно, тот знает. Однако, суть методов этих API примерно одинакова:

    — Публикация постов;
    — Публикация фотографий;
    — Изменение статуса;
    — и т.п.

    Чтобы всё было красиво, возьмите пример с авторизованной зоной и представьте, что каждая социальная сеть — это отдельная зона. Таким образом, для каждой зоны у вас будет свой Component, предоставляемый в зависимости от того, в какой из соцсетей авторизован пользователь. А работа с API будет происходить опять-таки через интерфейс с вышеописанными функциями, реализованными для каждой соцсети по-разному. Just do it! :)

    Пример с реализацией работы с соц.сетями на github

    ИТОГ ДВУХ СТАТЕЙ


    Как найти участки кода, где Dagger пришелся бы кстати?

    • Во-первых, это ветвления достаточно большой части логики, будь то бизнес-логика или UI/UX.
    • Во-вторых, в большинстве случаев это создание нового объекта, т.е. ориентируемся на ключевое слово new, либо фабричные методы.

    В общем-то, желаю всем удачи в постижении Dagger’а и паттерна Dependency Injection.
    Пишите свои интересные кейсы и задачи в комментариях, ну и следите за новостями!
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 0

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