Работая с Android часто можно видеть, как весь функциональный код помещается в методы жизненного цикла activity/fragment. В общем-то такой подход имеет некоторое обоснование — «методы жизненного цикла» всего лишь хэндлеры, обрабатывающие этапы создания компонента системой и специально предназначенные для наполнения их кодом. Добавив сюда то, что каркас UI описывается через xml файлы, мы уже получаем базовое разделение логики и интерфейса. Однако из-за не совсем «изящной» структуры жизненного цикла, его зависимости от множества флагов запуска, и различной (хоть и похожей) структуры для разных компонентов, эффективно воспользоваться подобным разделением не всегда бывает возможно, что в итоге выливается в написании всего кода в onCreate().
Model-View-Presenter+rxJava
MVP паттерн разработки для android, предлагающий разбивать приложение на следующие части:
- Model — представляет из себя точку входа к данным приложения (часто на каждый экран своя модель). При этом особой разницы откуда данные быть не должно — данные сетевых запросов или данные взаимодействия пользователя с UI (клики, свайпы и т.д). Хорошее место для внедрения «рукописных» кэшей. В связке с rxJava будет представлять из себя набор методов, отдающих Observable.
- View — представляет из себя класс, устанавливающий состояние UI элементов. Не путать термин с android.view.View
- 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);
}
Реализация
Так как 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);
}
})
.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