Создайте приложение, работающее в офлайн-режиме

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

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

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

  • Ограниченная пропускная способность интернета
  • Временные перебои в подключении, например в лифте или туннеле.
  • Периодический доступ к данным, например планшеты, поддерживающие только Wi-Fi.

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

  • Оставаться доступными даже при отсутствии стабильного подключения к сети.
  • Показывайте пользователям локальные данные сразу, не дожидаясь, пока завершится или прервется первый сетевой вызов.
  • Получайте данные с учетом состояния батареи и сети. Например, запрашивайте данные только при оптимальных условиях, таких как зарядка или подключение к Wi-Fi.

Приложение, соответствующее этим критериям, часто называют приложением, ориентированным на офлайн-режим.

Как разработать приложение, ориентированное на офлайн-режим

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

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

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

Моделирование данных в приложении, работающем в автономном режиме

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

  • Источник данных о товарах в местных магазинах
  • Источник данных сети
Слой данных, ориентированный на офлайн-режим, состоит из локальных и сетевых источников данных.
Рисунок 1. Репозиторий, ориентированный на офлайн-режим.

Источник данных о товарах в местных магазинах

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

  • Источники структурированных данных, например реляционные базы данных, такие как Room.
  • Неструктурированные источники данных, например буферы протокола с DataStore.
  • Простые файлы

Источник данных сети

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

Сравнение выборочной и управляемой синхронизации

Создать слой данных, ориентированный на офлайн-режим, можно двумя основными способами:

  • Собственный стек синхронизации. Вы поддерживаете локальные базы данных с помощью Room или файлов в локальном хранилище, создаете собственные сетевые клиенты и планируете сверку с помощью WorkManager. Этот подход обеспечивает максимальный контроль над схемами и пользовательскими алгоритмами разрешения конфликтов.
  • Управляемые облачные сервисы. Такие решения, как Cloud Firestore, могут предоставлять универсальный локальный кеш и облачный поток в реальном времени. SDK автоматически обрабатывает кеширование диска, автономную очередь записи и двунаправленную синхронизацию, доставляя потоки данных в реальном времени непосредственно в ваш репозиторий. Для реляционных бэкендов PostgreSQL Firebase SQL Connect генерирует типобезопасные клиентские SDK Kotlin, которые легко интегрируются с вашими репозиториями. Для неструктурированных данных (например, медиафайлов, больших файлов или резервных копий) Cloud Storage for Firebase предоставляет облачное хранилище объектов с операциями загрузки и скачивания, которые автоматически приостанавливаются и возобновляются при сбоях в сети.

Раскрытие ресурсов

Локальные и сетевые источники данных могут принципиально отличаться тем, как ваше приложение может читать и записывать в них. Запрос к локальному источнику данных может быть быстрым и гибким, например, при использовании SQL-запросов. Напротив, сетевые источники данных могут быть медленными и ограниченными, например, при постепенном доступе к ресурсам RESTful по идентификатору. В результате каждому источнику данных часто требуется собственное представление предоставляемых им данных. Поэтому у локального источника данных и источника данных сети могут быть собственные модели.

Структура каталогов, показанная ниже, поможет вам понять эту концепцию. AuthorEntity представляет автора, считанного из локальной базы данных приложения, а NetworkAuthor – автора, сериализованного по сети:

data/
├─ local/
│ ├─ entities/
│ │ ├─ AuthorEntity
│ ├─ dao/
│ ├─ NiADatabase
├─ network/
│ ├─ NiANetwork
│ ├─ models/
│ │ ├─ NetworkAuthor
├─ model/
│ ├─ Author
├─ repository/

Вот сведения о файлах AuthorEntity и NetworkAuthor:

/**
 * Network representation of [Author]
 */
@Serializable
data class NetworkAuthor(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

/**
 * Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
 * It has a many-to-many relationship with both entities
 */
@Entity(tableName = "authors")
data class AuthorEntity(
    @PrimaryKey
    val id: String,
    val name: String,
    @ColumnInfo(name = "image_url")
    val imageUrl: String,
    @ColumnInfo(defaultValue = "")
    val twitter: String,
    @ColumnInfo(name = "medium_page", defaultValue = "")
    val mediumPage: String,
    @ColumnInfo(defaultValue = "")
    val bio: String,
)

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

/**
 * External data layer representation of a "Now in Android" Author
 */
data class Author(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

Затем сетевая модель может определить метод расширения для преобразования в локальную модель, а локальная модель аналогичным образом имеет метод для преобразования во внешнее представление, как показано в следующем фрагменте:

/**
 * Converts the network model to the local model for persisting
 * by the local data source
 */
fun NetworkAuthor.asEntity() = AuthorEntity(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

/**
 * Converts the local model to the external model for use
 * by layers external to the data layer
 */
fun AuthorEntity.asExternalModel() = Author(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

Чтение

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

В следующем фрагменте кода OfflineFirstTopicRepository возвращает Flow для всех своих API чтения. Это позволяет обновлять читателей при получении обновлений из источника данных сети. Другими словами, он позволяет OfflineFirstTopicRepository отправлять изменения, когда его локальный источник данных становится недействительным. Поэтому каждый читатель OfflineFirstTopicRepository должен быть готов к обработке изменений данных, которые могут быть вызваны восстановлением сетевого подключения к приложению. Кроме того, OfflineFirstTopicRepository считывает данные непосредственно из локального источника данных. Оно может уведомлять своих читателей об изменениях данных, только сначала обновив свой локальный источник данных.

class TopicsViewModel(
    offlineFirstTopicsRepository: OfflineFirstTopicsRepository
) : ViewModel() {

    val topics: StateFlow<List<Topic>> = offlineFirstTopicsRepository.getTopicsStream()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )
}

В приложении Jetpack Compose используйте ViewModel для соединения слоя данных и пользовательского интерфейса. В ViewModel преобразуйте Flow в StateFlow с помощью оператора stateIn. Затем composable-функции собирают эти состояния с помощью collectAsStateWithLifecycle() и автоматически управляют подписками с учетом жизненного цикла.

Подробнее collectAsStateWithLifecycle() рассказывается в статье Состояние и Jetpack Compose.

Стратегии обработки ошибок

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

Источник данных о товарах в местных магазинах

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

Оператор catch можно использовать в ViewModel следующим образом:

class AuthorViewModel(
    authorsRepository: AuthorsRepository,
    ...
) : ViewModel() {
   private val authorId: String = ...

   // Observe author information
    private val authorStream: Flow<Author> =
        authorsRepository.getAuthorStream(
            id = authorId
        )
        .catch { emit(Author.empty()) }
}

Для более надежного подхода рассмотрите решение LCE (Loading Content Error). В LCE, когда происходит сбой при чтении, вы показываете состояние ошибки. Обычно для этого состояния интерфейса моделируются как запечатанные классы Kotlin.

// Define the LCE UI state
sealed interface AuthorUiState {
    data object Loading : AuthorUiState
    data class Success(val author: Author) : AuthorUiState
    data object Error : AuthorUiState
}

class AuthorViewModel(
    authorsRepository: AuthorsRepository,
    ...
) : ViewModel() {
    private val authorId: String = ...

    // Observe author information and map to LCE state
    val authorUiState: StateFlow<AuthorUiState> =
        authorsRepository.getAuthorStream(id = authorId)
            .map<Author, AuthorUiState> { author ->
                AuthorUiState.Success(author)
            }
            .catch { emit(AuthorUiState.Error) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = AuthorUiState.Loading
            )
}

Источник данных сети

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

Экспоненциальная задержка

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

Чтение данных с экспоненциальной выдержкой
Рисунок 2. Чтение данных с экспоненциальной выдержкой.

Приложение считается не соответствующим этому требованию, если:

  • Тип ошибки, указанный источником данных сети. Например, повторите сетевые вызовы, которые возвращают ошибку, указывающую на отсутствие подключения. Не повторяйте неавторизованные HTTP-запросы, пока не будут доступны правильные учетные данные.
  • Максимально допустимое количество повторных попыток.
Мониторинг сетевого подключения

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

Чтение данных с помощью сетевых мониторов и очередей
Рисунок 3. Очереди чтения с мониторингом сети.

Запись

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

interface UserDataRepository {
    /**
     * Updates the bookmarked status for a news resource
     */
    suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}

В предыдущем фрагменте асинхронный API выбора – это Coroutines, поскольку метод приостанавливается.

Стратегии написания текстов

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

Запись только в онлайн-режиме

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

Запись только онлайн
Рисунок 4. Запись только в онлайн-режиме.

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

  • Если для записи данных приложению требуется доступ к интернету, вы можете не показывать пользователю интерфейс для записи данных или отключить его.
  • Чтобы сообщить пользователю, что он офлайн, можно использовать AlertDialog, который нельзя закрыть, или Snackbar.

Записи в очереди

Когда у вас есть объект, который вы хотите записать, вставьте его в очередь. Когда приложение снова подключится к интернету, очистите очередь с экспоненциальной выдержкой. На Android очистка офлайн-очереди – это постоянная задача, которая часто делегируется WorkManager.

Очереди записи с повторными попытками
Рисунок 5. Очереди записи с повторными попытками.

Этот подход хорошо подходит для следующих сценариев:

  • Записывать данные в сеть необязательно.
  • Транзакция не зависит от времени.
  • Если операция не удалась, сообщать об этом пользователю необязательно.

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

Отложенная запись

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

Отложенная запись с мониторингом сети
Рисунок 6. Отложенная запись.

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

Синхронизация и разрешение конфликтов

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

  • Синхронизация на основе запросов
  • Синхронизация на основе push-уведомлений

Синхронизация на основе запросов

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

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

Синхронизация на основе запросов
Рисунок 7. Синхронизация на основе запросов. Устройство А получает доступ к ресурсам только для экранов А и Б, а устройство Б – только для экранов Б, В и Г.

Представьте приложение, в котором токены страниц используются для получения элементов в бесконечном прокручиваемом списке для определенного экрана. Реализация может лениво обращаться к сети, сохранять данные в локальном источнике данных, а затем считывать их из локального источника данных, чтобы представить информацию пользователю. В случае отсутствия сетевого подключения репозиторий может запрашивать данные только из локального источника данных. Этот шаблон используется в библиотеке Jetpack Paging с API RemoteMediator.

class FeedRepository(...) {

    fun feedPagingSource(): PagingSource<FeedItem> { ... }
}

class FeedViewModel(
    private val repository: FeedRepository
) : ViewModel() {
    private val pager = Pager(
        config = PagingConfig(
            pageSize = NETWORK_PAGE_SIZE,
            enablePlaceholders = false
        ),
        remoteMediator = FeedRemoteMediator(...),
        pagingSourceFactory = feedRepository::feedPagingSource
    )

    val feedPagingData = pager.flow
}

Преимущества и недостатки синхронизации на основе запросов приведены в таблице ниже.

Преимущества Недостатки
Относительно просто реализовать. Может расходовать много трафика. Это связано с тем, что повторные посещения пункта назначения навигации вызывают ненужное повторное получение неизмененной информации. Эту проблему можно решить с помощью кеширования. Это можно сделать на уровне пользовательского интерфейса с помощью оператора cachedIn или на сетевом уровне с помощью кеша HTTP.
Ненужные данные не извлекаются. Плохо масштабируется с реляционными данными, поскольку извлекаемая модель должна быть самодостаточной. Если синхронизируемая модель зависит от других моделей, которые необходимо получить для ее заполнения, проблема с большим объемом данных, упомянутая ранее, становится ещё более значительной. Кроме того, это может привести к зависимостям между хранилищами родительской и вложенной моделей.

Синхронизация на основе push-уведомлений

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

Синхронизация на основе push-уведомлений
Рисунок 8. Синхронизация на основе push-уведомлений. Сеть уведомляет приложение об изменении данных, и приложение реагирует на это, получая измененные данные.

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

class UserDataRepository(...) {

    suspend fun synchronize() {
        val userData = networkDataSource.fetchUserData()
        localDataSource.saveUserData(userData)
    }
}

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

Преимущества и недостатки синхронизации на основе push-уведомлений приведены в таблице ниже.

Преимущества Недостатки
Приложение может оставаться офлайн неограниченное время. Управление версиями данных для разрешения конфликтов – непростая задача.
Минимальный объем данных. Приложение получает только измененные данные. При синхронизации необходимо учитывать проблемы с записью.
Хорошо подходит для реляционных данных. Каждое хранилище отвечает за получение данных только для той модели, которую оно поддерживает. Источник данных сети должен поддерживать синхронизацию.

Гибридная синхронизация

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

В конечном счете выбор синхронизации с приоритетом офлайн-режима зависит от требований к продукту и доступной технической инфраструктуры.

Разрешение конфликтов

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

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

Приоритет у последней записи

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

Разрешение конфликтов по принципу &quot;последняя запись побеждает&quot;
Рисунок 9. "Последняя запись побеждает". Источник истины для данных определяется последним объектом, который записывает данные.

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

WorkManager в приложениях, работающих в офлайн-режиме

В обеих стратегиях чтения и записи, описанных ранее, есть две общие утилиты:

  • Очереди
    • Чтение. Используется для отсрочки чтения до тех пор, пока не будет доступно подключение к сети.
    • Запись. Используется для отсрочки записи до подключения к сети и для повторной постановки записи в очередь.
  • Мониторы сетевого подключения
    • Чтение. Используется как сигнал для очистки очереди чтения, когда приложение подключено, и для синхронизации.
    • Запись. Используется как сигнал для очистки очереди записи, когда приложение подключено, и для синхронизации.

Оба случая – примеры постоянных задач, для которых отлично подходит WorkManager.

Например, в приложении Now in Android WorkManager используется и как очередь чтения, и как монитор сети при синхронизации локального источника данных. При запуске приложение выполняет следующие действия:

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

Эти действия показаны на следующей диаграмме:

Синхронизация данных в приложении Now in Android
Рисунок 10. Синхронизация данных в приложении Now in Android.

Постановка в очередь задачи синхронизации с помощью WorkManager выполняется путем указания ее как уникальной задачи с помощью KEEP ExistingWorkPolicy:

class SyncInitializer : Initializer<Sync> {
   override fun create(context: Context): Sync {
       WorkManager.getInstance(context).apply {
           // Queue sync on app startup and ensure only one
           // sync worker runs at any time
           enqueueUniqueWork(
               SyncWorkName,
               ExistingWorkPolicy.KEEP,
               SyncWorker.startUpSyncWork()
           )
       }
       return Sync
   }
}

SyncWorker.startupSyncWork() определяется следующим образом:


/**
 Create a WorkRequest to call the SyncWorker using a DelegatingWorker.
 This allows for dependency injection into the SyncWorker in a different
 module than the app module without having to create a custom WorkManager
 configuration.
*/
fun startUpSyncWork() = OneTimeWorkRequestBuilder<DelegatingWorker>()
    // Run sync as expedited work if the app is able to.
    // If not, it runs as regular work.
   .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
   .setConstraints(SyncConstraints)
    // Delegate to the SyncWorker.
   .setInputData(SyncWorker::class.delegatedData())
   .build()

val SyncConstraints
   get() = Constraints.Builder()
       .setRequiredNetworkType(NetworkType.CONNECTED)
       .build()

В частности, Constraints, определенные в SyncConstraints, требуют, чтобы NetworkType был NetworkType.CONNECTED. То есть он ждет, пока сеть не станет доступной, прежде чем запуститься.

Когда сеть становится доступной, Worker очищает уникальную очередь заданий, указанную в SyncWorkName, делегируя их соответствующим экземплярам Repository. Если синхронизация не удалась, метод doWork() возвращает Result.retry(). WorkManager автоматически повторит попытку синхронизации с экспоненциальной выдержкой. В противном случае возвращается значение Result.success(), и синхронизация завершается.

class SyncWorker(...) : CoroutineWorker(appContext, workerParams), Synchronizer {

    override suspend fun doWork(): Result = withContext(ioDispatcher) {
        // First sync the repositories in parallel
        val syncedSuccessfully = awaitAll(
            async { topicRepository.sync() },
            async { authorsRepository.sync() },
            async { newsRepository.sync() },
        ).all { it }

        if (syncedSuccessfully) Result.success()
        else Result.retry()
    }
}

Образцы

Примеры приложений, работающих в офлайн-режиме, можно найти в следующих руководствах Google: Ознакомьтесь с ними, чтобы увидеть, как эти рекомендации применяются на практике: