Все, наверное, должны знать что MVC бывает двух типов — с активной моделью и пассивной, различие которых кроется в том, что пассивная модель служит простым источником данных (как, например, DAO для базы данных), а активная модель сама обновляет состояние своих подписчиков — View. Пассивная модель является более универсальной и простой, кроме того чаще всего используется в разработке, поэтому она будет использоваться для примера в этой статье. Давайте взглянем на её схему.
Пользователь взаимодействует с контроллером, контроллер запрашивает данные у модели и заполняет View, который отображается пользователю, всё просто.
- При использовании MVC в Android, Activity или Fragment является контроллером.
- Модель — набор классов, которые служат источником данных приложения.
- View — xml разметка и кастомные View компоненты, на подобие Button и т. д.
Если с контроллером и моделью, вроде бы, всё понятно, то со View возникают некоторые трудности, главная их причина — View, как такого, нет, никто не задумывается о создании отдельных View классов с интерфейсом, через который контроллер мог бы передавать данные для отображения. Большинство просто создаёт xml разметку и заполняет её прямо в контроллере, из-за чего код, который по идее должен содержать бизнес-логику переполняется деталями отображения, такими как цвет текста, размер шрифта, установка текста в TextView, работа с ActionBar'ом, NavigatonDrawer'ом и прочими. В результате код Activity разрастается до 1000 строк и на первый взгляд содержит какой-то мусор.
Давайте взглянем на то, как делается типичное Android приложение без создания отдельных View классов, и на другое, в котором в полной мере используется View.
Наше приложение будет решать вполне распространенную задачу — загружать и отображать профайл пользователя. Начнем реализацию.
Для этого создадим модельный класс User, в котором будет храниться имя и фамилия пользователя.
public class User {
private final String firstname;
private final String lastname;
public User(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
// getters
}
И класс provider, который будет её «загружать». Этот класс создан для демонстрационных целей, в реальном проекте не следует использовать AsyncTask для загрузки данных и не стоит писать свой велосипед, который даже не учитывает жизненный цикл Activity и не обрабатывает ошибки, лучше использовать готовое решение, например, RoboSpice. Здесь этот класс нужен, по большей части, только для того, чтобы скрыть детали реализации загрузки данных в отдельном потоке.
public class UserProvider {
// результат вернем в Callback
public void loadUser(Callback callback) {
new LoadUserTask(callback).execute();
}
public class LoadUserTask extends AsyncTask<Void, Void, User> {
private Callback callback;
public LoadUserTask(Callback callback) {
this.callback = callback;
}
@Override
protected User doInBackground(Void... params) {
User user = new User("firstname", "lastname");
return user;
}
@Override
protected void onPostExecute(User user) {
super.onPostExecute(user);
callback.onUserLoaded(user);
}
}
public interface Callback {
void onUserLoaded(User user);
}
}
Далее создается xml верстка, которую мы опустим и контроллер, который должен связать View и Model, и внести немного бизнес-логики в наше приложение. В виде контроллера выступает Activity, обычно он реализуется примерно так:
public class UserProfileActivity extends Activity implements Callback {
private TextView firstnameTxt, lastnameTxt;
private ProgressBar progressBar;
private UserProvider userProvider;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_profile);
firstnameTxt = (TextView) findViewById(R.id.firstname);
lastnameTxt = (TextView) findViewById(R.id.lastname);
progressBar = (Progressbar) findViewById(R.id.progressBar);
userProvider = new UserProvider();
loadUser();
}
@Override
public void onUserLoaded(User user) {
hideProgressBar();
showUser(user);
}
private void loadUser() {
showProgressBar();
userProvider.loadUser(this);
}
public void showUser(User user) {
firstnameTxt.setText(user.getFirstname());
lastnameTxt.setText(user.getLastname());
}
public void showProgressBar() {
progressBar.setVisibility(View.VISIBLE);
}
public void hideProgressBar() {
progressBar.setVisibility(View.INVISIBLE);
}
}
При открытии экрана начинается загрузка профайла, отображается progress bar, когда профайл будет загружен, progress bar скрывается и происходит наполнение экрана данными.
Как видно из этого кода — в нём перемешивается работа с представлением и бизнес-логика.
Если сейчас все выглядит не так плохо, то при развитии проекта такой код станет плохочитаемым и трудноподдерживаемым.
Давайте вспомним про ООП и добавим немного абстракции в наш код.
public class UserView {
private final TextView firstnameTxt, lastnameTxt;
private final ProgressBar progressBar;
public UserView(View rootView) {
firstnameTxt = (TextView) rootView.findViewById(R.id.firstname);
lastnameTxt = (TextView) rootView.findViewById(R.id.lastname);
progressBar = (ProgressBar) rootView.findViewById(R.id.progressBar);
}
public void showUser(User user) {
firstnameTxt.setText(user.getFirstname());
lastnameTxt.setText(user.getLastname());
}
public void showProgressBar() {
progressBar.setVisibility(View.VISIBLE);
}
public void hideProgressBar() {
progressBar.setVisibility(View.INVISIBLE);
}
}
View берет на себя всю работу с представлением Activity. Для отображения профайла пользователя нужно просто воспользоваться методом showUser(User) и передать ему модельный объект. В реальном проекте для View желательно создать базовый класс, в который можно перенести вспомогательные методы, такие как showProgressBar(), hideProgressBar(), и другие. В результате вся логика работы с представлением вынесена из Activity в отдельную сущность, что в разы уменьшает объемы кода контроллера и создаёт прозрачную абстракцию работы с View.
Activity же теперь ничего не знает о TextView и других контролах. Все взаимодействие с представлением происходит с помощью класса UserView и его интерфейса.
public class UserProfileActivity extends Activity {
private UserView userView;
private UserProvider userProvider;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
userView = new UserView(getWindow().getDecorView())
userProvider = new UserProvider();
loadUser();
}
@Override
public void onUserLoaded(User user) {
userView.hideProgressBar();
userView.showUser(user);
}
private void loadUser() {
userView.showProgressBar();
userProvider.loadUser(this);
}
}
Теперь контроллер оперирует всего двумя сущностями — UserView и UserProvider, в нём нет тонкостей реализации отображения данных. Код стал чище и понятней.
Сейчас класс UserView просто отображает данные, возможно вы захотите сделать сохранение состояния между поворотами экранов — этот вопрос можно легко решить создав метод, записывающий состояние View в Parcelable или Bundle. Также, скорей всего, понадобится возможность обработки нажатий, в этом случае сам OnClickListener лучше создать во View классе и в него передать Callback, который реализует ваш контроллер.
Вот, собственно, и все. Так решается проблема недооценённых View в Android. При использовании этого подхода количество кода в ваших контроллерах заметно уменьшится, уровень абстракций возрастет и доллар опять будет стоить 30 рублей.
Читайте также:
Стилизация iOS-приложений: как мы натягиваем шрифты, цвета и изображения
Архитектурный дизайн мобильных приложений
Архитектурный дизайн мобильных приложений: часть 2
Комментарии (51)
Revertis
13.05.2015 19:03+1А кто сказал, что Activity является контроллером, а не View, например? Этот класс решает задачи и отображения и обработки событий. Почему вам надо обязательно что-то от него оторвать?
anton9088 Автор
13.05.2015 19:07+11. Если бы Activity был View тогда где контроллер?
2. Крутые ребята обычно отделяют бизнес-логику от представленияRevertis
13.05.2015 19:17+1А зачем тащить веб-привычки в мир мобильной разработки?
Я не говорю, что логика должна быть в Activity, но изначально Гуглом сделано так, что этот класс управляет и отображением и обработкой событий. Если логики много, то она перетекает туда, где данные хранятся (модель), а отображение пусть останется тут (в отдельных коротеньких методах).
sferrka
13.05.2015 19:50+2userView.hideProgressBar();
У вас контроллер практически напрямую управляет отображением, знает о прогресс-баре и видимо вообще обо всех UI-элементах?
Бизнес-логика — это изменение состояния моделей, а у вас контроллер, опять же, меняет отображение. Т.е. в одном месте и бизнес-логика и логика отображения, где разделение?
Суть MVC, как раз таки, в односторонней зависимости, а не в том, что контроллер — место встречи всех.
Отображение знает об интерфейсе контроллере (получение и установка параметров, например, через вызов методов), контроллер знает об интерфейсе модели (тоже самое), модель не знает о них ничего.anton9088 Автор
13.05.2015 19:57+1Да, контроллер знает о том, что на экране можно показывать и скрывать progress bar, но он не реализует это поведение. Так же контроллер ничего не знает о UI компонентах, он работает только с интерфейсом класса View.
sferrka
13.05.2015 20:12+21. Что такое «реализует поведение»? Это то чем занимается ОС, VM или драйвер видеокарты?
2. В чем отличие «userView.showProgressBar();» от «progressBar.setVisibility(View.VISIBLE);»? Дополнительный слой абстракции — это я поняла, но в чем различие вашего Activity от View, если оба управляют отображением? Так ведь можно сколько угодно слоев вводить, причем тут MVC?anton9088 Автор
13.05.2015 20:19+11. «Реализует поведение» значит скрыть детали создания и наполнения данными View
2. Отличие в том, что Activity говорит View классу, что нужно сменить состояние экрана или заполнить его данными, а View делает это, причем реализацию метода showProgressBar можно менять для разных View, например на одном экране progress bar нужно показывать в центре экрана, для других это поведение может быть различнымsferrka
13.05.2015 20:35+1habrahabr.ru/company/redmadrobot/blog/257861/#comment_8416179
В таком случае можно сделать метод более общим, например, showLoading, и реализовывать его по-разному
Вы не можете знать о «будущих» случаях по вашему же примеру, иначе это теряет смысл. И да showLoading — это правильное решение, теперь осталось вас убедить, что контроллер не должен вызывать этот метод у view, а должен иметь такое состояние.
1. «Реализует поведение» значит скрыть детали создания и наполнения данными View
Наверное, «не реализует». Однако, почему «создание и наполнение данными» — это поведение, а отображение/скрытие на экране — что-то другое, не ясно. Казалось бы, наоборот.
2. Отличие в том, что Activity говорит View классу, что нужно сменить состояние экрана или заполнить его данными, а View делает это, причем реализацию метода showProgressBar можно менять для разных View, например на одном экране progress bar нужно показывать в центре экрана, для других это поведение может быть различным
Теперь подумайте, ваша основная задача — отвязать класс контроллера от класса представления, чтобы контроллер можно было использовать с разными представлениями.
При этом вы упорно используете экземпляр view в классе контроллера. Т.е. противоречите себе же.
Класс вашего controller знает об интерфейсе конкретно этого класса view. Знает о его showProgressBar. Если в другом view вообще не будет showProgressBar? Если другое представление вообще никак не реализует отображение загрузки? И у него нет метода showLoading и любого подобного?
anton9088 Автор
13.05.2015 20:48+1Главная моя задача была не отвязать контроллер от представления, а уменьшить количество кода в контроллере, путем создания слоя View классов, с чем они довольно хорошо справляются. Если вы хотите использовать разные View для одного и того же контроллера, логично что у такой View должен быть интерфейс, который она должна реализовать. У другой View не может не быть метода showProgressBar, т.к. он будет в её интерфейсе, либо если сильно хочется не показывать пользователю информацию о процессе загрузки, то реализацию метода можно оставить пустой.
sferrka
13.05.2015 20:53+1Главная моя задача была не отвязать контроллер от представления, а уменьшить количество кода в контроллере, путем создания слоя View классов, с чем они довольно хорошо справляются.
С чем хорошо справляются, с вынесением части кода в другой класс? Ну да, только почему вы называете это View? Назовите это ControllerPartial, а лучше ActivityPartial, ниже есть пример про region.
Это все нужно городить для того чтобы работать с абстракциями (интерфейсами) и локализовать код, который чаще всего подвергается изменениям(код представления) в отдельном классе
У другой View не может не быть метода showProgressBar, т.к. он будет в её интерфейсе, либо если сильно хочется не показывать пользователю информацию о процессе загрузки, то реализацию метода можно оставить пустой.
Т.е. если мы хотим из этой view убрать отображение загрузки — мы просто должны оставить метод пустым, так?
Теперь представим, что у нас не было этого метода изначально во view. Что мы теперь будем делать? Правильно, менять код контроллера и дописывать туда showProgressBar. И так с каждым изменением отображения…anton9088 Автор
13.05.2015 20:59Само собой, если мы хотим убрать из View отображение progress bar'a, или установку данных с помощью showUser, то это коснется контроллера, но это будет изменение одной строчки. Если же мы захотим поменять способ отображения, то это коснется только View
sferrka
13.05.2015 21:11Если же мы захотим поменять способ отображения, то это коснется только View
Это просто вынесение части логики в другое место. Вы можете точно так же создать класс ProgressBarController и в Activity вызывать ProgressBarController.show(). Тогда при изменении способа отображения прогресс-бара — activity-класс не будет меняться.
Этот паттерн называется — декомпозиция, а не MVC.anton9088 Автор
13.05.2015 21:15Тогда MVC можно назвать декомпозицей кода на модельные классы, классы представления и контроллеры. О чем и речь в статье
sferrka
13.05.2015 21:19-1Тогда MVC можно назвать декомпозицей кода на модельные классы, классы представления и контроллеры. О чем и речь в статье
MVC — можно назвать декомпозицией, а то, о чем речь в статье — нет.anton9088 Автор
13.05.2015 21:44-2Ну как же, рассказывается о контроллере, о View, тут и MVC рядом.
sferrka
13.05.2015 22:07Ну если контроллер и view — это любые классы, без классификации их смысла, просто содержащие в названии (или в мыслях) «controller» и «view», то хорошо, вы описали MVC.
anton9088 Автор
14.05.2015 00:03+2Ок. Давайте введем некоторые термины, т.к. мы по-разному понимаем MVC
Контроллер — ловит события от пользователя (клики, свайпы и тд) и обрабатывает их, ничего не знает о том как отображать данные
View — содержит информацию как визуализировать данные, не содержит никакой логики обработки событий
Model — предоставляет данные
Что и представлено в статье. Вы хотели создать ProgressBarController, но я не могу его назвать контроллером, т.к. там нет взаимодействия с пользователем. Так же не могу создать ControllerPartial или ActivityPartial по той же причине. А вот View отличное название, т.к. все что делает этот класс это визуализирует данные и под описание контроллера никак не подходит.sferrka
14.05.2015 09:18+1Контроллер — ловит события от пользователя (клики, свайпы и тд) и обрабатывает их, ничего не знает о том как отображать данные
У вас контроллер прекрасно знает, что состояние loading — означает показать progressbar. Это именно как. Т.е., ваш контроллер знает из чего состоит View (в ней есть прогрессбар), а значит ваша MVC не несет никакой практической ценности, ведь отделения логики от представления нет. В одном месте хранятся знания о модели и о представлении. А так как единственная и самая главная ценность MVC именно в этом, то ваша архитектура — не MVC.
Вы хотели создать ProgressBarController, но я не могу его назвать контроллером, т.к. там нет взаимодействия с пользователем.
Ок, пусть будет TextBoxController с подпиской на onchange событие.
или ActivityPartial по той же причине
?
А вот View отличное название, т.к. все что делает этот класс это визуализирует данные и под описание контроллера никак не подходит.
View — отличное название, с этим ведь никто и не спорит, только внимание в 3 раз задаю вопрос. В чем смысл этого разделения? Я привела кучу примеров и везде требовалось изменить код вашего контроллера и view.
Вы написали, что смысл просто в вынесении части кода, но причем тут MVC?anton9088 Автор
14.05.2015 13:22Вы считаете что MVC — это обязательно MVC с активной моделью, в которой контроллер не связан с View? В статье показан MVC с пассивной моделью когда контроллер обращается к View для его перерисовки.
sferrka
14.05.2015 14:01Вы считаете что MVC — это обязательно MVC с активной моделью, в которой контроллер не связан с View?
M — модель, предметная область, бизнес-логика.
C — Промежуточное звено для уменьшения связаности.
V — Интерфейс для конечного клиента.
Собственно, весь смысл MVC (в отличии от других шаблонов) именно в контроллере и именно в уменьшении связанности.
Нет никаких правил, что именно должно быть в модели, контроллере и виде, правило одно — в этом должен быть практический смысл. А главный смысл — повторное использование и минимальные изменения частей кода при изменении тех. задания.
В статье показан MVC с пассивной моделью когда контроллер обращается к View для его перерисовки.
В статье показан пример, в котором при изменении модели нам придется менять и модель и контроллер. При изменении отображения, нам придется менять и отображение и контроллер.
Т.е. контроллер является ТТУКом, который вместо внедрения еще одного слоя для уменьшения связанности — увеличивает ее. В одном месте находится и бизнес-логика (изменение модели), и работа с интерфейсом (обработка событий), и отображение (showProgressBar).
Т.е. никакого разделения на MVC нет, все в одном классе. Зато есть вынесение части логики отображения в другой класс.
И да, пассивная модель — это когда мы модель(бизнес-логику) описываем в одном классе с контролером. Это уже тоже MVC с натяжкой, но все еще лучше, чем у вас — все три части в одном месте.
Divers
13.05.2015 19:26Тысячу раз уже обсуждали, что MVC и андроид не совместимы. Зачем гордить все это, если SDK нам предоставляет готовые компоненты приложения, которые по своей натуре малосвязны? Эта статья нормально смотрелась в 2009-2010 году, когда народ перешел из web программирования и тянул свои концепции, но сейчас-то уже обо всем догорились и пора перестать заниматься вот этим.
Bringoff
13.05.2015 19:38Сейчас работаю на улучшением архитектуры своих приложений. Не подскажете, что тогда юзать тру, если не MVC?
anton9088 Автор
13.05.2015 20:00+1Следующая статья будет по MVP. Но правильное использование MVC это уже лучше чем ничего
Divers
13.05.2015 20:27-1Используйте сервисы, контент провайдеры, бродкаст ресиверы и активити.
Bringoff
14.05.2015 10:29-1Не знал, что Кэп пишет под Android) Это понятно, но как это все красиво архитектурно оформить, вот в чем вопрос. Если не MVC.
Divers
14.05.2015 11:16-1Это и есть архитектура. Все это слабосвязные компноенты by design. MVC, как уже в 1000 раз выяснили в комментариях, в условиях Android SDKне возможен. Вот наглядный пример.
Bringoff
14.05.2015 12:50И чего сразу минусовать? Я ведь согласен, что mvc на дроиде реализовывать — это оверинжиниринг. Но «как не делать mvc правильно» тоже надо уметь. И ioschedule не лучший пример, по крайней мере, за 2014 год. Подождем 2015-го, вроде не за горами.
Divers
14.05.2015 13:14Почему не лучший пример? Инженеры из команды Android показывают, как они пишут приложения — что может быть лучше?
Минус скорей всего поставили за «Кэп пишет под Android».Bringoff
14.05.2015 13:23Была когда-то статья по этому поводу: habrahabr.ru/post/241139
Divers
14.05.2015 13:37Согласен, посмотрел на код и он явно плохой. Хотя в целом, «архитектура» похожа на ту, что обычно все используют.
kemsky
08.06.2015 18:17Это кстати наглядный пример убогости разработки под андроид, приложение имеет баги с восстановлением состояния и имеет столько кода внутри (под 100тыс строк xml+java), что страшно становится, а написано гуглом.
anton9088 Автор
13.05.2015 20:09+1Это все нужно городить для того чтобы работать с абстракциями (интерфейсами) и локализовать код, который чаще всего подвергается изменениям(код представления) в отдельном классе
sferrka
13.05.2015 20:18Теперь представьте, что вместо прогресс-бара мы хотим крутить spinner или просто писать слово загрузка. В вашем примере придется менять код контроллера и код представления.
anton9088 Автор
13.05.2015 20:22+1В таком случае можно сделать метод более общим, например, showLoading, и реализовывать его по-разному
Divers
13.05.2015 20:24+1У вас есть статистика какой код чаще меняется? Посмотрите исходники гугловых приложений — там нет никакого MVC.
Divers
13.05.2015 19:30-3UPD: я вижу вы какие-то стажировки проводите. Пожалуйста, не учите этому там!
r_ii
13.05.2015 20:33+1Идея хорошая, но на практике часто связь между активити и контролами слишком большая чтоб их разделять.
Я в таких случаях группирую код относящийся к разным частям с помощью регионов (//region в Android Studio).
Также отсутствие в Java ивентов и делегатов усложняет реализацию красивых интерфейсов.
Дополню пост этой древней статьей.
forceLain
14.05.2015 11:56+3Есть мнение, что MVC в андроиде уже есть: View — это xml-разметка, Controller — это активити, ну а Model остаётся вам на реализацию.
bejibx
18.05.2015 14:47Хочу также добавить что в Android разработке чаще используется разновидность MVC-паттерна — MVP (Model-View-Presenter).
Более подробно можно почитать о них например тут:
http://habrahabr.ru/post/215605/
http://antonioleiva.com/mvp-android/ (на английском)
http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/ (на английском)
anton9088 Автор
07.06.2015 18:38Гугл наконец-то занялся архитектурой сам и сделал MVVM)
developer.android.com/tools/data-binding/guide.html
kemsky
08.06.2015 18:11Используя androidannotations @ViewGroup/View можно вынести по крайней мере часть UI логики из активити в другой класс, но целиком избавится от UI кода в активити не получится, но по крайней можно сократить и упростить за счет аннотаций. Теперь может где-то пригодится и биндинг, но вполне может оказаться, что проще и понятнее написать геттер/сеттер во вью, чем пытаться потребить биндинг от гугла :)
Добиться какой-то более-менее похожей реализации MVC вряд ли получится из-за специфического апи андроида, когда все прибито к активити гвоздями и завязано на ее жизненный цикл.
js605451
Предложенный подход не будет работать на практике: подписка Activity на AsyncTask — это наверное самая популярная джуниорская ошибка. Приводит к падениям (или зависаниям, или вообще не понятно к чему — как повезёт) при попытках перевернуть девайс в процессе выполнения сравнительно долгих операций. А чтобы от этой ошибки избавиться, нужно изобразить что-то типа habrahabr.ru/post/240543. И для предложенной красоты места там скорее всего не останется.
anton9088 Автор
Предложенный подход заключается в реализации View классов, а не в использовании AsyncTask. В реальных проектах, конечно, его не нужно использовать для загрузки данных, тем более когда есть множество готовых решений — Robospice, Volley. На счет красоты не согласен, если с умом реализовать модельный слой и не раздувать код контроллера, то все будет ок.
js605451
Я тогда наверное не понял о чём статья.
1. Статья называется «Сажаем контроллеры на диету». Т.е. про контроллеры.
2. В начале статьи сказано: «При использовании MVC в Android, Activity или Fragment является контроллером». Т.е. я в первую очередь смотрю на код Activity, потому что сказано, что «статья про контроллеры, а контроллеры — это Activity».
Посмотрел — прокомментировал. Интересно было бы посмотреть, насколько чистый код получился бы, если бы вы канонично связь model->view делали через Loader, а controller->model через IntentService. У меня есть искренние сомнения по поводу того, что от предложенного подхода в итоге что-то останется.
anton9088 Автор
Через Loader'ы загрузку данных с сети тоже не делают)
А для IntentService достаточно сделать хороший уровень абстракции, всю работу с Intent'ами скрыть в отдельной сущности и, если потребуется, написать немного кода в BaseFragment или BaseActivity для автоматического подключения/отключения листенеров
vladlichonos
Почему Loader не использовать для сети?
jaleel
upd. ответили