Глава 2. Android и MVC Создание нового класса На панели Package Explorer щелкните правой кнопкой мыши на пакете com.bignerd- ranch.android.geoquiz и выберите команду New Class . Введите имя класса TrueFalse , оставьте суперкласс по умолчанию java.lang.Object и щелкните на кнопке Finish Рис. 2.2. Создание класса TrueFalse Добавьте в файл TrueFalse.java два поля и конструктор: Листинг 2.1. Добавление класса TrueFalse public class TrueFalse { private int mQuestion; private boolean mTrueQuestion; public TrueFalse(int question, boolean trueQuestion) { mQuestion = question; mTrueQuestion = trueQuestion; } }
Генерирование get - и set-методов 53 Почему поле mQuestion объявлено с типом int , а не String ? В нем будет храниться идентификатор ресурса (всегда int ) строкового ресурса с текстом вопроса. Мы займемся созданием этих строковых ресурсов в следующем разделе. Переменная mTrueQuestion указывает, истинно или ложно утверждение в вопросе. Для переменных необходимо определить get - и set -методы. Вводить их самостоя- тельно не нужно — проще приказать Eclipse сгенерировать реализации. Генерирование get - и set-методов Прежде всего следует настроить Eclipse на распознавание префикса m в полях клас- сов и использование префикса is вместо get для логических переменных. Откройте окно настроек Eclipse (меню Eclipse на Mac, команда Windows Preferences в системе Windows). В настройках Java выберите категорию Code Style В таблице Conventions for variable names: выберите строку Fields (рис. 2.3). Щелкните на кнопке Edit и введите префикс m для полей. Затем добавьте префикс s для ста- тических полей. (В проекте GeoQuiz префикс s не используется, но он пригодится в будущих проектах.) Проследите за тем, чтобы флажок Use 'is' prefix for getters that return boolean был уста- новлен. Щелкните на кнопке OK Рис. 2.3. Настройка стиля оформления кода Java
54 Глава 2. Android и MVC Зачем мы задавали эти префиксы? Если теперь приказать Eclipse сгенерировать get -метод для mQuestion , среда сгенерирует методы с именами getQuestion() вместо getMQuestion() и isTrueQuestion() вместо isMTrueQuestion() Вернитесь к файлу TrueFalse.java , щелкните правой кнопкой мыши после конструктора и выберите команду Source Generate Getters And Setters... Щелкните на кнопке Select All , чтобы сгенерировать get - и set -метод для каждой переменной. Представление (макет) Модель Контроллер Рис. 2.4. Связи между объектами GeoQuiz Щелкните на кнопке OK . Eclipse генерирует код четырех методов. Листинг 2.2. Сгенерированные get- и set-методы public class TrueFalse { private int mQuestion; private boolean mTrueQuestion; public TrueFalse(int question, boolean trueQuestion) { mQuestion = question; mTrueQuestion = trueQuestion; } public int getQuestion() { return mQuestion; } public void setQuestion(int question) { mQuestion = question; }
Архитектура «Модель-Представление-Контроллер» и Android 55 public boolean isTrueQuestion() { return mTrueQuestion; } public void setTrueQuestion(boolean trueQuestion) { mTrueQuestion = trueQuestion; } } Класс TrueFalse готов. Вскоре мы внесем изменения в QuizActivity для работы с TrueFalse , но для начала посмотрим, как фрагменты GeoQuiz будут работать вместе. Класс QuizActivity должен создать массив объектов TrueFalse . В процессе работы он взаимодействует с TextView и тремя виджетами Button для вывода вопросов и предоставления обратной связи на ответы пользователя. Архитектура «Модель-Представление- Контроллер» и Android Вероятно, вы заметили, что объекты на рис. 2.4 разделены на три области: «Мо- дель», «Контроллер» и «Представление». Приложения Android строятся на базе архитектуры, называемой «Модель-Представление-Контроллер», или сокращенно MVC (Model-View-Controller). Согласно канонам MVC, каждый объект при- ложения должен быть объектом модели, объектом представления или объектом контроллера. Объект модели содержит данные приложения и «бизнес-логику». Классы мо- дели обычно проектируются для моделирования сущностей, с которыми имеет дело приложение — пользователь, продукт в магазине, фотография на сервере, вопрос «да/нет» и т. д. Объекты модели ничего не знают о пользовательском интерфейсе; их единственной целью является хранение и управление данными. В приложениях Android классы моделей обычно создаются разработчиком для конкретной задачи. Все объекты модели в вашем приложении составляют его уровень модели Уровень модели GeoQuiz состоит из класса TrueFalse Объекты представлений умеют отображать себя на экране и реагировать на ввод пользователя — например, касания. Простейшее правило: если вы видите что-то на экране — значит, это представление. Android предоставляет широкий набор настраиваемых классов представлений. Разработчик также может создавать собственные классы представлений. Объ- екты представления в приложении образуют уровень представления. В GeoQuiz уровень представления состоит из виджетов, заполненных по со- держимому файла activity_quiz.xml Объекты контроллеров связывают объекты представления и модели; они со- держат «логику приложения». Контроллеры реагируют на различные события,
56 Глава 2. Android и MVC инициируемые объектами представлений, и управляют потоками данных между объектами модели и уровнем представления. В Android контроллер обычно представляется субклассом Activity , Fragment или Service . (Фрагменты рассматриваются в главе 7, а службы — в главе 29.) Уровень контроллера GeoQuiz в настоящее время состоит только из класса QuizActivity На рис. 2.5 показана передача управления между объектами в ответ на пользо- вательское событие — такое, как нажатие кнопки. Обратите внимание: объекты модели и представлений не взаимодействуют друг с другом напрямую; в любом взаимодействии участвуют «посредники»-контроллеры, получающие сообщения от одних объектов и передающие инструкции другим. Пользователь взаимодействует… Ввод от пользователя Представ- ления Модель Конт- роллер Контроллер получает данные от объектов модели, представляющих интерес для его представлений Контроллер обновляет объекты модели Контроллер обновляет представление изменениями в объектах модели Представление отправляет сообщение контроллеру Рис. 2.5. Взаимодействия MVC при получении ввода от пользователя Преимущества MVC Приложение может обрастать функциональностью до тех пор, пока не станет слиш- ком сложным для понимания. Разделение кода на классы упрощает проектирование и понимание приложения в целом; разработчик мыслит в контексте классов, а не отдельных переменных и методов. Аналогичным образом разделение классов на уровни модели, представления и кон- троллера упрощает проектирование и понимание приложения; вы можете мыслить в контексте уровней, а не отдельных классов. GeoQuiz не является сложным приложением, но преимущества разделения уровней проявляются и здесь. Вскоре мы обновим уровень представления GeoQuiz и доба- вим в него кнопку Next . При этом нам совершенно не нужно помнить о только что созданном классе TrueFalse
Обновление уровня представления 57 MVC также упрощает повторное использование классов. Класс с ограниченными обязанностями лучше подходит для повторного использования, чем класс, который пытается заниматься всем сразу. Например, класс модели TrueFalse ничего не знает о виджетах, используемых для вывода вопроса «да/нет». Это упрощает использование TrueFalse в приложении для разных целей. Например, если потребуется вывести полный список всех во- просов, вы можете использовать тот же объект, который используется для вывода всего одного вопроса. Обновление уровня представления После теоретического знакомства с MVC пора переходить к практике — обновим уровень представления GeoQuiz и включим в него кнопку Next В Android объекты уровня представления обычно заполняются на основе разметки XML в файле макета. Весь макет GeoQuiz определяется в файле activity_quiz.xml В него следует внести изменения, представленные на рис. 2.6. (Для экономии места на рисунке не показаны атрибуты виджетов, оставшихся без изменений.) Рис. 2.6. Новая кнопка Итак, на уровне представления необходимо внести следующие изменения: Удалите атрибут android:text из TextView . Жестко запрограммированный текст вопроса не должен присутствовать в определении. Назначьте TextView атрибут android:id . Идентификатор ресурса необходим виджету для того, чтобы мы могли задать его текст в коде QuizActivity Добавьте новый виджет Button как потомка корневого элемента LinearLayout Вернитесь к файлу activity_quiz.xml и выполните все перечисленные действия. Листинг 2.3. Новая кнопка и изменения в TextView (activity_quiz.xml) ... > android:id="@+id/question_text_view" продолжение
58 Глава 2. Android и MVC Листинг 2.3 (продолжение) android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="24dp" android:text="@string/question_text" /> ... >
Обновление уровня контроллера 59 Обратите внимание на использование служебной последовательности \' для вклю- чения апострофа в последнюю строку. В строковых ресурсах могут использоваться все стандартные служебные последовательности, включая \n для обозначения новой строки. Сохраните файлы. Вернитесь к файлу activity_quiz.xml и ознакомьтесь с изменениями макета в графическом конструкторе. Пока это все, что относится к уровню представления GeoQuiz. Пора связать все воедино в классе контроллера QuizActivity Обновление уровня контроллера В предыдущей главе в единственном контроллере GeoQuiz — QuizActivity — не происходило почти ничего. Он отображал макет, определенный в файле activity_quiz. xml , назначал слушателей для двух кнопок и организовывал выдачу уведомлений. Теперь, когда у нас появились дополнительные вопросы, классу QuizActivity придется приложить дополнительные усилия для связывания уровней модели и представления GeoQuiz. Откройте файл QuizActivity.java . Добавьте переменные для TextView и новой кнопки Button . Также создайте массив объектов TrueFalse и переменную для индекса массива. Листинг 2.6. Добавление переменных и массива TrueFalse (QuizActivity.java) public class QuizActivity extends Activity { private Button mTrueButton; private Button mFalseButton; private Button mNextButton; private TextView mQuestionTextView; private TrueFalse[] mQuestionBank = new TrueFalse[] { new TrueFalse(R.string.question_oceans, true), new TrueFalse(R.string.question_mideast, false), new TrueFalse(R.string.question_africa, false) new TrueFalse(R.string.question_americas, true), new TrueFalse(R.string.question_asia, true), }; private int mCurrentIndex = 0; ... Программа несколько раз вызывает конструктор TrueFalse и создает массив объ- ектов TrueFalse (В более сложном проекте этот массив создавался бы и хранился в другом месте. Позднее мы рассмотрим более правильные варианты хранения данных модели. А пока для простоты массив будет создаваться в контроллере.) Мы собираемся использовать mQuestionBank , mCurrentIndex и методы доступа TrueFalse для вывода на экран серии вопросов.
60 Глава 2. Android и MVC Начнем с получения ссылки на TextView и задания тексту виджета вопроса с те- кущим индексом. Листинг 2.7. Подключение виджета TextView (QuizActivity.java) public class QuizActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); mQuestionTextView = (TextView)findViewById(R.id.question_text_view); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); mTrueButton = (Button)findViewById(R.id.true_button); } } Сохраните файлы и проверьте возможные ошибки. Запустите программу GeoQuiz. Первый вопрос из массива должен отображаться в виджете TextView Теперь разберемся с кнопкой Next . Получите ссылку на кнопку, назначьте ей слуша- теля View.OnClickListener . Этот слушатель будет увеличивать индекс и обновлять текст TextView Листинг 2.8. Подключение новой кнопки (QuizActivity.java) public class QuizActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); mQuestionTextView = (TextView)findViewById(R.id.question_text_view); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); mFalseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.correct_toast, Toast.LENGTH_SHORT).show(); } }); mNextButton = (Button)findViewById(R.id.next_button); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
Обновление уровня контроллера 61 int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } }); } } Обновление переменной mQuestionTextView осуществляется в двух разных местах. Лучше выделить этот код в закрытый метод, как показано в листинге 2.9. Далее оста- ется лишь вызвать этот метод в слушателе mNextButton и в конце onCreate(Bundle) для исходного заполнения текста в представлении активности. Листинг 2.9. Инкапсуляция в методе updateQuestion() (QuizActivity.java) public class QuizActivity extends Activity { private void updateQuestion() { int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } @Override protected void onCreate(Bundle savedInstanceState) { mQuestionTextView = (TextView)findViewById(R.id.question_text_view); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); updateQuestion(); } }); updateQuestion(); } } Запустите GeoQuiz и протестируйте новую кнопку Next Итак, с вопросами мы разобрались — пора обратиться к ответам. И снова мы ре- ализуем закрытый метод для инкапсуляции кода вместо того, чтобы вставлять одинаковый код в двух местах. Сигнатура метода, который будет добавлен в QuizActivity , выглядит так: private void checkAnswer(boolean userPressedTrue) Метод получает логическую переменную, которая указывает, какую кнопку нажал пользователь: True или False . Ответ пользователя проверяется по ответу текущего объекта TrueFalse . Наконец, после определения правильности ответа метод создает уведомление для вывода соответствующего сообщения.
62 Глава 2. Android и MVC Включите в файл QuizActivity.java реализацию checkAnswer(boolean) , приведенную в листинге 2.10. Листинг 2.10. Добавление метода checkAnswer(boolean) (QuizActivity.java) public class QuizActivity extends Activity { private void updateQuestion() { int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } private void checkAnswer(boolean userPressedTrue) { boolean answerIsTrue = mQuestionBank[mCurrentIndex].isTrueQuestion(); int messageResId = 0; if (userPressedTrue == answerIsTrue) { messageResId = R.string.correct_toast; } else { messageResId = R.string.incorrect_toast; } Toast.makeText(this, messageResId, Toast.LENGTH_SHORT) .show(); } @Override protected void onCreate(Bundle savedInstanceState) { } } Включите в слушателя кнопки вызов checkAnswer(boolean) , как показано в ли- стинге 2.11. Листинг 2.11. Вызов метода checkAnswer(boolean) (QuizActivity.java) public class QuizActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { mTrueButton = (Button)findViewById(R.id.true_button); mTrueButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.incorrect_toast, Toast.LENGTH_SHORT).show(); checkAnswer(true); } });
Запуск на устройстве 63 mFalseButton = (Button)findViewById(R.id.false_button); mFalseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.correct_toast, Toast.LENGTH_SHORT).show(); checkAnswer(false); } }); mNextButton = (Button)findViewById(R.id.next_button); } } Программа GeoQuiz снова готова к работе. Давайте запустим ее на реальном устройстве. Запуск на устройстве В этом разделе мы займемся настройкой системы, устройства и приложения для выполнения GeoQuiz на физическом устройстве. Подключение устройства Прежде всего подключите устройство к системе. Если разработка ведется на Mac, ваша система должна немедленно распознать устройство. В системе Windows может потребоваться установка драйвера adb (Android Debug Bridge). Если Windows не может найти драйвер adb, загрузите его с сайта производителя устройства. Чтобы убедиться в том, что устройство было успешно опознано, откройте панель Devices . Для этого проще всего открыть перспективу DDMS, щелкнув на кнопке DDMS в правом верхнем углу инструментального окна Eclipse. Панель Devices должна открыться в левой части инструментального окна. В списке должны присутствовать как AVD, так и физическое устройство. Чтобы вернуться к редактору и другим панелям, щелкните на кнопке Java в правом верхнем углу инструментального окна. Если у вас возникнут трудности с распознаванием устройства, прежде всего по- пробуйте заново инициализировать adb. На панели Devices щелкните на кнопке на стрелке, указывающей вниз, в правой верхней части панели. На экране появляется меню. Выберите в нем нижнюю команду Reset adb . Возможно, через непродолжи- тельное время устройство появится в списке. Если сброс не помогает, дополнительную информацию можно найти на сайте раз- работчиков Android. Начните со страницы https://developer.android.com/tools/device. html или обратитесь на форум книги forums.bignerdranch.com за дополнительной информацией о решении проблемы.