Сажаем контроллеры на диету. Android
Паттерн MVС появился достаточно давно и создавался с целью разделения бизнес-логики приложения от представления. Но далеко не все программисты реализуют его правильно, из-за чего возникают «Толстые тупые уродливые контроллеры» содержащие тонны кода. В этой статье пойдет речь о правильной реализации View классов, для того чтобы уменьшить количество кода в контроллерах и оставить место чистой бизнес-логике приложения.Все, наверное, должны знать что 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
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