AzoftБлогОптимизация архитектуры Android-приложения: работа с данными

Оптимизация архитектуры Android-приложения: работа с данными

Алексей Васильков Декабрь 24, 2012

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

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

С точки зрения архитектуры приложения загрузка данных с сервера не должна быть связана с отображением данных или с логикой хранения данных в локальном хранилище. Каждый компонент должен отвечать лишь за свою часть работы и предоставлять другим компонентам методы для работы с ним. Для увеличения скорости разработки и повышения качества приложения стоит придерживаться классической трехуровневой структуры приложения:

  • Уровень 1. Данные

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

  • Уровень 2. Бизнес-логика

На этом уровне формируется набор данных для отображения. Здесь отправляются необходимые запросы к серверу или локальному хранилищу, происходит группировка данных и др.

  • Уровень 3. Отображение данных

Этот уровень отвечает за вывод имеющихся данных пользователю и получение данных от пользователя. К этому уровню относится работа с компонентами Activities, Fragments, Adapters, Views и т.д.

Рассмотрим некоторые компоненты приложения с такой трехуровневой структурой.

Предметная область

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

Общение с сервером

Все взаимодействие с сервером должно происходить только внутри одного компонента. Здесь будут находиться методы, с помощью которых мы будем получать данные с сервера или загружать данные на сервер. На выходе методов будут лишь объекты предметной области или примитивы, такие как String, int, boolean и др.

Для лучшей инкапсуляции компонента не стоит напрямую связывать предметную область с API сервера. Например, пользуясь популярной библиотекой GSON для парсинга JSON-объектов, лучше создать отдельный набор классов, которые будут применяться для парсинга ответов сервера. Кроме того, стоит организовать конвертацию полученных объектов в объекты предметной области.

Даже если парсинг осуществляется вручную, существование таких промежуточных классов может упростить нам жизнь. Особенно если структура ответов сервера не соответствует внутренней структуре объектов предметной области. Такой подход позволяет легче справляться с изменениями в API сервера, а также организовать более удобную структуру предметной области. В качестве минусов этого подхода стоит отметить увеличение нагрузки на процесс «сборки мусора» (garbage collector), которому придется удалять все освободившиеся объекты.

Также стоит уделить большое внимание обработке ошибок сервера. Ошибки могут быть как специальными сообщениями об ошибке с кодами в API сервера, так и стандартными кодами ошибок HTTP (например, HTTP 500 — внутренняя ошибка сервера). Для работы с такими ошибками отлично подходит стандартный в языке Java механизм «Исключения» (Exceptions).

Локальное хранение данных

Существует несколько способов хранения данных в Android-приложении:

  • с помощью класса SharedPreferences
  • в базе данных SQLite
  • во внутренней памяти устройства (на диске)

Для хранения с помощью класса SharedPreferences больше подходят единичные данные, такие как профиль пользователя. Также в SharedPreferences можно хранить и списки примитивов, например, список id-объектов через запятую. Несмотря на то, что обращения к SharedPrefereneces являются операциями «ввода-вывода» (I/O), которые потенциально могут выполняться очень долго, на практике их вполне можно вызывать в UI-потоке.

Встроенная база данных SQLite хорошо подходит для хранения списков объектов. Правда, предлагаемые методы работы с ней в Android достаточно неудобны, поэтому для хранения сложных объектов лучше воспользоваться дополнительной библиотекой ORMlite.

Так же как и при работе с сервером, логика работы с локально хранимыми данными должна быть спрятана внутри соответствующего компонента. Единственное, что должны знать остальные части приложения — это то, какие методы надо вызвать для получения необходимых данных (объектов предметной области) или для совершения какой-либо операции с ними.

Кэширование данных

Мобильные устройства не всегда имеют стабильное подключение к Интернету, поэтому часто возникает потребность в кэшировании данных. Большим минусом приложения будет, если без Интернета пользователь останется и без нужной информации. Кроме того, кэширование применяется чтобы ускорить работу приложения. Например, если одни и те же данные выводятся на разных экранах или часто используются для формирования контента экрана. Типичный пример — кэширование изображений, загруженных с сервера.

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

Для кэширования подходят любые варианты локального хранения данных, в том числе хранение данных в оперативной памяти приложения. В этом случае данные хранятся с помощью static-полей в специальном классе языка или с помощью singleton-класса. Еще одним достаточно удобным способом кэширования является сохранение ответов сервера на диск в виде отдельных файлов.

Работа с данными

В большинстве случаев получение данных из источника — с сервера, локальной базы данных или внутренней памяти устройства — является достаточно длительной операцией. Чтобы не блокировать пользовательский интерфейс, мы осуществляем эту операцию в отдельном потоке. Кроме того, при получении данных могут возникнуть различные ошибки — ошибки чтения с диска, ошибки подключения к Интернету или ошибки, пришедшие в ответе сервера. Нужно создать централизованное место, которое бы обрабатывало ошибки и выводило их на экран пользователям. 

Для решения данных проблем хорошо подходит абстрактный класс-наследник AsyncTask. Внутри него будет происходить обработка ошибок:

Далее для каждой операции с данными создается свой класс, наследник ErrorHandlingTask. В нем достаточно определить метод doBackgroundTask, ошибки в котором будут обработаны родительским классом.

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

Если результат операции, происходящей в сервисе, должен отображаться в пользовательском интерфейсе (например, прогресс загрузки файла на сервер), то лучше всего воспользоваться таким полезным инструментом, как broadcast. Он позволяет подписаться на определенные события, происходящие в другой части приложения, и реагировать на них соответствующим образом.

Также broadcast подходит для обработки некоторых видов ошибок сервера. Например, если при запросе к серверу приходит ошибка «User is not logged in», то необходимо закрыть все открытые экраны и предложить пользователю ввести свои учетные данные. В данном случае мы можем послать специальный broadcast, получив который, все активные экраны приложения самостоятельно закроются.

В данной статье я хотел подчеркнуть, что при создании приложений следует:

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

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

Комментарии

комментарии



Content created by Alex Vasilkov