Всем привет! В прошлый раз мы разобрались с реализацией Subcomponent и случаями использования его на примере отдельно взятого экрана. Здесь будет несколько отсылок к той статье, поэтому лучше сначала ознакомиться с ней.
Сегодня же мы обсудим создание реальной авторизованной зоны приложения и работу с соцсетями. Конечно же не без помощи 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.
Пишите свои интересные кейсы и задачи в комментариях, ну и следите за новостями!
Автор: htc-cs