Сажаем контроллеры на диету: Android

  • Tutorial
Паттерн 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<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
Метки:
Redmadrobot 96,62
№1 в разработке мобильных решений для бизнеса
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 52
  • 0
    Предложенный подход не будет работать на практике: подписка Activity на AsyncTask — это наверное самая популярная джуниорская ошибка. Приводит к падениям (или зависаниям, или вообще не понятно к чему — как повезёт) при попытках перевернуть девайс в процессе выполнения сравнительно долгих операций. А чтобы от этой ошибки избавиться, нужно изобразить что-то типа habrahabr.ru/post/240543. И для предложенной красоты места там скорее всего не останется.
    • +2
      Предложенный подход заключается в реализации View классов, а не в использовании AsyncTask. В реальных проектах, конечно, его не нужно использовать для загрузки данных, тем более когда есть множество готовых решений — Robospice, Volley. На счет красоты не согласен, если с умом реализовать модельный слой и не раздувать код контроллера, то все будет ок.
      • 0
        Я тогда наверное не понял о чём статья.
        1. Статья называется «Сажаем контроллеры на диету». Т.е. про контроллеры.
        2. В начале статьи сказано: «При использовании MVC в Android, Activity или Fragment является контроллером». Т.е. я в первую очередь смотрю на код Activity, потому что сказано, что «статья про контроллеры, а контроллеры — это Activity».

        Посмотрел — прокомментировал. Интересно было бы посмотреть, насколько чистый код получился бы, если бы вы канонично связь model->view делали через Loader, а controller->model через IntentService. У меня есть искренние сомнения по поводу того, что от предложенного подхода в итоге что-то останется.
        • +1
          Через Loader'ы загрузку данных с сети тоже не делают)
          А для IntentService достаточно сделать хороший уровень абстракции, всю работу с Intent'ами скрыть в отдельной сущности и, если потребуется, написать немного кода в BaseFragment или BaseActivity для автоматического подключения/отключения листенеров
          • 0
            Почему Loader не использовать для сети?
      • 0
        upd. ответили
      • +1
        А кто сказал, что Activity является контроллером, а не View, например? Этот класс решает задачи и отображения и обработки событий. Почему вам надо обязательно что-то от него оторвать?
        • +1
          1. Если бы Activity был View тогда где контроллер?
          2. Крутые ребята обычно отделяют бизнес-логику от представления
          • +1
            А зачем тащить веб-привычки в мир мобильной разработки?
            Я не говорю, что логика должна быть в Activity, но изначально Гуглом сделано так, что этот класс управляет и отображением и обработкой событий. Если логики много, то она перетекает туда, где данные хранятся (модель), а отображение пусть останется тут (в отдельных коротеньких методах).
            • +2
              userView.hideProgressBar();

              У вас контроллер практически напрямую управляет отображением, знает о прогресс-баре и видимо вообще обо всех UI-элементах?
              Бизнес-логика — это изменение состояния моделей, а у вас контроллер, опять же, меняет отображение. Т.е. в одном месте и бизнес-логика и логика отображения, где разделение?
              Суть MVC, как раз таки, в односторонней зависимости, а не в том, что контроллер — место встречи всех.

              Отображение знает об интерфейсе контроллере (получение и установка параметров, например, через вызов методов), контроллер знает об интерфейсе модели (тоже самое), модель не знает о них ничего.
              • +1
                Да, контроллер знает о том, что на экране можно показывать и скрывать progress bar, но он не реализует это поведение. Так же контроллер ничего не знает о UI компонентах, он работает только с интерфейсом класса View.
                • +2
                  1. Что такое «реализует поведение»? Это то чем занимается ОС, VM или драйвер видеокарты?
                  2. В чем отличие «userView.showProgressBar();» от «progressBar.setVisibility(View.VISIBLE);»? Дополнительный слой абстракции — это я поняла, но в чем различие вашего Activity от View, если оба управляют отображением? Так ведь можно сколько угодно слоев вводить, причем тут MVC?
                  • +1
                    1. «Реализует поведение» значит скрыть детали создания и наполнения данными View
                    2. Отличие в том, что Activity говорит View классу, что нужно сменить состояние экрана или заполнить его данными, а View делает это, причем реализацию метода showProgressBar можно менять для разных View, например на одном экране progress bar нужно показывать в центре экрана, для других это поведение может быть различным
                    • +1
                      habrahabr.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 и любого подобного?

                      • +1
                        Главная моя задача была не отвязать контроллер от представления, а уменьшить количество кода в контроллере, путем создания слоя View классов, с чем они довольно хорошо справляются. Если вы хотите использовать разные View для одного и того же контроллера, логично что у такой View должен быть интерфейс, который она должна реализовать. У другой View не может не быть метода showProgressBar, т.к. он будет в её интерфейсе, либо если сильно хочется не показывать пользователю информацию о процессе загрузки, то реализацию метода можно оставить пустой.
                        • +1
                          Главная моя задача была не отвязать контроллер от представления, а уменьшить количество кода в контроллере, путем создания слоя View классов, с чем они довольно хорошо справляются.

                          С чем хорошо справляются, с вынесением части кода в другой класс? Ну да, только почему вы называете это View? Назовите это ControllerPartial, а лучше ActivityPartial, ниже есть пример про region.
                          Это все нужно городить для того чтобы работать с абстракциями (интерфейсами) и локализовать код, который чаще всего подвергается изменениям(код представления) в отдельном классе

                          У другой View не может не быть метода showProgressBar, т.к. он будет в её интерфейсе, либо если сильно хочется не показывать пользователю информацию о процессе загрузки, то реализацию метода можно оставить пустой.

                          Т.е. если мы хотим из этой view убрать отображение загрузки — мы просто должны оставить метод пустым, так?
                          Теперь представим, что у нас не было этого метода изначально во view. Что мы теперь будем делать? Правильно, менять код контроллера и дописывать туда showProgressBar. И так с каждым изменением отображения…
                          • 0
                            Само собой, если мы хотим убрать из View отображение progress bar'a, или установку данных с помощью showUser, то это коснется контроллера, но это будет изменение одной строчки. Если же мы захотим поменять способ отображения, то это коснется только View
                            • 0
                              Если же мы захотим поменять способ отображения, то это коснется только View

                              Это просто вынесение части логики в другое место. Вы можете точно так же создать класс ProgressBarController и в Activity вызывать ProgressBarController.show(). Тогда при изменении способа отображения прогресс-бара — activity-класс не будет меняться.
                              Этот паттерн называется — декомпозиция, а не MVC.
                              • 0
                                Тогда MVC можно назвать декомпозицей кода на модельные классы, классы представления и контроллеры. О чем и речь в статье
                                • –1
                                  Тогда MVC можно назвать декомпозицей кода на модельные классы, классы представления и контроллеры. О чем и речь в статье

                                  MVC — можно назвать декомпозицией, а то, о чем речь в статье — нет.
                                  • –2
                                    Ну как же, рассказывается о контроллере, о View, тут и MVC рядом.
                                    • 0
                                      Ну если контроллер и view — это любые классы, без классификации их смысла, просто содержащие в названии (или в мыслях) «controller» и «view», то хорошо, вы описали MVC.
                                      • +2
                                        Ок. Давайте введем некоторые термины, т.к. мы по-разному понимаем MVC

                                        Контроллер — ловит события от пользователя (клики, свайпы и тд) и обрабатывает их, ничего не знает о том как отображать данные
                                        View — содержит информацию как визуализировать данные, не содержит никакой логики обработки событий
                                        Model — предоставляет данные

                                        Что и представлено в статье. Вы хотели создать ProgressBarController, но я не могу его назвать контроллером, т.к. там нет взаимодействия с пользователем. Так же не могу создать ControllerPartial или ActivityPartial по той же причине. А вот View отличное название, т.к. все что делает этот класс это визуализирует данные и под описание контроллера никак не подходит.
                                        • +1
                                          Контроллер — ловит события от пользователя (клики, свайпы и тд) и обрабатывает их, ничего не знает о том как отображать данные

                                          У вас контроллер прекрасно знает, что состояние loading — означает показать progressbar. Это именно как. Т.е., ваш контроллер знает из чего состоит View (в ней есть прогрессбар), а значит ваша MVC не несет никакой практической ценности, ведь отделения логики от представления нет. В одном месте хранятся знания о модели и о представлении. А так как единственная и самая главная ценность MVC именно в этом, то ваша архитектура — не MVC.

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

                                          Ок, пусть будет TextBoxController с подпиской на onchange событие.
                                          или ActivityPartial по той же причине

                                          ?
                                          А вот View отличное название, т.к. все что делает этот класс это визуализирует данные и под описание контроллера никак не подходит.

                                          View — отличное название, с этим ведь никто и не спорит, только внимание в 3 раз задаю вопрос. В чем смысл этого разделения? Я привела кучу примеров и везде требовалось изменить код вашего контроллера и view.

                                          Вы написали, что смысл просто в вынесении части кода, но причем тут MVC?
                                          • 0
                                            Вы считаете что MVC — это обязательно MVC с активной моделью, в которой контроллер не связан с View? В статье показан MVC с пассивной моделью когда контроллер обращается к View для его перерисовки.
                                            • 0
                                              Вы считаете что MVC — это обязательно MVC с активной моделью, в которой контроллер не связан с View?

                                              M — модель, предметная область, бизнес-логика.
                                              C — Промежуточное звено для уменьшения связаности.
                                              V — Интерфейс для конечного клиента.
                                              Собственно, весь смысл MVC (в отличии от других шаблонов) именно в контроллере и именно в уменьшении связанности.
                                              Нет никаких правил, что именно должно быть в модели, контроллере и виде, правило одно — в этом должен быть практический смысл. А главный смысл — повторное использование и минимальные изменения частей кода при изменении тех. задания.

                                              В статье показан MVC с пассивной моделью когда контроллер обращается к View для его перерисовки.

                                              В статье показан пример, в котором при изменении модели нам придется менять и модель и контроллер. При изменении отображения, нам придется менять и отображение и контроллер.
                                              Т.е. контроллер является ТТУКом, который вместо внедрения еще одного слоя для уменьшения связанности — увеличивает ее. В одном месте находится и бизнес-логика (изменение модели), и работа с интерфейсом (обработка событий), и отображение (showProgressBar).
                                              Т.е. никакого разделения на MVC нет, все в одном классе. Зато есть вынесение части логики отображения в другой класс.

                                              И да, пассивная модель — это когда мы модель(бизнес-логику) описываем в одном классе с контролером. Это уже тоже MVC с натяжкой, но все еще лучше, чем у вас — все три части в одном месте.
                                              • 0

                                                Вопрос: как вы предлагаете реализовывать контроллер, ничего не знающий о вью? Кто тогда должен решать, когда отобразить прогресс?

          • 0
            Тысячу раз уже обсуждали, что MVC и андроид не совместимы. Зачем гордить все это, если SDK нам предоставляет готовые компоненты приложения, которые по своей натуре малосвязны? Эта статья нормально смотрелась в 2009-2010 году, когда народ перешел из web программирования и тянул свои концепции, но сейчас-то уже обо всем догорились и пора перестать заниматься вот этим.
            • 0
              Сейчас работаю на улучшением архитектуры своих приложений. Не подскажете, что тогда юзать тру, если не MVC?
              • +1
                Следующая статья будет по MVP. Но правильное использование MVC это уже лучше чем ничего
                • +2
                  Я все же прочитал статью и код ), и могу сказать, что согласен с вашим подходом. Только к MVC он отношения не имеет.
              • –1
                Используйте сервисы, контент провайдеры, бродкаст ресиверы и активити.
                • –1
                  Не знал, что Кэп пишет под Android) Это понятно, но как это все красиво архитектурно оформить, вот в чем вопрос. Если не MVC.
                  • –1
                    Это и есть архитектура. Все это слабосвязные компноенты by design. MVC, как уже в 1000 раз выяснили в комментариях, в условиях Android SDKне возможен. Вот наглядный пример.
                    • 0
                      И чего сразу минусовать? Я ведь согласен, что mvc на дроиде реализовывать — это оверинжиниринг. Но «как не делать mvc правильно» тоже надо уметь. И ioschedule не лучший пример, по крайней мере, за 2014 год. Подождем 2015-го, вроде не за горами.
                      • 0
                        Почему не лучший пример? Инженеры из команды Android показывают, как они пишут приложения — что может быть лучше?

                        Минус скорей всего поставили за «Кэп пишет под Android».
                        • 0
                          Была когда-то статья по этому поводу: habrahabr.ru/post/241139
                          • 0
                            Согласен, посмотрел на код и он явно плохой. Хотя в целом, «архитектура» похожа на ту, что обычно все используют.
                            • 0
                              Легок на помине) Только что приложение обновилось до 2015. Ждем код. Или он уже есть, надо посмотреть.
                              • 0
                                Ан нет, как обычно, после ивента.
                      • 0
                        Это кстати наглядный пример убогости разработки под андроид, приложение имеет баги с восстановлением состояния и имеет столько кода внутри (под 100тыс строк xml+java), что страшно становится, а написано гуглом.
                • +1
                  Это все нужно городить для того чтобы работать с абстракциями (интерфейсами) и локализовать код, который чаще всего подвергается изменениям(код представления) в отдельном классе
                  • 0
                    Теперь представьте, что вместо прогресс-бара мы хотим крутить spinner или просто писать слово загрузка. В вашем примере придется менять код контроллера и код представления.
                    • +1
                      В таком случае можно сделать метод более общим, например, showLoading, и реализовывать его по-разному
                    • +1
                      У вас есть статистика какой код чаще меняется? Посмотрите исходники гугловых приложений — там нет никакого MVC.
                  • –3
                    UPD: я вижу вы какие-то стажировки проводите. Пожалуйста, не учите этому там!
                    • +1
                      Идея хорошая, но на практике часто связь между активити и контролами слишком большая чтоб их разделять.
                      Я в таких случаях группирую код относящийся к разным частям с помощью регионов (//region в Android Studio).
                      Также отсутствие в Java ивентов и делегатов усложняет реализацию красивых интерфейсов.
                      Дополню пост этой древней статьей.
                      • +3
                        Есть мнение, что MVC в андроиде уже есть: View — это xml-разметка, Controller — это активити, ну а Model остаётся вам на реализацию.
                        • 0
                          Хочу также добавить что в 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/ (на английском)
                          • 0
                            Гугл наконец-то занялся архитектурой сам и сделал MVVM)
                            developer.android.com/tools/data-binding/guide.html
                            • 0
                              Используя androidannotations @ViewGroup/View можно вынести по крайней мере часть UI логики из активити в другой класс, но целиком избавится от UI кода в активити не получится, но по крайней можно сократить и упростить за счет аннотаций. Теперь может где-то пригодится и биндинг, но вполне может оказаться, что проще и понятнее написать геттер/сеттер во вью, чем пытаться потребить биндинг от гугла :)

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

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

                              Самое читаемое