Pull to refresh

Dagger 2. Subcomponents. Best practice. Part 2

Reading time6 min
Views11K
Всем привет! В прошлый раз мы разобрались с реализацией 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.
Пишите свои интересные кейсы и задачи в комментариях, ну и следите за новостями!
Tags:
Hubs:
Total votes 8: ↑7 and ↓1+6
Comments0

Articles