Синхронизация в Android приложениях. Часть вторая


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

Вторым условием является наличие ContentProvider’а, процесс написания которого разжеван в документации. Признаться честно, мне не очень нравится как там это описано: все кажется громоздким и сложным. Поэтому немного повелосипедим и еще разок пережуем эту тему. Можно было бы обойтись и провайдером-заглушкой, но мы люди серьезные и будем использовать всю мощь этого инструмента.

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

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

Чтобы настраивать автоматическую синхронизацию через определенные интервалы времени, нам потребуется экран настроек этого добра. Желательно, чтобы доступ к нему был не только из приложения, но и из системного экрана нашего аккаунта (как на скриншоте к статье). Используем для этого PreferenceFragment’ы. С функциональностью определились, приступим.

Account

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


 

Нам потребуется немного модифицировать файл res/xml/authenticator.xml, чтобы добавить ему возможность перехода на экран настроек синхронизации. Добавим параметр android:accountPreferences с указанием файла, из которого эти самые Preferences нужно подтянуть. При клике на элемент «Синхронизация» будет открываться SyncSettingsActivity нашего приложения.



 

ContentProvider

Наш провайдер будет оберткой над SQLite базой данных, в которой мы будем хранить новости. Остановимся немного и подробнее рассмотрим его реализацию. Провайдер умеет работать с двумя типами Uri:
content://authority/table — выборка всех значений из таблицы
content://authority/table/_id — выборка одного значения по primary key
в методе onCreate с помощью PackageManager.getProviderInfo мы получаем authority для этого провайдера и регистрируем их в SQLiteUriMatcher. Что происходит в методах: провайдер берет из uri название таблицы, затем из SCHEMA для этой таблицы берется конкретная реализация SQLiteTableProvider (провайдера для таблицы). У SQLiteTableProvider вызываются соответствующие методы (по сути, происходит проксирование вызова). Такой подход позволяет для каждой таблицы кастомизировать работу с данными. В зависимости от результатов, ContentResolver (а с ним и наше приложение) получает уведомление об изменении данных. Для uri типа content://authority/table/_id переписывается условие where, чтобы обеспечить работу по первичному ключу. При желании, можно немного докрутить этот провайдер и вынести в библиотечный класс. Как показывает практика, такой реализации достаточно для 90% задач (остальные 10 — full text search, like nocase search).


 

Теперь нужно прописать провайдер в AndroidManifest.xml и обратить внимание на параметр android:syncable=«true». Этот флаг говорит о том, что наш провайдер поддерживает синхронизацию.


 

Также представляет интерес класс FeedProvider — реализация SQLiteTableProvider для работы с лентами новостей. При вставке (!) в эту таблицу (подписка на новую ленту) будет вызываться принудительная синхронизация. За это отвечает метод onContentChanged, который дергается из SQLiteContentProvider при изменении данных (insert/update/delete). Для таблицы будет создан триггер (onCreate), который будет удалять связанные с лентой новости. Почему стоит вызывать синхронизацию только при вставке? Чтобы избежать зацикливания, потому что наш провайдер будет обновлять таблицу (добавлять заголовок, ссылку на картинку, дату публикации и т.д.). Дополнительные параметры синхронизации передаются через syncExtras.


За сим кроличья норка заканчивается, и начинается зазеркалье.

SyncAdapter

Перед тем как ворваться в процесс создания SyncAdapter’а, давайте подумаем, зачем вообще это нужно, какие преимущества дает. Если верить документации, то, как минимум, мы получим:

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

Уже неплохо, правда? Добавим, что при использовании ContentProvider’а, мы можем запускать синхронизацию при изменении данных в нем. Это полностью снимает с нас необходимость отслеживать изменение данных в приложении и выполнять синхронизацию в «ручном режиме».

Процесс интеграции этого добра очень похож на процесс интеграции своего аккаунта в приложение. Нам потребуется реализация AbstractThreadedSyncAdapter и Service для интеграции в систему. AbstractThreadedSyncAdapter имеет всего один абстрактный метод onPerformSync, в котором и происходит вся магия. Что же именно тут происходит? В зависимости от переданных extras-параметров (помните syncExtras в FeedProvider.onContentChanged) синхронизируется или одна лента или все. В общем, мы выбираем из базы ленты, парсим rss по ссылке и складываем в нашу базу с помощью ContentProviderClient provider. Для информирования системы о статусе (количестве обновлений, ошибок и т.д.) синхронизации используется SyncResult syncResult.


 

Реализация SyncService тоже очень проста. Все, что нам нужно это отдать IBinder объект системе, для связи с нашим SyncAdapter’ом. Чтобы система поняла, что за адаптер мы регистрируем, понадобится xml-мета файл sync_adapter.xml, а также прописать все это добро в AndroidManifest.xml.


 

 

А теперь пример

image
Вот так будет выглядеть окно со списком лент. Как вы помните, мы договорились использовать SwipeRefreshLayoutдля принудительной синхронизации и визуализации этого процесса. Список лент FeedList.java и список новостей NewsList.java будут наследоваться от общего родителя SwipeToRefreshList.java.

Для отслеживания статуса синхронизации, необходимо зарегистрировать Observer в ContentResolver’е (метод SwipeToRefreshList.onResume()). Для этого служит метод ContentResolver.addStatusChangeListener. В методе SwipeToRefreshList.onStatusChanged проверяем статус синхронизации с помощью метода ContentResolver.isSyncActiveи передаем этот результат в метод SwipeToRefreshList.onSyncStatusChanged, который будет переопределен наследниками. Все, что будет делать этот метод — прятать/показывать полоску прогресса у SwipeRefreshLayout. Так как SyncStatusObserver.onStatusChanged вызывается из отдельного потока, оборачиваем результат в хэндлер. Метод SwipeToRefreshList.onRefresh в потомках запускает принудительную синхронизацию с помощью ContentResolver.requestSync.

Все списки загружаются и отображаются с помощью CursorLoader + CursorAdapter, которые тоже замечательно работают в связке с ContentProvider’ом, избавляя нас от необходимости следить за актуальностью списков. Как только новый элемент будет добавлен в провайдер, все CursorLoader’ы получат уведомления и актуализируют данные в CursorAdapter’ах.


 

image
Итак, с принудительной синхронизацией разобрались. Но самый сок — синхронизация автоматическая. Помните, мы добавляли в наш аккаунт поддержку экрана настроек? Хорошая практика — не заставлять пользователя совершать лишних действий. Поэтому доступ к этому экрану продублирован кнопкой в экшен баре.

Что он из себя представляет — видно слева. Технически же — это активити с одним PreferenceFragment’ом (SyncSettings.java), настройки которого берутся из res/xml/sync_prefs.xml.

Изменение параметров отслеживаем в методе onSharedPreferenceChanged (реализация OnSharedPreferenceChangeListener). Для включения периодической синхронизации существует метод ContentResolver.addPeriodicSync, для отключения, как ни странно, — ContentResolver.removePeriodicSync. Для обновления интервала синхронизации используется так же метод ContentResolver.addPeriodicSync. Потому что, как говорит документация к этому методу: «If there is already another periodic sync scheduled with the account, authority and extras then a new periodic sync won’t be added, instead the frequency of the previous one will be updated.» (если синхронизация уже запланирована, extra и authority не будут добавлены в новую синхронизацию, вместо этого будет обновлен интервал предыдущей).

 

 


 

 

Собрав все это в кучу, мы получаем рабочее приложение, со всеми плюшками, которые предоставляет нам система Android. За кадром осталось много всего вкусного, но и этого достаточно, чтобы понять мощь SyncAdapter Framework’а.

Вот, вроде бы и все. Полные исходники проекта можно взять тут. Благодарю за внимание. Конструктивная критика приветствуется.

Синхронизация в Android приложениях. Часть первая.

Оставьте комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *