- PVSM.RU - https://www.pvsm.ru -
Однажды мы в компании EastBanc Technologies устали бороться с теми архитектурными проблемами, которые возникают в Android-разработке и решили все исправить:). Мы хотели найти решение, которое удовлетворит всем нашим требованиям.
И, как это часто бывает, готового решения тогда не нашлось и нам пришлось сделать собственную библиотеку, которая уже приносит счастье нам, и может помочь и вам.
Какие проблемы решали:

Лирическое отступление. Почему Reamp?
Это же вроде такая приблуда для записывания электрогитар?
Конечно, в нашем случае Reamp к звукозаписи никакого отношения не имеет. Изначально мы думали что это будет аббревиатура, потому что там есть M и P (model и presenter), A — уже и не помним зачем, RE — потому что это было на реактиве написано. Но реактив мы уже выкинули, и осталось просто прикольное название.
В процессе реализации мы старались следовать манифесту, который сами же и придумали:
В результате у нас получилась MVP/MVVM библиотека, которую мы с успехом используем уже больше года и пока не собираемся менять. Мы считаем, что теперь пришло время поделиться ей с общественностью!
Давайте рассмотрим решение самой типовой задачи практически любого мобильного приложения – авторизация.
У нас есть поля ввода логина и пароля, кнопка входа, ProgressBar для отображения хода операции и TextView, чтобы показать результат.

Требования к поведению такого экрана довольно типичны:
Давайте проанализируем, о чем должен подумать разработчик при решении такой задачи.
А что тут сложного? На loginEditText вешаем changeListener, который включает или выключает кнопку, когда login пустой или не пустой!
loginEditText.addTextChangeListener = { text -> button.setEnabled(text.length() > 0) }
Да, но это будет работать только для одного поля. А у нас еще есть пароль:
loginEditText.addTextChangeListener = { text -> validate() }
passwordEditText.addTextChangeListener = { text -> validate() }
private void validate() {
boolean loginValid = loginEditText.getText().toString().lenght() > 0
boolean passwordValid = passwordEditText.getText().toString().lenght() > 0
button.setEnabled(loginValid && passwordValid)
}
Ну теперь то точно все! Не а, есть еще асинхронная операция входа, в процессе которой кнопка должна быть заблокирована.
Ок, просто выключаем кнопку перед выполнением запроса и… тогда ее можно будет включить, поменяв текст в loginEditText или passwordEditText.
Правильнее будет добавить проверку наличия активного запроса внутрь метода validate().
Наверное вы уже догадались, к чему этот пункт. Нужно помнить о куче вещей и их связей, которые могут влиять на UI.
О них легко забыть, когда нужно добавить и провалидировать еще одно поле ввода или Switch.
Для входа нам нужна асинхронная операция, будь то AsyncTask или RxJava + Scheduler, неважно.
Важно то, что мы не можем написать ее внутри нашей Activity, ведь мы не хотим останавливать ее при повороте экрана.
Нужно вынести задачу за рамки Activity, при ее запуске придумать и запомнить какой-то ее идентификатор, чтобы позднее иметь возможность проверить статус этой задачи или получить ее результат.
И нужно будет написать какой-то менеджер подобных операций или взять из готовых, благо таковых много.
Состояние экрана — это то, с чем приходится иметь дело постоянно.
Парадоксально, но факт — многие разработчики продолжают игнорировать состояние экрана в своих приложениях, оправдываясь тем, что его программа работает только в одной ориентации.
В то время, как EditText умеет самостоятельно хранить введенный в него текст, состояние кнопки входа придется восстанавливать в соответствии с введенным текстом и текущей сетевой операцией.
Чем больше различных данных нужно хранить и восстанавливать в Activity, тем сложнее за ними следить и тем проще что-то упустить.
В Reamp мы используем Presenter для реализации поведения экрана и StateModel для хранения тех данных, которые этому экрану нужны.
Все довольно просто. Presenter практически не зависит от жизненного цикла экрана.
Выполняя какие-то операции, которые от него требуются, Presenter заполняет объект StateModel разными нужными данными.
Каждый раз, когда Presenter считает, что свежие данные нужно показать на эране, он сообщает об этом своей View.
На практике это работает следующим образом:
LoginState – класс, содержащий информацию о том, что должно отображаться на экране:
нужно ли показывать ProgressBar, какое состояние должно быть у кнопки входа, что написано в текстовых полях ввода и т.п.
LoginPresenter получает события от LoginActivity (ввели текст, нажали кнопку),
выполняет нужные операции, заполняет класс LoginState нужными данными и отправляет в LoginActivity на “рендеринг”.
LoginActivity получает событие о том, что данные в LoginState изменились и настраивает свой layout в соответствии с ними.
//LoginState
public class LoginState extends SerializableStateModel {
public String login;
public String password;
public boolean showProgress;
public Boolean loggedIn;
public boolean isSuccessLogin() {
return loggedIn != null && loggedIn;
}
}
//LoginPresenter
public class LoginPresenter extends MvpPresenter<LoginState> {
@Override
public void onPresenterCreated() {
super.onPresenterCreated();
//настраиваем отображение при свежем старте
getStateModel().setLogin("");
getStateModel().setPassword("");
getStateModel().setLoggedIn(null);
getStateModel().setShowProgress(false);
sendStateModel(); //отправляем LoginState на "отрисовку"
}
// вызывается классом View, когда требуется выполнить логин
public void login() {
getStateModel().setShowProgress(true); // экран должен показать индикатор прогресса
getStateModel().setLoggedIn(null); // результат входа пока неизвестен
sendStateModel(); // отправляем текущее состояние экрана на "отрисовку"
// эмулируем пятисекундный запрос на вход
new Handler()
.postDelayed(new Runnable() {
@Override
public void run() {
getStateModel().setLoggedIn(true); // сообщаем об успешном входе
getStateModel().setShowProgress(false); // убираем индикатор прогресса
sendStateModel(); // отправляем текущее состояние экрана на "отрисовку"
}
}, 5000);
}
public void loginChanged(String login) {
getStateModel().setLogin(login); // запоминаем то, что ввел пользователь
}
public void passwordChanged(String password) {
getStateModel().setPassword(password); // запоминаем то, что ввел пользователь
}
}
//LoginActivity
public class LoginActivity extends MvpAppCompatActivity<LoginPresenter, LoginState> {
/***/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
/***/
loginActionView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getPresenter().login(); // сообщаем о событии презентеру
}
});
// следим за тем, что ввел пользователь
loginInput.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void afterTextChanged(Editable s) {
getPresenter().loginChanged(s.toString()); // сообщаем о событии презентеру
}
});
// следим за тем, что ввел пользователь
passwordInput.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void afterTextChanged(Editable s) {
getPresenter().passwordChanged(s.toString()); // сообщаем о событии презентеру
}
});
}
// вызывается библиотекой, когда требуется создать свежий экземпляр модели LoginState
@Override
public LoginState onCreateStateModel() {
return new LoginState();
}
// вызывается библиотекой, когда требуется создать свежий экземпляр презентера LoginPresenter
@Override
public MvpPresenter<LoginState> onCreatePresenter() {
return new LoginPresenter();
}
// вызывается библиотекой каждый раз, когда состояние экрана поменялось
@Override
public void onStateChanged(LoginState stateModel) {
progressView.setVisibility(stateModel.showProgress ? View.VISIBLE : View.GONE); // устанавливаем нужное состояние индикатора прогресса
loginActionView.setEnabled(!stateModel.showProgress); // пока происходит запрос, кнопка входа недоступна
successView.setVisibility(stateModel.isSuccessLogin() ? View.VISIBLE : View.GONE); // устанавливаем нужное состояние "успешного" виджета
}
}
На первый взгляд все, что мы сделали – это вынесли значимые динамические данные в LoginState, перенесли часть кода (такую как запрос на вход) из Activity в Presenter и больше ничего. На второй взгляд — это действительно так :) Потому, что всю скучную работу за нас делает Reamp:
LoginActivity она сразу получит последнее состояние LoginState. Если запрос все еще выполняется, LoginState будет содержать информацию о том, что кнопка входа неактивна, а индикатор загрузки показывается. Если же операция входа успеет завершиться как раз в момент поворота экрана, презентер заполнит LoginState результатом входа и будущая LoginActivity сразу получит этот результат.LoginState попадают в Bundle savedState, когда система просит сохранить состояние экрана. Разумеется, Reamp умеет восстанавливать LoginState из Bundle, если наша программа была выгружена из памяти ранее. По умолчанию для сохранения LoginState используется механизм сериализации объектов, но вы всегда можете написать свой, если нужно.savedState на null при старте LoginActivity, так же как и нет вероятности забыть показать ProgressBar, если запрос на вход уже в процессе. Весь код, отвечающий за отображение текущего состояния сосредоточен в одном месте и всегда учитывает данные из LoginState целиком. Такой подход обеспечивает консистентность данных на UI.Activity перед тем, как что-то сделать с UI, как это делается в некоторых других MVP-библиотеках. Другими словами, нет бесконечных проверок if (view != null). В презентере мы работаем напрямую с состоянием, которое доступно в любой момент времени.Мы перечислили, как Reamp помогает избавиться от boilerplate-кода, но это далеко не весь профит от использования библиотеки. С помощью Reamp мы повышаем стабильность работы приложения: Reamp позаботится о том, чтобы вызов метода onStateChanged(...) всегда происходил в главном потоке.
Все исключения, возникающие внутри вызова onStateChanged(...) не роняют процесс приложения. Правильная работа с исключениями в Java это высокий скилл, но исключения, возникающие на самом верхнем UI уровне (при настройке layout), чаще оказываются досадными недоразумениями, чем преднамеренным событием и аварийное завершение программы здесь абсолютно лишнее.
С Reamp можно не бояться утечек Activity, т.к. вы всегда работаете напрямую с классами презентера и состояния.
Last but not least, с помощью Reamp мы повышаем качество кода:
Код становится более тестируемым. В действительности, нам даже не нужны Instrumentation-тесты, т.к. достаточно протестировать презентер и убедиться, что после каждой операции наш LoginState имеет правильный набор данных
Класс состояния – это отличный кандидат для хранения UI логики. Если наш LoginStateзнает о прогрессе входа, введенных логине и пароле, то он уже имеет все исходные данные, чтобы решить нужно ли включить кнопку входа
public class LoginState extends SerializableStateModel {
/***/
public boolean isLoginActionEnabled() {
return !showProgress
&& (loggedIn == null || !loggedIn)
&& !TextUtils.isEmpty(login)
&& !TextUtils.isEmpty(password);
}
}
Такой подход хорошо согласуется с принципом разделения ответственности и сильно разгружает код класса нашей LoginActicity.
Код становится переиспользуемым. LoginPresenter можно использовать и в других проектах, где нужно реализовать похожий экран, просто поменяв UI составляющую этого экрана.
Безусловно, Reamp – не единственная MVP/MVVM библиотека, тысячи их!
Когда мы начинали делать Reamp мы сознательно хотели написать то, что нужно именно нам.
И, конечно, мы изучали имеющиеся на то время альтернативы, чтобы взять лучшее и избежать того, что нам не понравится :)
Не хочется устраивать холивар и тем более тыкать в кого-то пальцем, просто резюмируем то, что нам нравится в Reamp, а чего мы стараемся в нем избегать.
Во-первых, Reamp очень простой в использовании. Мы не используем генерацию кода и стараемся вводить минимум новых классов, которые нужны лишь для работы самой библиотеки.
В отличие, к примеру, от новых Android Architecture Components, нам не требуется целого зоопарка вспомогательных технических классов и аннотаций, чтобы решить те же проблемы.
Второй пункт является отчасти следствием первого. Имея неперегруженную архитектуру и минимум зависимостей можно легко интегрироваться со многими популярными современными технологиями.
Например, с DataBinding, ведь StateModel уже и есть квинтэссенция тех данных, которые нужны DataBinding-у для работы.
Еще один пример, не имея никакой магии с байт-кодом, мы без всяких проблем используем Reamp программируя на Kotlin.
В-третьих, нет необходимости глобально менять какой-то существующий проект, можно просто начать использовать Reamp в уже существующем проекте.
В одной статье сложно рассказать про все, что хочется, но у нас есть демо-приложение [1], которое шаг за шагом покажет все возможности Reamp, от самых простых до комплексных решений.

Reamp на GitHub — https://github.com/eastbanctechru/Reamp [2]
Демо-приложение — https://github.com/eastbanctechru/Reamp/tree/master/sample [1]
Если вы хотите попробовать Reamp в своем проекте или хотите получить больше информации,
загляните в Wiki [3] проекта, а в особенности в раздел FAQ [4].
Автор: eastbanctech
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/open-source/264468
Ссылки в тексте:
[1] демо-приложение: https://github.com/eastbanctechru/Reamp/tree/master/sample
[2] https://github.com/eastbanctechru/Reamp: https://github.com/eastbanctechru/Reamp
[3] Wiki: https://github.com/eastbanctechru/Reamp/wiki
[4] FAQ: https://github.com/eastbanctechru/Reamp/wiki/Questions-and-Answers
[5] Источник: https://habrahabr.ru/post/338744/
Нажмите здесь для печати.