16395959247_abdf585b09_b

Продолжение.

Сохраняющиеся данные

Room представляет собой библиотеку отображения объектов, которая обеспечивает локальное сохранение данных с минимальным шаблонным кодом. Во время компиляции он проверяет каждый SQL запрос, так что плохие SQL-запросы приводят к ошибкам компиляции, а не к ошибкам времени выполнения. Room абстрагирует некоторые из основных деталей реализации работы с сырыми таблицами SQL и запросами. Он также позволяет наблюдать изменения данных в базе данных (включая collections и join запросы), показывая такие изменения объектам LiveData. Кроме того, он явно определяет ограничения потоков, которые затрагивают общие проблемы, такие как доступ к хранилищу в основном потоке приложения.

(прим. мое) При попытке доступа к БД в основном потоке, вы получите: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.

Примечание. Если вы знакомы с другим решением постоянного хранения, таким как SQLite ORM или другой базой данных, например Realm, вам не нужно заменять его на Room.

Чтобы использовать Room, нам нужно определить нашу локальную схему. Во-первых, аннотируйте класс User с @Entity, чтобы пометить его как таблицу в вашей базе данных.

Затем создайте класс базы данных, расширив RoomDatabase для вашего приложения:

Обратите внимание, что класс MyDatabase является абстрактным. Room автоматически обеспечивает его реализацию. Подробную информацию см. в документации к Room.

Теперь нам нужен способ вставить пользовательские данные в базу данных. Для этого мы создадим объект доступа к данным (DAO) — data access object.

Затем обратитесь к DAO из нашего класса базы данных.

Обратите внимание, что метод load возвращает LiveData<User>. Room знает, когда база данных будет изменена, и она будет автоматически уведомлять всех активных наблюдателей при изменении данных. Поскольку он использует LiveData, это будет эффективно, потому что оно будет обновлять данные только в том случае, если есть хотя бы один активный наблюдатель.

Примечание. Room проверяет недействительности на основе изменений таблицы, что означает, что он может отправлять ложноположительные (false positive ) уведомления.

Теперь мы можем изменить наш UserRepository, чтобы включить источник данных Room.

Обратите внимание, что даже если мы изменили источник, откуда поступают данные в UserRepository, нам не нужно изменять наш UserProfileViewModel или UserProfileFragment. Это гибкость, обеспечиваемая абстракцией. Это также отлично подходит для тестирования, потому что вы можете предоставить поддельный (fake) UserRepository при тестировании вашего UserProfileViewModel.

Читать ещё :   LiveData. Часть 1

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

В некоторых случаях использования, таких как pull-to-refresh, важно, чтобы пользовательский интерфейс показывал пользователю, если в настоящее время выполняется работа в сети.

Эффективная практика заключается в том, чтобы отделить действие пользовательского интерфейса от фактических данных, поскольку они могут быть обновлены по разным причинам (например, если мы получаем список друзей, тот же пользователь может быть снова загружен с помощью обновления LiveData<User>). С точки зрения UI, тот факт, что есть запрос в полете, — это еще одна точка данных, подобная любой другой части данных (например, объект User).

Существует два распространенных решения для этого варианта использования (use case):

  1. Измените getUser, чтобы вернуть LiveData, который включает статус сетевой операции. Пример реализации представлен в разделе «Приложение : раскрытие статуса сети«.
  2. Предоставьте другую публичную функцию в классе repository, которая может вернуть статус обновления User. Эта опция лучше, если вы хотите показать статус сети в пользовательском интерфейсе только в ответ на явное действие пользователя (например, pull-to-refresh).

Единственный правдивый источник

Для разных конечных точек (endpoint) API REST обычно возвращает одни и те же данные. Например, если у нашего бэкэнд была другая конечная точка, которая возвращает список друзей, один и тот же пользовательский объект может поступать из двух разных конечных точек API, возможно, в разных деталях. Если UserRepository должен был вернуть ответ из запроса Webservice как есть, наши пользовательские интерфейсы могут потенциально отображать несогласованные данные, поскольку данные могут измениться на стороне сервера между этими запросами. Вот почему в реализации UserRepository обратный вызов веб-службы просто сохраняет данные в базе данных. Затем изменения в базе данных будут инициировать обратные вызовы для активных объектов LiveData.

В этой модели база данных служит единственным правдивым источником (single source of truth), а другие части приложения получают доступ к ней через репозиторий. Независимо от того, используете ли вы дисковый кэш, рекомендуется, чтобы ваш репозиторий обозначил источник данных как единственный источник правды для остальной части вашего приложения.

Тестирование

Мы упоминали, что одним из преимуществ разделения является тестируемость. Давайте посмотрим, как мы можем протестировать каждый модуль кода.

Пользовательский интерфейс и взаимодействие

Это будет единственный раз, когда вам понадобится инструментарий для теста Android UI. Лучший способ проверить код пользовательского интерфейса — создать тест Espresso. Вы можете создать фрагмент и предоставить ему макет (mock) ViewModel. Поскольку фрагмент говорит только с ViewModel, mocking ViewModel будет достаточно, чтобы полностью протестировать UI.

ViewModel

ViewModel можно протестировать с помощью теста JUnit. Вам нужно mock только UserRepository, чтобы проверить его.

UserRepository

Вы также можете протестировать UserRepository с помощью теста JUnit. Вам нужно mock Webservice и DAO. Вы можете проверить, что он:

  • делает правильные вызовы веб-сервисов,
  • сохраняет результат в базе данных
  • и не делает ненужных запросов, если данные кэшированы и обновлены.

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

UserDao

Рекомендуемый подход для тестирования классов DAO — это использование instrumentation тестов. Поскольку эти контрольные тесты не требуют какого-либо пользовательского интерфейса, они все будут работать быстро. Для каждого теста вы можете создать базу данных в памяти, чтобы убедиться, что тест не имеет побочных эффектов (например, изменение файлов базы данных на диске).

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

Webservice

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

Testing Artifacts

Architecture Components предоставляют артефакт maven для управления фоновыми потоками (background threads). Внутри android.arch.core:core-testing, существует 2 правила JUnit:

InstantTaskExecutorRule: это правило может использоваться для принудительного добавления Architecture Components для выполнения любой фоновой операции в вызывающем потоке.

CountingTaskExecutorRule: это правило может использоваться в контрольных тестах для ожидания фоновых операций Architecture Components или подключения к Espresso в качестве бездействующего ресурса (idling resource).

Окончательная архитектура

На следующей диаграмме показаны все модули в нашей рекомендуемой архитектуре и то, как они взаимодействуют друг с другом:

final-architecture

 

Руководящие принципы

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

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

  • Точки входа, которые вы определяете в своем манифесте: activities, services, broadcast receivers, etc — не являются источником данных (для остальных частей приложения. прим. мое.). Вместо этого они должны только координировать подмножество данных, относящихся к этой точке входа. Поскольку срок жизни каждого компонента приложения довольно короткий, в зависимости от взаимодействия пользователя с устройством и общего текущего состояния среды выполнения, вы не хотите, чтобы какая-либо из этих точек входа была источником данных.
  • Будьте беспощадны в создании четко определенных границ ответственности между различными модулями вашего приложения. Например, не распыляйте код, который загружает данные из сети на несколько классов или пакетов. Точно так же не объединяйте несвязанные обязанности, такие как кэширование данных и data binding, в один класс.
  • Выставляйте как можно меньше от каждого модуля. Не испытывайте соблазна создать «только тот» ярлык, который раскрывает детали внутренней реализации из одного модуля. Вы можете получить немного времени в краткосрочной перспективе, но вы будете платить технический долг много раз, так как ваша кодовая база развивается.
  • Когда вы определяете взаимодействие между модулями, подумайте о том, как сделать каждый из которых можно проверить изолированно. Например, наличие четко определенного API для извлечения данных из сети упростит проверку модуля, который сохраняет эти данные в локальной базе данных. Если вместо этого вы смешаете логику с этими двумя модулями в одном месте или посыпьте свой сетевой код по всей вашей кодовой базе, это будет намного сложнее — если не невозможно — протестировать.
  • Ядро вашего приложения — вот что отличает его от остальных. Не тратьте время на изобретать колесо или писать тот же шаблонный код снова и снова. Вместо этого сосредоточьте свою умственную энергию на том, что делает ваше приложение уникальным, и пусть Android Architecture Components и другие рекомендуемые библиотеки обрабатывают повторяющийся шаблон (repetitive boilerplate).
  • Сохраняйте как можно больше актуальных и свежих данных, чтобы ваше приложение можно было использовать, когда устройство находится в автономном режиме. Хотя вы можете наслаждаться постоянным и высокоскоростным подключением, ваши пользователи могут этого не делать.Ваш репозиторий должен обозначать один источник данных как единственный источник правды. Всякий раз, когда ваше приложение должно получить доступ к этой части данных, оно всегда должно происходить из единственного источника правды. Для получения дополнительной информации см. единый правдивый источник.

 

Читать ещё :   Руководство по архитектуре приложений. Часть 1

Приложение: отображение статуса сети

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

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

Ниже приведен пример реализации:

См. оригинальный код

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

См. оригинальный код

Он начинается с наблюдения (observing) за базой данных для ресурса. Когда запись загружается из базы данных в первый раз, NetworkBoundResource проверяет, является ли результат достаточно хорошим для отправки и / или он должен быть получен из сети. Обратите внимание, что оба они могут произойти одновременно, поскольку вы, вероятно, хотите отображать кэшированные данные пока они обновляются из сети.

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

Примечание. После сохранения новых данных на диск мы повторно инициализируем поток (stream) из базы данных, хотя обычно нам этого не нужно, поскольку база данных отправит изменение. С другой стороны, полагаться на базу данных для отправки изменений будет полагаться на побочные эффекты, которые не являются хорошими, потому что они могут сломаться, если база данных может избежать отправки изменений, если данные не изменились. Мы также не хотим отправлять результат, полученный из сети, потому что это будет идти против единственного истинного источника (возможно, в базе данных есть триггеры, которые изменят значение при сохранении). Мы также не хотели отправлять SUCCESS без новых данных, так как он посылал бы неверную информацию клиенту.

Обратите внимание, что класс выше определяет два типа параметров (ResultType, RequestType), поскольку тип данных, возвращаемый API, может не соответствовать типу данных, используемому локально.

Также обратите внимание, что приведенный выше код использует ApiResponse для сетевого запроса. ApiResponse — простая оболочка класса Retrofit2.Call для преобразования своего ответа в LiveData.