MVP в Android на примере экрана с пин кодом
Недавно в одном проекте мне понадобилось реализовать классическую схему с пин кодом: при первом запуске приложение просит создать пин код, а при последующих запусках оно просит его ввести и проверяет, что введенный пин правильный. Также есть возможность в настройках сменить пин на новый.
Таким образом, экран пин кода должен поддерживать три режима: создание, проверка и смена пин кода. Реализовав этот экран, я понял, что получился неплохой пример для демонстрации принципов MVP (Model-View-Presenter).
Если вы пока не очень хорошо представляете себе, что такое MVP, то посмотрите сначала этот пост. Он проще и понятнее.
Пример
Напомню, что у экрана с пин кодом может быть три режима.
Соответственно, на экране может быть одно, два или три поля ввода. Каждое поле содержит подпись (label) и имеет ограничение по длине в 4 символа.
При реализации этого экрана я решил использовать одно View и три презентера - отдельный презентер для каждого из трех режимов.
Можно, конечно, было все три режима реализовать в одном презентере, но зачем? Это будет сложно и запутанно. А раз уж я использую MVP, я могу просто разделить логику на три презентера и использовать их для работы с одним экраном.
Пример я оформил отдельным проектом и залил на GitHub. В проекте используются ButterKnife, Dagger 2 и RxJava. Я не буду подробно останавливаться на объяснении принципов их работы. Если вы еще не знакомы с этими инструментами, то я вам рекомендую изучить их. Они сейчас востребованы и активно используются.
Основные компоненты приложения.
Класс по работе с префами. Используется для чтения/записи пин кода.
Здесь описаны Enum для режимов экрана и текстовый ключ, для помещения данных в intent.
Это техническое Activity, без layout. С него стартует приложение.
Из префов читается текущий пин код. Если он задан, то запускается PinCodeActivity в режиме проверки пин кода, иначе - в режиме создания пин кода. И на этом работа Activity завершается.Если у вас есть Activity с заставкой, можно эту логику поместить в него.
Это основной экран приложения.
Содержит кнопки для сброса и изменения пина.
Рассмотрим классы/интерфейсы, принимающие непосредственное участие в MVP.
Базовые классы и интерфейсы MVPИнтерфейс, который будет реализован всеми View, которые будут работать по MVP. В нашем случае интерфейс пустой.
Интерфейс, который будет реализован всеми презентерами, которые будут работать по MVP. Он содержит несколько методов, которые будут вызываться из View.
attachView(V mvpView) - метод для передачи View презентеру. Т.е. View вызовет его и передаст туда себя.
viewIsReady - сигнал презентеру о том, что View готово к работе. Презентер может начинать, например, загружать данные.
detachView - презентер должен отпустить View. Вызывается, например, при повороте экрана, когда уничтожается старый экземпляр Activity, или при закрытии Activity. Презентер должен обнулить ссылку на Activity.
destroy - сигнал презентеру о том, что View завершает свою работу и будет закрыто. Т.е. Здесь необходимо отписываться от всех моделей, завершать все текущие операции и т.п.
Небольшой базовый класс, который будет наследоваться всеми презентерами. В нем реализованы общие методы по работе с View. В этом примере он достаточно прост. Но обычно здесь располагаются также механизмы для сбора RxJava подписок, которые завершаются в методе destroy.
Переходим к реализации MVP на примере экрана для работы с пин кодами.
ContractПлюс MVP в том, что вы легко можете заменить View или Presenter, и вся схема продолжит работать. Чтобы иметь возможность легко заменить один класс другим, необходимо использовать интерфейсы. Вы создаете интерфейс для View и интерфейс для Presenter, и далее в работе используете именно их. В этом случае, вы всегда сможете подменить одну реализацию презентера (или экрана) другой без каких-либо последствий.
В этом файле описаны интерфейсы для View и Presenter для экрана, который будет работать с пин кодом.
Полей для ввода пин кода будет всего три, поэтому названия методов содержат порядковые числительные: first, second, third.
В интерфейсе View описаны методы в основном для работы с полями ввода: отображение, получение текста, установка фокуса, очистка. А также методы для показа сообщений пользователю, запуска следующего экрана и закрытия экрана.
В интерфейсе презентера описаны методы, которые будут вызываться, когда соответствующее поле будет полностью заполнено (когда пользователь ввел 4 символа).
Класс PinCodeActivity.java реализует интерфейс PinCodeContract.View, т.к. это Activity играет роль View в MVP схеме.
Рассмотрим некоторые фрагменты кода.
Методы запуска Activity:
Общий метод startActivity создает Intent для запуска PinCodeActivity и помещает в интент параметр - режим запуска. Методы createPinCode, checkPinCode, changePinCode используют метод startActivity и соответствующий режим.
В onCreate вытаскиваем режим PinCodeMode из интента и используем его для создания презентера.
Для создания презентера тут используется даггер. И код, который он выполняет, выглядит так:
В зависимости от режима, мы создаем один из трех презентеров. Это три разных класса, но все они реализуют интерфейс PinCodeContract.Presenter.
А в PinCodeActivity даггер помещает созданный презентер в поле:
Как видите, мы используем интерфейс и, тем самым, мы не зависим от конкретной реализации презентера. Это очень удобно и позволяет нам использовать любой из трех существующих классов презентеров в зависимости от режима.
Далее в onCreate мы даем созданному презентеру View (оно же Activity) и сообщаем, что все готово к работе.
Методом detachView мы просим presenter освободить View. А если Activity закрывается насовсем, то выполняем destroy для презентера и освобождаем компонент даггера.
Далее идет реализация методов интерфейса PinCodeContract.View
Ставим label текст в TextView и показываем его и само поле. Далее с помощью RxJava вешаем обработчика для поля ввода, который будет срабатывать при вводе символов в поле. Добавляем фильтр, чтобы обработчик срабатывал только, когда поле содержит 4 символа (т.е. полностью заполнено). И указываем, что обработчику необходимо будет вызвать метод presenter.onTextFirst().
Тем самым мы сообщим презентеру, что пользователь ввел 4 символа в поле и надо что-то делать дальше.
Методы showSecond и showThird полностью аналогичны.
Методы getTextFirst, getTextSecond и getTextThird просто возвращают содержимое полей ввода.
Методы focusFirst, focusSecond, focusThird ставят фокус в поля ввода.
clearAll очищает все поля ввода.
showMessage показывает Toast.
next запускает MainActivity.
close закрывает Activity.
PresenterДавайте смотреть код презентеров и там станет понятно, зачем нужны все эти методы в Activity.
Создание пин кодаОбратите внимание, что класс презентера наследует PresenterBase (чтобы иметь базовые методы работы с View) и реализует PinCodeContract.Presenter (чтобы соответствовать требованиям контракта).
Этот презентер используется, когда мы открываем экран в режиме создания пин кода
Нам нужно отобразить два поля из трех. Это мы и делаем в методе viewIsReady. Для доступа к View используем getView из базового класса презентера.
Метод onTextFirst будет вызван из View, когда первое поле будет заполнено. В этом случае мы переводим фокус на второе поле методом focusSecond.
Метод onTextSecond будет вызван из View, когда второе поле будет заполнено. В этом случае мы сверяем значения из первого и второго полей.
Если значения равны, то мы - сохраняем новый пин- показываем сообщение, что пин успешно создан- открываем следующий экран- закрываем текущий экран
Если же значения не равны- показываем сообщение о том, что пины не совпадают- очищаем все поля- ставим фокус в первое поле
В методе onTextThird ничего не делаем. Он не будет вызван, т.к. мы не отображали третье поле ввода.
Проверка пин кодаОн используется при режиме проверки пин кода
В этом случае нам необходимо показать всего одно поле, что мы и делаем в viewIsReady.
Когда пользователь введет пин код в первое поле, будет вызван метод onTextFirst. В нем проверяем, что введенное значение совпадает с пин кодом который хранится в префах.
Если совпадает, то:- открываем следующий экран- закрываем текущий экран
Иначе:- показываем сообщение об ошибке- очищаем все поля
Методы onTextSecond и onTextThird остаются пустыми. Они не будут вызваны из View, т.к. мы не отображали второе и третье поля.
Изменение пин кодаИспользуется в режиме смены пин кода.
Здесь мы отображаем все три поля в методе viewIsReady.
В методах onTextFirst и onTextSecond просто переводим фокус на следующее поле.
В методе onTextThird сначала проверяем, что старый пин введен верно в первое поле. Если пин не совпадает с текущим сохраненным, то - показываем сообщение об ошибке- очищаем все поля- ставим фокус на первое поле- заканчиваем работу метода
Если же старый пин был указан правильно, то проверяем, что во втором и третьем поле было введено одинаковое значение нового пина.
Если значения совпадают, то- сохраняем новый пин- показываем сообщение об успехе- закрываем экран
Иначе- показываем сообщение об ошибке- очищаем все поля- ставим курсор на первое поле
Если забыть про MVP и поместить всю логику View и трех презентеров в одно Activity, то оно получится достаточно сложным и перегруженным. Вариант с MVP кажется мне более предпочтительным.
Пример был сделан максимально легким и, возможно, не выполняет все необходимые проверки. Основная цель была - показать, как взаимодействуют View и Presenter в MVP.
Также я хотел бы заметить, что в данном примере, при поворотах экрана ваш презентер будет продолжать существовать, и не будет пересоздаваться. Это может быть удобно, когда вы выполняете какие-то долгие операции, например - запросы на сервер. Реализовано это с помощью Dagger 2, но можно и любым другим способом.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
- новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме