Предпосылки

В андроид-сообществе сформировалось много различных подходов к архитектуре. Начнинали с Activity, затем к ним добавились Fragments и нужно было все это как-то организовывать. Да так, чтобы это не превращалось со временем в God-объекты подпертые костылями, а было удобным и для расширения и для изменения. Разработчики почесали голову и вспомнили, что за них уже придумали MVC, MVP и другие аббревиатуры. Сейчас на странице https://github.com/googlesamples/android-architecture представлены наиболее популярные архитектуры.

Что такое архитектура?

Не только UI.

Одно Activity или несколько?

Я думаю - одно. Если у приложения будет всего одна активити, то мы можем точно сказать, что визуальное взаимодействие с пользователем происходит с момента onCreate до момента onDestroy (с перерывами на onStop-onStart при сворачивании). Как минимум, это удобно.

Что предлагает Google

https://developer.android.com/topic/libraries/architecture/index.html

В активити есть жизненный цикл. В новых Architecture Components от Google он был прокинут в данные и так появился LiveData. Также любой объект теперь можно усложнить жизненным циклом, используя LifecycleOwner интерфейс компонентов LifecycleActivity и LifecycleFragment. Впоследствии эти классы были объявлены @Deprecated, наследовать чужой класс не удобно, если есть уже построенная иерархия.

Реактивный подход

Что, если рассмотреть приложение как набор эвентов и реакций на них?

Действительно, что такое Activity? Это класс-точка входа. У него есть эвенты-события, говорящие, что пользователь увидел визуальную часть и что визуальная часть скрыта. Также там присутствуют некоторые дополнительные колбэки, вроде нового пришедшего интента, с помощью которого можно понять, откуда произошел запуск. Или нажатия физических кнопок громкости. Еще можно поймать результат запуска другой активити. На этом все, в остальном приложении мы пользуемся только этой основной информацией.

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

	BehaviorSubject<StateEvent> getStateEventBehaviorSubject(final int type) {
		BehaviorSubject<StateEvent> eventsObservable = mEventObservablesByType.get(type);
		if (eventsObservable == null) {
			eventsObservable = BehaviorSubject.create();
			mEventObservablesByType.put(type, eventsObservable);
		}
		return eventsObservable;
	}

Теперь на события жизненного цикла можно подписаться:

appStatesGraph
				.eventsOfType(AppStatesGraph.Type.LIFECYCLE, LifecycleEventResume.class)
        .subscribe()

Другие состояния приложения превращаем в потоки событий смены состояний. Например, состояние наличия и отсутствия интернет-соединения будут представлены двумя событиями:

public class Connected extends SimpleEvent {
	
	public Connected() {
		super(AppStatesGraph.Type.CONNECTION);
	}
}
public class Disconnected extends SimpleEvent {
	
	public Disconnected() {
		super(AppStatesGraph.Type.CONNECTION);
	}
}

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

appStatesGraph
				.eventsOfType(AppStatesGraph.Type.APP_KEYS_INITIALIZED)
				.take(1)
				
				.zipWith(appStatesGraph
					.eventsOfType(AppStatesGraph.Type.LIFECYCLE, LifecycleEventStart.class)
          .take(1), (o1, o2)
					-> RxUtils.IGNORED)
				
				.zipWith(appStatesGraph
					.eventsOfType(AppStatesGraph.Type.CONNECTION, Connected.class)
          .take(1), (o1, o2)
					-> RxUtils.IGNORED)
         
         .subscribe(); //todo стартуем, когда все условия соблюдены

Такие комбинации будем называть UseCase - юз кейсами в переводе на англицизм. Удобство их заключается в том, что логика действия вынесена в отдельную сущность юз-кейса, которую можно покрывать тестами. Она не требует зависимости от активити или от какого-либо другого компонента sdk, требующего нетривиального мока в тесте. Второй плюс в инкапсуляции - все правила данного юз-кейса лежат в отдельном и единственном классе. Но в нем не должно быть других правил, не связанных с текущим юз-кейсом и за этим нужно следить (в этом я вижу минус подхода). Назовите класс именем, в котором содержится информация что_делает и когда_делает данный конкретный юз-кейс и придерживайтесь строгих ограничений.

Например, в текущем проекте с данным подходом есть такие классы юз-кейсов:

UseCaseAppCheckWhoAmIOnStart
UseCaseShowOnboardingOnGuideEndOrSkip
UseCaseShowMainPageAfterOnboardings
UseCaseShowDialogsOnAppStart
...

Эти классы подписываются на стримы и делают ровно одну вещь, которая отражена в их названии.

Заключение

В статье привел лишь подход. Конкретная реализация может быть любой на ваше усмотрение. Мне, например, понравилось, как это можно сделать с помощью отдельных классов, соединенных с помощью даггера. Это позволило создать именно центральную часть приложения с бизнес-логикой, находящеся на уровне выше, чем просто логика одного экрана. Обычно эта логика находится в частях, связанных с классами-точками входа, как-то: активити, фрагменты, ресиверы.

Присмотритесь к данному подходу, если логика вашего приложения выходит из контекста одного экрана.