Всем привет! В прошлый раз мы разобрались с реализацией Subcomponent и случаями использования его на примере отдельно взятого экрана. Здесь будет несколько отсылок к той статье, поэтому лучше сначала ознакомиться с ней.
Сегодня же мы обсудим создание реальной авторизованной зоны приложения и работу с соцсетями. Конечно же не без помощи Dagger’а!
Интересно? Добро пожаловать под кат!

Абстрагируемся
Предыдущая статья строилась на абстракции врача и ассистента, но чтобы описать заявленные во вступлении кейсы, я решил подобрать более подходящую.
Представьте крупный железнодорожный вокзал со всеми его развилками, семафорами и длинными грузовыми составами. На платформе 3-го пути под моросящим дождём ожидает свою электричку толпа народу. Вдалеке уже слышен звонкий гудок, люди встают со своих сумок, как вдруг… Долгожданный поезд прибывает на 7-й путь! Видать, машинист не перевел стрелку (а в нашем примере именно машинист переводит стрелки).
И, в общем-то, ничего страшного — добежать до нужной платформы через надземный переход. Страшно, что на 7-й путь с другой стороны подъезжает ТОВАРНЯК!
Давайте разберемся, как так могло получиться? Причин может быть несколько:
На самом же деле, причина здесь одна — это сама возможность машиниста поехать не туда. Он провел долгое время в пути и его не должна обременять еще и логистика каждого вокзала.
Фух… Как же все-таки хорошо, что в жизни машинист не сам переводит стрелки, а за него это делают работники станции.
Но причем тут авторизованная зона?
Немного боли
Допустим, перед нами стоит задача: авторизованный пользователь должен видеть одну анимацию, а аноним другую. В большинстве приложений, реализующих подобную логику, вы увидите примерно следующее:
И увидите много раз :(

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

Время перемен
Любой функционал, метод или кусок кода можно вынести в класс, чтобы в дальнейшем иметь возможность предоставить или подменить его объект. Вынесем анимацию в UserAnimationFunction и AnonymousAnimationFunction соответственно. Ну и, как мы это уже умеем, заинжектим.
Преобразование 1
От злосчастного if мы, конечно, пока не ушли.
Правильная мысль:
Машинист должен просто сообщить о том, что он прибывает, а уже работники станции подготовят для него определенный путь.
(равно как)
Программный компонент должен сообщить о необходимости показать анимацию, а ответственный за анимацию объект уже сам решит, какую именно.
Это — ничто иное, как интерфейс. Определим единый интерфейс для наших анимаций:
Тогда нам останется лишь вызвать тех самых работников станции, которые выберут за нас путь. Наш компонент должен прийти к виду:
Преобразование 2
Спроси меня, «как?»
Вся магия становится возможной с помощью Subcomponent’ов. Как вы помните, в прошлой статье мы использовали Subcomponent’ы, чтобы каждый экран имел собственный граф зависимостей. Время жизни такого графа зависело от времени жизни экрана.
В нашем случае мы будем иметь 2 графа зависимостей: один для авторизованной зоны и другой — для анонимной. Не трудно догадаться, что время жизни будет зависеть от времени авторизованности.
Зачастую, запросы к API включают некоторый токен пользователя, а потому такой токен можно взять за ядро нашего авторизованного графа.
Для анонимного графа можно ядро опустить (всё на ваш вкус, можно опустить ядро и для авторизованного).
Схематично наша система будет выглядеть так:

Вы, наверное, обратили внимание на интерфейс AuthDependentComponent. Работая с интерфейсами, в своем программном компоненте мы избавились от всех кейсов выбора логики, кроме одного. Наш выбор логики свелся к выбору Component’а для инъекции:
А общий интерфейс AuthDependentComponent для двух Subcomponent’ов как раз позволит избавиться и от этого кода.
Единственным ответственным, знающим текущее состояние авторизованности, становится наш класс App, он и есть работник станции.
В итоге наш экран будет выглядеть так:
Что касается перехода между зонами
На своей практике я встречал полное пересоздание всех компонентов приложения при смене состояния авторизованности. В принципе — это нормально, т.к. зачастую различия в зонах существенные. В случае пересоздания вновь созданный экран получит себе зависимости уже из соответствующей зоны, т.е. наш код не нуждается в доработках.
Если же ваша логика предусматривает незначительные различия в логике, вы можете обратить внимание на реинжект зависимостей. По сути, при изменении состояния авторизованности просто принудительно вызывайте inject() для программного компонента:
Так ранее созданные зависимости из старого компонента перепишутся зависимостями из нового.

Кажется, я что-то упоминал о соцсетях?
Проблема в том, что разработчики разных соцсетей реализовали свои API кто во что горазд. Кто пробовал поддерживать больше чем одну соцсеть одновременно, тот знает. Однако, суть методов этих API примерно одинакова:
— Публикация постов;
— Публикация фотографий;
— Изменение статуса;
— и т.п.
Чтобы всё было красиво, возьмите пример с авторизованной зоной и представьте, что каждая социальная сеть — это отдельная зона. Таким образом, для каждой зоны у вас будет свой Component, предоставляемый в зависимости от того, в какой из соцсетей авторизован пользователь. А работа с API будет происходить опять-таки через интерфейс с вышеописанными функциями, реализованными для каждой соцсети по-разному. Just do it! :)
Пример с реализацией работы с соц.сетями на github
Как найти участки кода, где Dagger пришелся бы кстати?
В общем-то, желаю всем удачи в постижении Dagger’а и паттерна Dependency Injection.
Пишите свои интересные кейсы и задачи в комментариях, ну и следите за новостями!
Сегодня же мы обсудим создание реальной авторизованной зоны приложения и работу с соцсетями. Конечно же не без помощи Dagger’а!
Интересно? Добро пожаловать под кат!

Абстрагируемся
Предыдущая статья строилась на абстракции врача и ассистента, но чтобы описать заявленные во вступлении кейсы, я решил подобрать более подходящую.
Представьте крупный железнодорожный вокзал со всеми его развилками, семафорами и длинными грузовыми составами. На платформе 3-го пути под моросящим дождём ожидает свою электричку толпа народу. Вдалеке уже слышен звонкий гудок, люди встают со своих сумок, как вдруг… Долгожданный поезд прибывает на 7-й путь! Видать, машинист не перевел стрелку (а в нашем примере именно машинист переводит стрелки).
И, в общем-то, ничего страшного — добежать до нужной платформы через надземный переход. Страшно, что на 7-й путь с другой стороны подъезжает ТОВАРНЯК!
СТОП
Давайте разберемся, как так могло получиться? Причин может быть несколько:
- Первая — это, конечно, невнимательность. Возможно, наш железнодорожный дальнобойщик увлекся написанием смс жене, чтобы та ставила воду на пельмени.
- Нельзя отбросить и человеческий фактор, все-таки «путей много, а он один».
- А может быть он специально?! Ох уж эти нынешние нелегкие времена...
На самом же деле, причина здесь одна — это сама возможность машиниста поехать не туда. Он провел долгое время в пути и его не должна обременять еще и логистика каждого вокзала.
Фух… Как же все-таки хорошо, что в жизни машинист не сам переводит стрелки, а за него это делают работники станции.
Но причем тут авторизованная зона?
Немного боли
Допустим, перед нами стоит задача: авторизованный пользователь должен видеть одну анимацию, а аноним другую. В большинстве приложений, реализующих подобную логику, вы увидите примерно следующее:
if (userIsAuthourized) { // Здесь может быть всё, что угодно. doSomething(); showAnimation(); } else { // doOtherthing(); showOtherAnimation(); }
И увидите много раз :(

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

Время перемен
Любой функционал, метод или кусок кода можно вынести в класс, чтобы в дальнейшем иметь возможность предоставить или подменить его объект. Вынесем анимацию в 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 включают некоторый токен пользователя, а потому такой токен можно взять за ядро нашего авторизованного графа.
Для анонимного графа можно ядро опустить (всё на ваш вкус, можно опустить ядро и для авторизованного).
Схематично наша система будет выглядеть так:

Вы, наверное, обратили внимание на интерфейс 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);
Так ранее созданные зависимости из старого компонента перепишутся зависимостями из нового.

БОНУС
Кажется, я что-то упоминал о соцсетях?
Проблема в том, что разработчики разных соцсетей реализовали свои API кто во что горазд. Кто пробовал поддерживать больше чем одну соцсеть одновременно, тот знает. Однако, суть методов этих API примерно одинакова:
— Публикация постов;
— Публикация фотографий;
— Изменение статуса;
— и т.п.
Чтобы всё было красиво, возьмите пример с авторизованной зоной и представьте, что каждая социальная сеть — это отдельная зона. Таким образом, для каждой зоны у вас будет свой Component, предоставляемый в зависимости от того, в какой из соцсетей авторизован пользователь. А работа с API будет происходить опять-таки через интерфейс с вышеописанными функциями, реализованными для каждой соцсети по-разному. Just do it! :)
Пример с реализацией работы с соц.сетями на github
ИТОГ ДВУХ СТАТЕЙ
Как найти участки кода, где Dagger пришелся бы кстати?
- Во-первых, это ветвления достаточно большой части логики, будь то бизнес-логика или UI/UX.
- Во-вторых, в большинстве случаев это создание нового объекта, т.е. ориентируемся на ключевое слово new, либо фабричные методы.
В общем-то, желаю всем удачи в постижении Dagger’а и паттерна Dependency Injection.
Пишите свои интересные кейсы и задачи в комментариях, ну и следите за новостями!
