Через пару дней после моего 28-летия я получил свое первое предложение работать полный рабочий день после получения степени магистра компьютерных наук. Должность заключалась в прохождении трехмесячной стажировки в Floreo, стартапе из Вашингтона, округ Колумбия, который занимается созданием мобильных приложений виртуальной реальности, которые помогают людям с аутизмом.

Хотя моя должность была только «Инженер-программист, стажер», я отвечал за «ведущий разработчик Android». Мне было поручено создать все мобильное приложение для Android с нуля! Когда я начал стажировку 1 февраля 2018 года, приложение было создано для мобильного устройства iOS, которое подключалось по беспроводной сети к iPad, чтобы можно было контролировать опыт виртуальной реальности. Из мобильной кодовой базы iOS мне пришлось перевести эквивалентный код для Android. Конечная цель заключалась в том, чтобы создать полностью работающее мобильное приложение для Android, которое загружает графику VR и подключается по беспроводной сети к монитору iPad.

Миссия выполнена. Вот как я это сделал:

Модель - Представление - Архитектура контроллера

Модель-представление-контроллер (MVP) - это архитектурный шаблон для структурирования кода в программе. MVP - это разделение компонентов кода: модель, которая представляет собой данные, которые использует ваше приложение, отделена от представления, который представляет собой код, который разрабатывает пользовательский интерфейс приложения, и оба они отделены от контроллера, который представляет собой код, который управляет пользовательским интерфейсом.

Я следовал соглашениям MVP при разработке архитектуры проекта.

Модели

Модели определены как обычные классы Java. Каждый отдельный класс имеет уникальное имя и определяет новый тип модели данных. Назначение класса модели - предоставить шаблон для построения данных и для хранения этих данных в основной памяти.

Как показано на изображении навигатора проекта выше, я инкапсулировал файлы данных модели в каталог с именем «модель». Каждый подкаталог содержит один или несколько файлов Java, используемых для определенной цели. Например, каталог «loginResponse» содержит классы Java модели, используемые для формирования данных ответа, возвращаемых сервером после выполнения сетевого запроса входа в систему.

Просмотры

Представления состоят из файлов макета XML, которые определяют, как должен выглядеть пользовательский интерфейс. Представление содержит код для разработки фактического внешнего вида, который пользователь увидит на экране. Как правило, каждый отдельный файл макета XML представляет собой отдельный экран, который будет отображаться на устройстве пользователя. XML-файл содержит ссылки на любые изображения (например, файлы .png), кнопки, форматирование, стили, цвета и текст для проектирования этого конкретного экрана.

Контроллеры

Контроллер - это особый тип класса Java, называемый классом Activity, который соответствует отдельному XML-файлу макета. В коде каждого приложения Android есть специальный файл под названием AndroidManifest, который содержит список всех классов Activity приложения.

В AndroidManifest я объявил LandingPageActivity как действие, код которого будет выполняться первым при каждом запуске приложения. Как видно из строки кода внизу, класс активности указывает, какое представление отображать в пользовательском интерфейсе. Он также содержит всю логику того, какие инструкции делать, пока целевая страница отображается на устройстве, а также код для перехода к следующему классу действий после его завершения.

Навигация по пользовательскому интерфейсу

Моей целью для навигации по пользовательскому интерфейсу приложения было имитировать внешний вид и последовательность аутентификации, входа в систему, игры и сопряжения приложения Floreo iOS.

Аутентификация

  1. Пользователь запускает приложение и видит LandingPageActivity.
  2. Через 2 секунды приложение перейдет к RegistrationCheckActivity.
  3. RegistrationCheckActivity ожидает, пока пользователь введет свой адрес электронной почты и коснется кнопки «Далее». Незаметно для вас инициируется сетевой запрос, чтобы определить, одобрено ли бета-тестирование электронной почты пользователя.
  4. Если это так, приложение переходит к CreateUserAccountActivity.
  5. В противном случае приложение переходит к RegistrationRequestActivity.

Авторизация

Приложение может несколькими путями перейти к LoginActivity из RegistrationCheckActivity.

  1. Во-первых, если пользователь случайно инициирует сетевой запрос проверки регистрации, но у пользователя уже есть учетная запись, то…
  2. приложение перейдет к LoginActivity.
  3. Во-вторых, после того, как бета-одобренный пользователь…
  4. создает свою учетную запись,
  5. приложение перейдет к LoginActivity.
  6. И наконец, когда зарегистрированный пользователь просто нажимает ссылку «Войти» в RegistrationCheckActivity.
  7. Пользователь вводит свои учетные данные для входа и нажимает кнопку «Вход», которая инициирует сетевой запрос входа в систему.

Игра и сопряжение

  1. После успешного входа пользователя в систему приложение переходит от LoginActivity к PlayerManagementActivity, где пользователю отображается список игроков.
  2. Список пользовательского интерфейса заполняется списком игроков, полученным из сетевого запроса на получение игроков.
  3. Список заполняется данными, определенными классом java модели Player.
  4. Когда пользователь нажимает на игрока в списке пользовательского интерфейса,
  5. приложение переходит к WaitForMonitorActivity, где отображается индикатор выполнения, поскольку за кулисами выполняются сетевые запросы для сопряжения устройства с монитором iPad.

Сохранение данных

Общие настройки

Чтобы сохранять данные между сеансами пользователя, я использовал библиотеку Android SharedPreferences. SharedPreferences позволяет сохранять данные в файл на устройстве пользователя в виде хранилища "ключ-значение", которое можно читать или записывать во время будущих запусков приложения.

  1. Пользователь сначала запускает приложение, вводит свои учетные данные и нажимает «Войти».
  2. В случае успеха вызывается функция шифрования, передавая учетные данные пользователя.
  3. Функция шифрования возвращает зашифрованные значения учетных данных для входа.
  4. Затем вызывается функция для получения идентификатора устройства.
  5. Наконец, вызывается функция для сохранения зашифрованных учетных данных для входа в систему, в которой хранятся добавленные версии строки идентификатора устройства в качестве ключей для зашифрованного имени пользователя и пароля.

Когда пользователь в будущем запускает приложение и пытается перейти к LoginActivity, устройство сначала проверяет наличие файла SharedPreferences на устройстве. Если он существует, приложение использует идентификатор устройства для поиска и извлечения всех учетных данных для входа, расшифровывает их и автоматически входит в систему пользователя.

Операции, не связанные с пользовательским интерфейсом

Утилиты

Чтобы придерживаться лучших практик программирования, я очистил код. Во-первых, я реорганизовал методы в своих классах действий, которые не нужно было объявлять в контексте действия. Во-вторых, я создал отдельный класс java, который назвал классом Util.

Класс Util - это класс, который не является операцией Android. Это означает, что класс не соответствует XML-файлу макета пользовательского интерфейса и не служит шаблоном для создания экземпляров объектов. Класс Util - это просто класс, содержащий ряд функций, каждая из которых выполняет определенную задачу.

После создания Util.java я переместил функции encrypt (), getDeviceId () и persistData () в класс Util.

Преимущество этого заключается в том, что эти функции теперь доступны для всех других классов и действий. Вместо того, чтобы переопределять эти функции в каждом действии, я могу определить их один раз в Util.java, а затем повторно использовать их по мере необходимости в остальной части проекта.

Сети

Большую часть времени в ходе стажировки я писал код для выполнения сетевых запросов и обработки ответов с сервера. Вот типы запросов, которые делает приложение:
- Вход,
- Обновить сеанс входа,
- Проверить статус учетной записи,
- Создать учетную запись,
- Получить игроки,
- отправить заявку на сопряжение устройств,
- потребовать заявку на сопряжение устройств и
- сообщить о событиях.

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

Наивный подход 1: подкласс AsyncTask

Мой первый подход заключался в создании подкласса AsyncTask для выполнения сетевых запросов. AsyncTask используется для выполнения операций, не связанных с пользовательским интерфейсом, в фоновом потоке. Я написал весь код для выполнения сетевых запросов, обработки ответа сервера, синтаксического анализа JSON и предоставления результатов Activity, инициировавшему запрос.

Наивный подход 2: сетевая библиотека и подкласс AsyncTask

Мой второй подход заключался в использовании сторонних библиотек для выполнения сетевых запросов и анализа ответов JSON. Я использовал библиотеку Retrofit 2.0 для запуска запросов и обработки обратных вызовов для ответов. Я также использовал библиотеку конвертера Gson для анализа JSON и преобразования ответа в объекты данных модели.

Я добился этого, создав класс Floreo API (FLAPI), который определяет сигнатуры методов для всего набора методов, которые запускают сетевые запросы. Затем каждый из этих методов был реализован в подклассе AsyncTask.

Проблема: утечки памяти и неиспользуемые ресурсы

При использовании AsyncTask для обработки фоновых операций AsyncTask сохраняет ссылку на класс активности, который ее инициировал. Сохраняя ссылку на класс активности, мой код был уязвим для потенциальных проблем: утечек памяти и неиспользуемых ресурсов памяти.

В качестве примера:

  1. Пользователь переходит к LoginActivity, и устройство сохраняет экземпляр действия в основной памяти.
  2. Когда пользователь вводит свои учетные данные и нажимает кнопку «Войти», создается AsyncTask. Для выполнения сетевого запроса входа в систему инициируется фоновый поток, а экземпляр AsyncTask сохраняется в основной памяти. AsyncTask содержит ссылку на вызывающий экземпляр LoginActivity.
  3. Предположим, что происходит изменение конфигурации, например, когда пользователь переворачивает устройство с книжной на альбомную. Затем экземпляр LoginActivity уничтожается в основной памяти.
  4. Изменение конфигурации вызывает создание нового экземпляра LoginActivity. Предположим, что AsyncTask еще не завершен. Этот сценарий приводит к утечке памяти, поскольку AsyncTask все еще имеет ссылку на исходный несуществующий экземпляр LoginActivity, что приводит к сбою приложения.
  5. Если после изменения конфигурации пользователь снова нажмет кнопку «Вход», будет создана и сохранена в памяти другая AsyncTask, что означает, что приложение использует больше ресурсов памяти, чем необходимо.

Решение: библиотеки RxJava и RxAndroid

RxJava - это реактивная библиотека, которая следует парадигме функционального программирования. Вместо того, чтобы создавать подклассы AsyncTask или писать беспорядочный код потоковой передачи, RxJava упрощает процесс асинхронных задач, не блокируя основной поток. Строительными блоками RxJava являются O bservables и S ubscribers.

Observable передает данные асинхронно, а подписчик потребляет эти данные. Метод subscribeOn () указывает O bservable, в каком потоке следует передать поток данных. По завершении метод Наблюдать () передает данные в указанный поток, который в моем случае является основным потоком пользовательского интерфейса.

Шаблоны проектирования

Синглтон

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

Сначала, каждый раз, когда действию или потоку требовался доступ к таким данным, они вызывали функцию Util. Например, каждый раз, когда приложению требуется доступ к данным сеанса, оно извлекает данные из общих настроек, затем вызывает функцию дешифрования и затем передает расшифрованные учетные данные вызывающему. Эти данные сеанса требуются почти для каждого сетевого запроса, который делает приложение. Это означает, что код не был оптимизирован, поскольку он неоднократно выполнял одни и те же операции.

Поскольку данные используются на протяжении всего выполнения приложения и эти данные нужно инициализировать только один раз, я реализовал шаблон Singleton. Операции по установке значений вызываются один раз и затем доступны только для чтения. В результате после создания экземпляра данных их значения остаются неизменными на протяжении всего выполнения приложения и могут быть безопасно прочитаны любым потоком.

Адаптер пользовательского интерфейса

После того, как пользователь успешно входит в систему, приложение выполняет сетевой запрос на получение игроков и направляет пользователя к FLPlayerManagementActivity. Цель FLPlayerManagementActivity - показать список игроков.

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

В моем каталоге макета я определяю два файла макета: файл макета, который соответствует FLPlayerManagementActivity, который содержит элемент ListView, и файл макета элемента списка, который определяет макет для отдельных элементов, которые будут заполнять ListView.

Вот как это работает:

После получения списка игроков из сетевого запроса этот список передается в конструктор для создания данных модели PlayerAdapter. PlayerAdapter подклассы BaseAdapter, которая представляет собой библиотеку Android, используемую для заполнения ListView элементами списка путем адаптации базового элемента списка к спецификациям, определенным в XML-файле элемента списка.

В классе PlayerAdapter я определил статический класс ViewHolder. Метод getView () вызывается для каждого элемента списка, когда список в настоящее время находится на переднем плане экрана пользователя. В методе getView () объект ViewHolder объявляется и используется в качестве заполнителя. Затем ViewHolder преобразует (или «адаптирует») каждый базовый экземпляр Player в реальный объект элемента списка.

После того, как список становится видимым для пользователя, действие ожидает, пока пользователь выберет игрока, нажав на один из элементов списка. Класс super BaseAdapter предоставляет методы, необходимые для того, для какого элемента списка проигрывателя пользователь выбирает ListView.

Оптимизация параллелизма

Хотя все сетевые запросы обрабатывались в фоновых потоках, все еще оставалось место для улучшения. Большая часть кода потоковой передачи была довольно запутанной, поскольку использовала обработчики и Runnables.

Поэтому я уделил большое внимание анализу и классификации типов сетевых запросов. Моими целями были оптимизация для параллелизма, сокращение использования памяти и улучшение взаимодействия с пользователем.

Темы в Android

основной поток, также известный как поток пользовательского интерфейса, выполняет весь код активности. Он используется на протяжении всего жизненного цикла приложения от запуска до закрытия.

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

Разработчик несет ответственность за явное планирование задач в фоновых потоках. В противном случае по умолчанию код работает с потоком пользовательского интерфейса. По сравнению с основным потоком время жизни фонового потока обычно короче - он будет использоваться для выполнения такой задачи, как сетевой запрос, и завершится, когда задача завершится.

Категоризация сетевых запросов

На основе этого я разделил сетевые запросы на три различных типа:

  1. Запросы, ответы на которые вызывают изменения в пользовательском интерфейсе
  2. Запросы, ответы на которые вызывают последующий запрос, и
  3. Запросы, ответы на которые неуместны.

1 - Запросы, вызывающие изменения пользовательского интерфейса

Вход в систему, получение игроков и получение билетов попадают в первую категорию. После входа пользователя в систему выполняется сетевой запрос входа в систему, и в случае успешного ответа приложение переходит к FLPlayerManagementActivity и отображает соответствующий макет XML.

Из трех категорий этот тип запроса требует наибольшего взаимодействия между фоновым и основным потоками. По этой причине я реализовал подписчиков и наблюдаемые объекты из библиотек RxJava и RxAndroid, чтобы запускать запросы, преобразовывать ответы в данные модели, а затем передавать эти данные в основной поток, как только они становятся доступными.

2 - Запросы, запускающие последующие запросы

Запрос почтового билета попадает во вторую категорию. Из FLPlayerManagementActivity, когда пользователь нажимает на элемент списка игроков, приложение запускает запрос билета публикации в фоновом потоке. Как только этот поток получает ответ от сервера, он запускает запрос на получение билета.

Поскольку ответ на запрос на публикацию не требуется предоставлять потоку пользовательского интерфейса, я использовал библиотеку Android ThreadExecutor. Запрос почтового билета выполняется как вызов Retrofit 2.0, который ставит вызов в очередь. Затем ответ отправляется в фоновый поток и никогда не возвращается в поток пользовательского интерфейса.

Я использовал ThreadExecutor как чистую альтернативу обработчикам и исполняемым файлам. Это позволило мне абстрагироваться от деталей фоновой потоковой передачи из кода активности.

3 - Запросы без изменений

Когда пользователь выбирает игрока из ListView в FLPlayerManagementActivity, приложение переходит к UnityPlayerActivity и пытается подключиться к движку Unity. На этом этапе приложение отображает графику VR.

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

Я решил использовать библиотеку Android IntentService для обработки запросов событий отчетов. Служба в Android - это компонент, используемый для длительных фоновых задач, не связанных с пользовательским интерфейсом. Я реализовал запущенную службу, поскольку после запуска фоновые операции будут продолжать обрабатываться, пока пользователь не закроет приложение. Я настроил свой IntentService с помощью таймера. Через каждые 60 секунд служба собирает данные устройства, а затем делает запрос о событиях отчета для отправки этих данных на сервер.

Межплатформенное общение

Чтобы визуализировать графику VR, мне пришлось научиться заставлять мое приложение Android взаимодействовать с движком Unity.

Первым шагом был запуск сборок Unity. Каждое обновление кода требовало запуска новой сборки Unity для Android. Я добавил логику в сценарии сборки, чтобы каждая сборка была оборудована для поддержки как iOS, так и Android. После завершения сценария сборки он сгенерирует новый проект Android, содержащий весь написанный мной код и весь код Unity, необходимый для его поддержки.

В Android мне пришлось добавить библиотеки в файл манифеста Android, чтобы он мог поддерживать графику VR. Затем в серии файлов Java и файлов Unity я настроил методы, которые устанавливали канал связи между двумя разными платформами. Код межплатформенной связи содержал следующее: файлы Java, содержащие методы, вызываемые из Android, файлы Java, содержащие методы, вызываемые из Unity, и файлы C # в проекте Unity, которые вызывают код Android.

Когда пользователь переходит к UnityPlayerActivity и успешно подключается к монитору iPad, Android обращается к Unity для подключения к движку. После подключения происходит рендеринг графики, и пользователь может окунуться в новый мир виртуальной реальности.

Подробнее откуда это взялось

Эта история публикуется в журнале Noteworthy, куда каждый день приходят тысячи людей, чтобы узнать о людях и идеях, формирующих наши любимые продукты.

Следите за нашей публикацией, чтобы увидеть больше историй о продуктах и ​​дизайне, представленных командой Journal.