Пользователь
0,0
рейтинг
12 марта 2015 в 16:33

Разработка → Android. Пару слов об MVP + rxJava из песочницы



Работая с Android часто можно видеть, как весь функциональный код помещается в методы жизненного цикла activity/fragment. В общем-то такой подход имеет некоторое обоснование — «методы жизненного цикла» всего лишь хэндлеры, обрабатывающие этапы создания компонента системой и специально предназначенные для наполнения их кодом. Добавив сюда то, что каркас UI описывается через xml файлы, мы уже получаем базовое разделение логики и интерфейса. Однако из-за не совсем «изящной» структуры жизненного цикла, его зависимости от множества флагов запуска, и различной (хоть и похожей) структуры для разных компонентов, эффективно воспользоваться подобным разделением не всегда бывает возможно, что в итоге выливается в написании всего кода в onCreate().

Model-View-Presenter+rxJava


MVP паттерн разработки для android, предлагающий разбивать приложение на следующие части:

  1. Model — представляет из себя точку входа к данным приложения (часто на каждый экран своя модель). При этом особой разницы откуда данные быть не должно — данные сетевых запросов или данные взаимодействия пользователя с UI (клики, свайпы и т.д). Хорошее место для внедрения «рукописных» кэшей. В связке с rxJava будет представлять из себя набор методов, отдающих Observable.
  2. View — представляет из себя класс, устанавливающий состояние UI элементов. Не путать термин с android.view.View
  3. Presenter — устанавливает связь между обработкой данных, получаемых из Model и вызовом методов у View, реализуя тем самым реакцию UI компонентов на на данные. Методы Presenter вызываются из методов жизненного цикла activity/fragment и часто «симметричны» им.

Model/View/Presenter должны представлять из себя интерфейсы для большей гибкости модификации кода.

Пример


Рассмотрим пример приложения, состоящего из одного экрана, на котором находится EditText и TextView. При этом по мере редактирования текста в EditText отправляются сетевые запросы, результат которых должен отображаться в TextView (конкретика запроса не должна нас волновать, это может быть перевод, краткая справка по термину или что то подобное).

ExampleModel.java:

public interface ExampleModel {  
    Observable<String> changeText();  
    Observable<String> request(String query);  
}  

ExampleView.java:

public interface ExampleView {  
    void showResponse(String result);  
}  

ExamplePresenter.java:

public interface ExamplePresenter {  
    void onCreate(Activity activity, Bundle savedInstanceState);
}  

Реализация


Пояснения
Так как в комментариях было несколько серьезных замечания, думаю разумным внести некоторые примечания:
  • По поводу терминологии: в статье используется та же терминология, что и в достаточно по популярной статье про mvp для android-а от Antonio Leiva, здесь можно ознакомится с моим, совсем уж любительским переводом.
  • Некоторых смутило помещение на уровень Model-и метода, отдающего Observable, который отвечает за события, связанные с действиями пользователя (что приводит к необходимости в реализации Model-и содержать объект, связанный android view-шками), но я все же считаю это корректным и даже удобным, так как позволяет рассуждать о событиях редактирование текста, для которого требуется обработка, именно, как о потоке данных, которые лишь в конкретной реализации связаны с UI-ем. Если вас принципиально не устраивает это, то можете рассмотреть этот совсем уреазный пример т.е просто часть методов перейдет из Model в View. Так же можете рассмотреть пример из статье Antonio Leiva, связь компонентов там такая M<=P<=>V


Так как Model и View используют одни и тебе виджеты (в нашем случае EditText и TextView) для своей работы, разумно будет реализовать содержащий их класс.

ExampleViewHolder.java:

public class ExampleViewHolder {  
    public final EditText editText;  
    public final TextView textView;  

    public ExampleViewHolder(EditText editText, TextView textView) {  
        this.editText = editText;  
        this.textView = textView;  
    }  
}  

При реализации Model мы предполагаем использование rxAndroid, для «оборачивания» EditTetx, и retrofit для реализации сетевых запросов.

ExampleModelImpl.java:

public class ExampleModelImpl implements ExampleModel {  
    private final ExampleViewHolder viewHolder;  

     public ExampleModelImpl(final ExampleViewHolder viewHolder) {  
        this.viewHolder = viewHolder;  
    }

    @Override  
    public Observable<String> changeText() {  
        return WidgetObservable  
            .text(viewHolder.editText)  
            .map(new Func1<OnTextChangeEvent, String>() {  
                @Override  
                public String call(OnTextChangeEvent event) {  
                    return event.toString().trim();  
                }  
            });  
    }  

    @Override  
    public Observable<String> request(String query) {
        //всю работу берет на себя retrofit  
        return RestManager.newInstance().request(query); 
    }  
} 


ExampleViewImpl.java:

public class ExampleViewImpl implements ExampleView {  
    private final ExampleViewHolder viewHolder;  
       
    public ExampleViewImpl(final ExampleViewHolder viewHolder) {  
        this.viewHolder = viewHolder;  
    }  
   
    @Override  
    public void showResponse(final String result) {  
        viewHolder.textView.setText(result);  
    }  
} 


Так как количество сетевых запросов зависит от скорости набора текста (а она может быть достаточно высока), существует естественное желание ограничить частоту событий редактирование текста в EditText. В данном случае это реализуется оператором debounce (при этом, естественно, ввод текста не блокируется, а лишь пропускается часть событий редактирования, произошедших в временной промежуток в 150 миллисекунд).

ExamplePresenterImpl.java:

public class ExamplePresenterImpl implements ExamplePresenter {  
    private final ExampleModel model;  
    private final ExampleView view;  
    private Subscription subscription;  

    public ExamplePresenterImpl(ExampleModel model, ExampleView view) {  
        this.model = model;  
        this.view = view;  
    }
  
    @Override  
    public void onCreate(Activity activity, Bundle savedInstanceState) {  
        subscription = model  
            .changeText()  
            //ограничивает частоту событий
            .debounce(150, TimeUnit.MILLISECONDS)  
            .switchMap(new Func1<String, Observable<String>>() {  
                @Override  
                 public Observable<String> call(String query) {  
                     return model.request(query);  
                 }  
            })
           .observeOn(AndroidSchedulers.mainThread())
           .subscribe(new Action1<String>() {  
               @Override  
               public void call(String result) {  
                   view.showResponse(result);  
               }     
           });  
    }
    
    @Override  
    public void onDestroy() {  
        if (subscription != null) {  
            subscription.unsubscribe();  
         }  
    }  
}  


Реализация activity, передающая всю сущностную часть работы Presenter:

ExampleActivity.java
public class ExampleActivity extends Activity {  
    private ExamplePresenter examplePresenter;  

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

        final ExampleViewHolder exampleViewHolder = new ExampleViewHolder(
            (TextView) findViewById(R.id.text_view),  
            (EditText) findViewById(R.id.edit_text)  
        );  

        final ExampleModel exampleModel 
            = new ExampleModelImpl(exampleViewHolder);  

        final ExampleView exampleView 
            = new ExampleViewImpl(exampleViewHolder);  

        examplePresenter 
          = new ExamplePresenterImpl(exampleModel, exampleView);  
       
        examplePresenter.onCreate(this, savedInstanceState);  
    }  

    @Override  
    protected void onDestroy() {  
        super.onDestroy();  
        examplePresenter.onDestroy();  
    }  
}

Заключение


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

  • Обработка отсутствие сети — решается на уровне Model-и и View.
    Кэширование результатов запросов решается на уровне Model (можно на уровне retrofit, путем настройки okhttp.Cache или HttpResponsecache — в зависимости от того, что используется).
  • Общая обработка ошибок решается на уровне Presenter добавлением обработчика ошибок при subscribe.
  • Логирование решается в зависимости от того, что надо логировать.
  • Создание более сложного UI, возможно анимации — нужно модифицировать ViewHolder, View.

Эпилог


MVP — не единственный способ разбиения Android-приложения на компоненты, и уж тем более он не предполагает обязательного использования rxJava вместе с ним. Однако одновременное их использование дает приемлемые результаты в упрощении структуры поддерживаемого приложения.
Никита @7voprosov
карма
11,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (20)

  • 0
    Я уж думал, что в русскоязычном сообществе Android разработчиков эта тема никого не интересует. Статья хорошая, но два небольших момента:

    • debounce — это не директива, а оператор, в терминологии Rx
    • Введённые интерфейсы в данном примере — интерфейсы ради интерфейсов, никакого смысла вводить их без нескольких реализаций нет, а рефакторинг в IDE позволяет легко сделать это при необходимости


    Пишите ещё :)
    • +1
      Ну, если так судить, то почему тогда Вас не смущает самое наличие MVP в примере без тестов?) Абстракция ради абстракции ведь! Подумаешь, тот же код из onCreate, но раскидали по классам с модными названиями, зачем пыжились? Нет, если я правильно думаю и автор не угомонит свои таланты, то нас ждут логичные продолжения с DI и тестами, где как раз и заиграют красками интерфейсы. По крайней мере, я очень на это надеюсь, потому что начало хорошее, а пробуя писать тесты для Rx в последний раз я чувствовал боль.
      • +1
        Ну как бы, ничего не мешает писать тесты без интерфейсов, конечно, если вы не будете объявлять все методы как final, если говорить о юнит тестах с моками (mockito, например).

        Логичные продолжения с DI, да, было бы не плохо просветить людей, особенно, если это будет Dagger 2.

        Тесты с Rx пишутся на раз два, гораздо удобнее, чем для коллбек-ориентированной асинхронщины, т.к. вы можете выполнить Observable в текущем потоке — toBlocking().

        Ваш стиль наезда меня позабавил, спасибо :)
  • 0
    Когда-то однажды наткнулся на интересную библиотеку github.com/ogaclejapan/RxBinding
  • +1
    А какие-то чуть более примитивные примеры использования rx в Android может кто-нибудь привести? Я догадываюсь, что в интерфейсе Idea без этого явно не обошлось. Но большинство задач ui: ввести что-то -> отправить на сервер и загрузить что-то -> отобразить. Зачем rxJava в подобных задачах?
    • 0
      Вы можете решать эти задачи, используя RxJava, будет элегантно (особенно с лямбдами) и дополняемо/изменяемо.
      • 0
        А в чем соль? Там нет каких-либо зависимостей, как правило, и/или пересчетов. Не очень понимаю зачем.
        • +1
          Например, вы хотите преобразовать результат запроса в нечто другое, или объединить один асинхронный запрос с другим, для таких задач RxJava очень хороша.
    • +2
      Задача работы с сетью в рамках android-а предполагает наличие отдельного патока и его правильного старта, учитывающая повороты, закрытие экрана. Далее идут задачи по обработке отсутствие сети (как, в общем-то, было указано в статье) и т.д

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

  • 0
    Так как Model и View используют одни и тебе виджеты (в нашем случае EditText и TextView) для своей работы, разумно будет реализовать содержащий их класс.ExampleViewHolder.java:

    Недопустимо ссылаться на виджеты равно как на любую UI или Presenter специфик логику из модели, теряется весь смысл концепции MVP. В идеале части M V и P разделены на отдельные модули и имеют односторонние зависимости M <= P <= V.
    • 0
      В данном случаи ссылаются на одни и те же UI элементы конкретные реализации, поэтому действительно фраза не совсем верная, хотя дальше видно по примеру что имелось ввиду.

      По поводу разделение — оно делается, что бы представить программу в простом линейном виде: получение данных -> обработка данных -> отображение данных

      Так как UI элементы в данном случаи будут выступать и как источник данных и элементы для отображение, то ссылки естественно возникают в реализации Model и View, Presenter связывает Model с View.

      В итоге получаем схему:

      Presenter {
         Model model = ...
         View view = ...
         
         onCreate(...) {
             model.getData(...).flatMap(...).subscribe(view.show());   
         }
      }
      

      • +1
        Так как UI элементы в данном случаи будут выступать и как источник данных и элементы для отображение, то ссылки естественно возникают в реализации Model и View, Presenter связывает Model с View.


        Нет, это уже не MVP и с ним общего ничего не имеет, кроме названий классов. Что вы будете делать, когда вам понадобится сделать версию под планшет, где, предположим, из-за особенностей UI придётся использовать совсем другие виджеты? Т.е. я ставлю вас перед фактом, что надо поддерживать два набора View с разным набором виджетов. Как вы это организуете в вашем варианте?
  • 0
    Если мы говорим о действительно разном наборе виджетов, что чем вас не устраивает самый тривиальный вариант:
       Model model = isTable ? new TableModelImpl(...) : new PhoneModelImpl(...)
       View view = isTable ? new TableViewImpl(...) : new PhoneViewImpl(...)
    
       Presenter presenter = new Presenter(model, view);
    

    ?
    • 0
      Конечно, Presenter presenter = new PresenterIml(model, view);
      • 0
        DrVirtual прав, нельзя знать про особенности View в модели, вообще. Модель — это слой логики, который работает с данными (в вашей доменной области).

        Суть модели именно в том, что она может быть использована откуда угодно, а значит не должна знать специфики таких вещей, как View.

        Делать две версии модели из-за особенностей Ui — нарушение DRY и здравой логики.
        • 0
          Так в Model вы и не знаете про View ни чего. Две реализации только в том случаи если вы имеете два разных виджета т.е в первом случаи вы получаете текст запроса из edittext-а, а во втором из spinner-а, при этом интерфейс доступа к данным у вас один и тот же, а каждая реализация оборачивает свои UI элементы в Model, представляя общий интерфейс доступа к данным.
          • 0
            Ваше представление об MVP в корне неверно. Либо вы читали такие же некорректные статьи, либо некорректно интерпретировали. У вас даже на КДПВ нет ссылки от Model к View. Хотя и она неверна, т.к. связи должны быть односторонними, а в обратную сторону работа идёт через интерфейсы.
            Так в Model вы и не знаете про View ни чего

            Вы обманываете, т.к. эти ваши
            edittext-а, а во втором из spinner-а
            — это часть View.
            каждая реализация оборачивает свои UI элементы в Model, представляя общий интерфейс доступа к данным.

            Я не занимаюсь разработкой под Android, но в идеальном варианте ни Model, ни Presenter не имеют доступа ни то, что к View, но и к пакету android в принципе, т.е. они не знают, что работают под android'ом. И уж особенно нельзя нарушать правила разделения зависимостей между M, V и P — код в разных модулях и зависимости между ними настроены односторонне M<=P<=V, это принципиально.

            MVP это про распределение обязанностей и зависимостей, в вашем случае реализация примера должна иметь следующий вид:
            Пакет Model:
            класс с данными ExampleModel
            Пакет Presenter:
            интерфейс ExampleView с методом updateResponse(String response)
            класс ExamplePresenter'а с методами doRequest(String response)
            Пакет View:
            класс-реализация интерфейса ExampleView

            ExampleView дёргает ExamplePresenter.doRequest, ExamplePresenter устанавливает в ExampleModel данные запроса, и делает запрос на сервер, когда получает ответ, то устанавливает его в ExampleModel и дёргает метод интерфейса ExampleView.updateResponse.

            Также важно выделять весь View в отдельный пакет и обращаться только через интерфейсы, т.к. это позволит делать unit тесты, подменив реализации на тестовые.

            150 человек добавило эту статью в избранное — кто-то мог воспринять её серьёзно. Рекомендую удалить её пока, ознакомиться с темой более внимательно и написать действительно полезную статью. В качестве хорошей реализации MVP рекомендую ознакомиться с реализацией на www.mvcsharp.org (она на c#, но смысл от этого не меняется). Плюс там есть несколько статей на эту тему. Можете консультироваться, если будут вопросы.
            • 0
              Хорошо, тогда я проконсультируюсь у вас по поводу корректной реализации и внесу необходимые правки в статью.
            • 0
              Добавил пояснения по терминологии и с ссылкой на пример, вами провалидированный.

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