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

Таким образом, экран пин кода должен поддерживать три режима: создание, проверка и смена пин кода. Реализовав этот экран, я понял, что получился неплохой пример для демонстрации принципов MVP (Model-View-Presenter).

 

Если вы пока не очень хорошо представляете себе, что такое MVP, то посмотрите сначала этот пост. Он проще и понятнее.

 

 

 

Пример

Напомню, что у экрана с пин кодом может быть три режима.

 

Создание

 

 

Проверка

 

 

Изменение

 

Соответственно, на экране может быть одно, два или три поля ввода. Каждое поле содержит подпись (label) и имеет ограничение по длине в 4 символа.

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

Можно, конечно, было все три режима реализовать в одном презентере, но зачем? Это будет сложно и запутанно. А раз уж я использую MVP, я могу просто разделить логику на три презентера и использовать их для работы с одним экраном.

Пример я оформил отдельным проектом и залил на GitHub. В проекте используются ButterKnife, Dagger 2 и RxJava. Я не буду подробно останавливаться на объяснении принципов их работы. Если вы еще не знакомы с этими инструментами, то я вам рекомендую изучить их. Они сейчас востребованы и активно используются.

 

 

 

Основные компоненты приложения.

 

Preferences.java:

public class Preferences {

    final static String FILE_NAME = "preferences";

    final static String PREF_PIN = "pin";

    private SharedPreferences preferences;

    public Preferences(Context context) {
        preferences = context.getSharedPreferences(FILE_NAME, 0);
    }

    private SharedPreferences.Editor getEditor() {
        return preferences.edit();
    }

    public void setPin(String data) {
        getEditor().putString(PREF_PIN, data).commit();
    }

    public String getPin() {
        return preferences.getString(PREF_PIN, "");
    }
}

Класс по работе с префами. Используется для чтения/записи пин кода.

 

 

Constants.java:

public class Constants {

    public enum PinCodeMode {
        CREATE, CHECK, CHANGE;
    }

    public static final String EXTRA_MODE = "mode";

}

Здесь описаны Enum для режимов экрана и текстовый ключ, для помещения данных в intent.

 


StartActivity.java:

public class StartActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Preferences preferences = App.getApp(this).getComponentsHolder().getAppComponent().getPreferences();

        String pin = preferences.getPin();

        // check current PIN
        if (TextUtils.isEmpty(pin)) {
            PinCodeActivity.createPinCode(this);
        } else {
            PinCodeActivity.checkPinCode(this);
        }
        finish();
    }
}

Это техническое Activity, без layout. С него стартует приложение.

Из префов читается текущий пин код. Если он задан, то запускается PinCodeActivity в режиме проверки пин кода, иначе - в режиме создания пин кода. И на этом работа Activity завершается.
Если у вас есть Activity с заставкой, можно эту логику поместить в него.

 

 

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.change_pin)
    void onChangePinClick() {
        PinCodeActivity.changePinCode(this);
    }

    @OnClick(R.id.reset_pin)
    void onResetPinClick() {
        App.getApp(this).getComponentsHolder().getAppComponent().getPreferences().setPin("");
    }
}

Это основной экран приложения.

Содержит кнопки для сброса и изменения пина.

 

 

 

 

MVP

Рассмотрим классы/интерфейсы, принимающие непосредственное участие в MVP.

  

Базовые классы и интерфейсы MVP

 

MvpView.java:

public interface MvpView {
}

Интерфейс, который будет реализован всеми View, которые будут работать по MVP. В нашем случае интерфейс пустой.

 

 

MvpPresenter.java:

public interface MvpPresenter<V extends MvpView> {

    void attachView(V mvpView);

    void viewIsReady();

    void detachView();

    void destroy();
}

Интерфейс, который будет реализован всеми презентерами, которые будут работать по MVP. Он содержит несколько методов, которые будут вызываться из View.

attachView(V mvpView) - метод для передачи View презентеру. Т.е. View вызовет его и передаст туда себя.

viewIsReady - сигнал презентеру о том, что View готово к работе. Презентер может начинать, например, загружать данные.

detachView - презентер должен отпустить View. Вызывается, например, при повороте экрана, когда уничтожается старый экземпляр Activity, или при закрытии Activity. Презентер должен обнулить ссылку на Activity.

destroy - сигнал презентеру о том, что View завершает свою работу и будет закрыто. Т.е. Здесь необходимо отписываться от всех моделей, завершать все текущие операции и т.п.

 

 

PresenterBase.java:

public abstract class PresenterBase<T extends MvpView> implements MvpPresenter<T> {

    private T view;

    @Override
    public void attachView(T mvpView) {
        view = mvpView;
    }

    @Override
    public void detachView() {
        view = null;
    }

    public T getView() {
        return view;
    }

    protected boolean isViewAttached() {
        return view != null;
    }

    @Override
    public void destroy() {

    }
}

Небольшой базовый класс, который будет наследоваться всеми презентерами. В нем реализованы общие методы по работе с View. В этом примере он достаточно прост. Но обычно здесь располагаются также механизмы для сбора RxJava подписок, которые завершаются в методе destroy.

 

Переходим к реализации MVP на примере экрана для работы с пин кодами.

 

 

Contract

Плюс MVP в том, что вы легко можете заменить View или Presenter, и вся схема продолжит работать. Чтобы иметь возможность легко заменить один класс другим, необходимо использовать интерфейсы. Вы создаете интерфейс для View и интерфейс для Presenter, и далее в работе используете именно их. В этом случае, вы всегда сможете подменить одну реализацию презентера (или экрана) другой без каких-либо последствий.

 

PinCodeContract.java:

public interface PinCodeContract {

    interface View extends MvpView {

        // show field with label
        void showFirst(int labelResId);
        void showSecond(int labelResId);
        void showThird(int labelResId);

        // get text from field
        String getTextFirst();
        String getTextSecond();
        String getTextThird();

        // set focus on field
        void focusFirst();
        void focusSecond();
        void focusThird();

        // clear all fields
        void clearAll();

        // show message to user
        void showMessage(int messageResId);

        // go to next screen
        void next();

        // close screen
        void close();
    }


    interface Presenter extends MvpPresenter<View> {

        // field is filled
        void onTextFirst();
        void onTextSecond();
        void onTextThird();
    }


}

В этом файле описаны интерфейсы для View и Presenter для экрана, который будет работать с пин кодом.

Полей для ввода пин кода будет всего три, поэтому названия методов содержат порядковые числительные: first, second, third.

В интерфейсе View описаны методы в основном для работы с полями ввода: отображение, получение текста, установка фокуса, очистка. А также методы для показа сообщений пользователю, запуска следующего экрана и закрытия экрана.

В интерфейсе презентера описаны методы, которые будут вызываться, когда соответствующее поле будет полностью заполнено (когда пользователь ввел 4 символа).

 

View

Класс PinCodeActivity.java реализует интерфейс PinCodeContract.View, т.к. это Activity играет роль View в MVP схеме.

Рассмотрим некоторые фрагменты кода.

 

 

Методы запуска Activity:

    public static void createPinCode(Context context) {
        startActivity(context, Constants.PinCodeMode.CREATE);
    }

    public static void checkPinCode(Context context) {
        startActivity(context, Constants.PinCodeMode.CHECK);
    }

    public static void changePinCode(Context context) {
        startActivity(context, Constants.PinCodeMode.CHANGE);
    }

    private static void startActivity(Context context, Constants.PinCodeMode pinCodeMode) {
        Intent intent = new Intent(context, PinCodeActivity.class);
        intent.putExtra(Constants.EXTRA_MODE, pinCodeMode);
        context.startActivity(intent);
    }

Общий метод startActivity создает Intent для запуска PinCodeActivity и помещает в интент параметр - режим запуска. Методы createPinCode, checkPinCode, changePinCode используют метод startActivity и соответствующий режим.

 

 

Метод onCreate:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.pincode_activity);
        initView();

        // extract PIN code screen mode from intent
        Constants.PinCodeMode pinCodeMode = (Constants.PinCodeMode)
                getIntent().getSerializableExtra(Constants.EXTRA_MODE);

        // inject activity
        App.getApp(this)
                .getComponentsHolder()
                .getActivityComponent(getClass(), new PinCodeActivityModule(pinCodeMode))
                .inject(this);

        // attach view to presenter
        presenter.attachView(this);

        // view is ready to work
        presenter.viewIsReady();
    }

В onCreate вытаскиваем режим PinCodeMode из интента и используем его для создания презентера.

Для создания презентера тут используется даггер. И код, который он выполняет, выглядит так:

PinCodeActivityModule.java:

    PinCodeContract.Presenter providePinCodePresenter(Preferences preferences) {
        switch (pinCodeMode) {
            case CREATE:
                return new PinCodeCreatePresenter(preferences);
            case CHANGE:
                return new PinCodeChangePresenter(preferences);
            case CHECK:
                return new PinCodeCheckPresenter(preferences);
            default:
                return null;
        }
    }

В зависимости от режима, мы создаем один из трех презентеров. Это три разных класса, но все они реализуют интерфейс PinCodeContract.Presenter.

А в PinCodeActivity даггер помещает созданный презентер в поле:

PinCodeContract.Presenter presenter;

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

 

Далее в onCreate мы даем созданному презентеру View (оно же Activity) и сообщаем, что все готово к работе.

 

 

Метод onDestroy:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        presenter.detachView();
        if (isFinishing()) {
            presenter.destroy();
            App.getApp(this).getComponentsHolder().releaseActivityComponent(getClass());
        }
    }

Методом detachView мы просим presenter освободить View. А если Activity закрывается насовсем, то выполняем destroy для презентера и освобождаем компонент даггера.

 

 

Далее идет реализация методов интерфейса PinCodeContract.View

 

Метод showFirst:

    @Override
    public void showFirst(int labelResId) {
        // set text to label and show it
        textViewFirstLabel.setText(labelResId);
        textViewFirstLabel.setVisibility(View.VISIBLE);

        // show field
        editTextFirstValue.setVisibility(View.VISIBLE);

        // set textChange listener
        RxTextView.afterTextChangeEvents(editTextFirstValue)
                .skipInitialValue()
                .filter(new Predicate<TextViewAfterTextChangeEvent>() {
                    @Override
                    public boolean test(@NonNull TextViewAfterTextChangeEvent textViewAfterTextChangeEvent) throws Exception {
                        return (textViewAfterTextChangeEvent.editable().toString().length() == 4);
                    }
                })
                .subscribe(new Consumer<TextViewAfterTextChangeEvent>() {
                    @Override
                    public void accept(@NonNull TextViewAfterTextChangeEvent textViewAfterTextChangeEvent) throws Exception {
                        presenter.onTextFirst();
                    }
                });
    }

Ставим label текст в TextView и показываем его и само поле. Далее с помощью RxJava вешаем обработчика для поля ввода, который будет срабатывать при вводе символов в поле. Добавляем фильтр, чтобы обработчик срабатывал только, когда поле содержит 4 символа (т.е. полностью заполнено). И указываем, что обработчику необходимо будет вызвать метод presenter.onTextFirst().

Тем самым мы сообщим презентеру, что пользователь ввел 4 символа в поле и надо что-то делать дальше.

Методы showSecond и showThird полностью аналогичны.

 

Методы getTextFirst, getTextSecond и getTextThird просто возвращают содержимое полей ввода.

 

Методы focusFirst, focusSecond, focusThird ставят фокус в поля ввода.

 

clearAll очищает все поля ввода.

 

showMessage показывает Toast.

 

next запускает MainActivity.

 

close закрывает Activity.

 

 

 

Presenter

Давайте смотреть код презентеров и там станет понятно, зачем нужны все эти методы в Activity.

 

 

Создание пин кода

PinCodeCreatePresenter.java:

public class PinCodeCreatePresenter extends PresenterBase<PinCodeContract.View> implements PinCodeContract.Presenter {

    private final Preferences preferences;

    public PinCodeCreatePresenter(Preferences preferences) {
        this.preferences = preferences;
    }


    @Override
    public void viewIsReady() {
        getView().showFirst(R.string.create_new_pin);
        getView().showSecond(R.string.repeat_new_pin);
    }

    @Override
    public void onTextFirst() {
        getView().focusSecond();
    }

    @Override
    public void onTextSecond() {
        if (getView().getTextFirst().equals(getView().getTextSecond())) {
            preferences.setPin(getView().getTextFirst());
            getView().showMessage(R.string.pin_created);
            getView().next();
            getView().close();
        } else {
            getView().showMessage(R.string.no_match);
            getView().clearAll();
            getView().focusFirst();
        }
    }

    @Override
    public void onTextThird() {
        // do nothing
    }
}

Обратите внимание, что класс презентера наследует PresenterBase (чтобы иметь базовые методы работы с View) и реализует PinCodeContract.Presenter (чтобы соответствовать требованиям контракта).

 

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

Нам нужно отобразить два поля из трех. Это мы и делаем в методе viewIsReady. Для доступа к View используем getView из базового класса презентера.

Метод onTextFirst будет вызван из View, когда первое поле будет заполнено. В этом случае мы переводим фокус на второе поле методом focusSecond.

Метод onTextSecond будет вызван из View, когда второе поле будет заполнено. В этом случае мы сверяем значения из первого и второго полей.

Если значения равны, то мы
- сохраняем новый пин
- показываем сообщение, что пин успешно создан
- открываем следующий экран
- закрываем текущий экран

Если же значения не равны
- показываем сообщение о том, что пины не совпадают
- очищаем все поля
- ставим фокус в первое поле


В методе onTextThird ничего не делаем. Он не будет вызван, т.к. мы не отображали третье поле ввода.

 

 

Проверка пин кода

PinCodeCheckPresenter.java:

public class PinCodeCheckPresenter extends PresenterBase<PinCodeContract.View> implements PinCodeContract.Presenter {

    private final Preferences preferences;

    public PinCodeCheckPresenter(Preferences preferences) {
        this.preferences = preferences;
    }

    @Override
    public void viewIsReady() {
        getView().showFirst(R.string.enter_pin);
    }

    @Override
    public void onTextFirst() {
        if (getView().getTextFirst().equals(preferences.getPin())) {
            getView().next();
            getView().close();
        } else {
            getView().showMessage(R.string.wrong_pin);
            getView().clearAll();
        }
    }

    @Override
    public void onTextSecond() {
        //do nothing
    }

    @Override
    public void onTextThird() {
        //do nothing
    }
}

 

 

Он используется при режиме проверки пин кода

В этом случае нам необходимо показать всего одно поле, что мы и делаем в viewIsReady.

Когда пользователь введет пин код в первое поле, будет вызван метод onTextFirst. В нем проверяем, что введенное значение совпадает с пин кодом который хранится в префах.

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

Иначе:
- показываем сообщение об ошибке
- очищаем все поля

Методы onTextSecond и onTextThird остаются пустыми. Они не будут вызваны из View, т.к. мы не отображали второе и третье поля.

 

 

Изменение пин кода

PinCodeChangePresenter.java:

public class PinCodeChangePresenter extends PresenterBase<PinCodeContract.View> implements PinCodeContract.Presenter {

    private final Preferences preferences;

    public PinCodeChangePresenter(Preferences preferences) {
        this.preferences = preferences;
    }

    @Override
    public void viewIsReady() {
        getView().showFirst(R.string.enter_old_pin);
        getView().showSecond(R.string.create_new_pin);
        getView().showThird(R.string.repeat_new_pin);
    }

    @Override
    public void onTextFirst() {
        getView().focusSecond();
    }

    @Override
    public void onTextSecond() {
        getView().focusThird();
    }

    @Override
    public void onTextThird() {
        if (!getView().getTextFirst().equals(preferences.getPin())) {
            getView().showMessage(R.string.wrong_pin);
            getView().clearAll();
            getView().focusFirst();
            return;
        }

        if (getView().getTextSecond().equals(getView().getTextThird())) {
            preferences.setPin(getView().getTextSecond());
            getView().showMessage(R.string.pin_changed);
            getView().close();
        } else {
            getView().showMessage(R.string.no_match);
            getView().clearAll();
            getView().focusFirst();
        }
    }
}

 

Используется в режиме смены пин кода.

Здесь мы отображаем все три поля в методе viewIsReady.

В методах onTextFirst и onTextSecond просто переводим фокус на следующее поле.

В методе onTextThird сначала проверяем, что старый пин введен верно в первое поле. Если пин не совпадает с текущим сохраненным, то
- показываем сообщение об ошибке
- очищаем все поля
- ставим фокус на первое поле
- заканчиваем работу метода

Если же старый пин был указан правильно, то проверяем, что во втором и третьем поле было введено одинаковое значение нового пина.

Если значения совпадают, то
- сохраняем новый пин
- показываем сообщение об успехе
- закрываем экран

Иначе
- показываем сообщение об ошибке
- очищаем все поля
- ставим курсор на первое поле

 

 

 

Если забыть про MVP и поместить всю логику View и трех презентеров в одно Activity, то оно получится достаточно сложным и перегруженным. Вариант с MVP кажется мне более предпочтительным.

Пример был сделан максимально легким и, возможно, не выполняет все необходимые проверки. Основная цель была - показать, как взаимодействуют View и Presenter в MVP.

Также я хотел бы заметить, что в данном примере, при поворотах экрана ваш презентер будет продолжать существовать, и не будет пересоздаваться. Это может быть удобно, когда вы выполняете какие-то долгие операции, например - запросы на сервер. Реализовано это с помощью Dagger 2, но можно и любым другим способом.


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal