12 March 2015

Android. Пару слов об MVP + rxJava

Development for Android
Sandbox


Работая с 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 вместе с ним. Однако одновременное их использование дает приемлемые результаты в упрощении структуры поддерживаемого приложения.
Tags:androidmvprxjava
Hubs: Development for Android
+22
73.4k 228
Comments 20
Popular right now
Top of the last 24 hours