Tworzenie aplikacji offline

Aplikacja offline to aplikacja, która jest w stanie wykonywać wszystkie lub kluczowe funkcje bez dostępu do internetu. Oznacza to, że może realizować część lub całość swojej logiki biznesowej offline.

Uwagi dotyczące tworzenia aplikacji działających offline zaczynają się w warstwie danych, która zapewnia dostęp do danych aplikacji i logiki biznesowej. Aplikacja może co jakiś czas odświeżać te dane ze źródeł zewnętrznych. Aby na bieżąco korzystać z danych, konieczne może być wywołanie zasobów sieciowych.

Dostępność sieci nie zawsze jest gwarantowana. Na urządzeniach często występują zakłócenia lub wolne połączenie sieciowe. Mogą wystąpić następujące problemy:

  • Ograniczona przepustowość internetu
  • Przejściowe przerwy w połączeniach, na przykład podczas podróży w windą lub w tunelu.
  • Okresowy dostęp do danych. np. tablety obsługujące tylko sieć Wi-Fi.

Bez względu na przyczynę często w takich warunkach aplikacja może działać prawidłowo. Aby aplikacja działała prawidłowo w trybie offline, powinna mieć do dyspozycji te funkcje:

  • Działaj bez stabilnego połączenia sieciowego.
  • Przedstaw użytkownikom dane lokalne od razu, zamiast czekać na zakończenie lub nieudane wywołanie sieciowe.
  • Pobieraj dane w sposób uwzględniający stan baterii i dane. Na przykład może to robić tylko w optymalnych warunkach, np. podczas ładowania lub połączenia z Wi-Fi.

Aplikacja, która może spełnić powyższe kryteria, jest często nazywana aplikacją działającą offline.

Projektowanie aplikacji offline

Projektując aplikację offline, należy zacząć od warstwy danych i dwóch głównych operacji, które można wykonać na danych aplikacji:

  • Odczyty: pobieranie danych do wykorzystania przez inne części aplikacji, np. wyświetlanie informacji użytkownikowi.
  • Zapisy: zachowywanie danych wejściowych użytkownika do późniejszego pobrania.

Repozytoria w warstwie danych odpowiadają za łączenie źródeł danych, aby dostarczać dane aplikacji. W aplikacji działającej w trybie offline musi istnieć co najmniej 1 źródło danych, które nie wymaga dostępu do sieci do wykonywania najważniejszych zadań. Jednym z tych zadań jest odczyt danych.

Modelowanie danych w aplikacji offline

Aplikacja działająca w trybie offline ma co najmniej 2 źródła danych dla każdego repozytorium, które korzysta z zasobów sieciowych:

  • Lokalne źródło danych
  • Źródło danych sieci
Warstwa danych działająca w trybie offline składa się zarówno z lokalnych, jak i sieciowych źródeł danych
Rys. 1. Repozytorium działające w trybie offline

Lokalne źródło danych

Lokalne źródło danych jest kanonicznym źródłem informacji aplikacji. Powinno ono być wyłącznym źródłem wszelkich danych odczytywanych przez wyższe warstwy aplikacji. Zapewnia to spójność danych między stanami połączenia. Lokalne źródło danych jest często wspierane przez pamięć masową, która pozostaje na dysku. Oto kilka typowych sposobów przechowywania danych na dysku:

  • źródła uporządkowanych danych, np. relacyjne bazy danych, np. Room;
  • Nieuporządkowane źródła danych. Przykładem może być buforowanie protokołów w Datastore.
  • Proste pliki

Źródło danych sieci

Sieciowe źródło danych to rzeczywisty stan aplikacji. Lokalne źródło danych najlepiej synchronizuje się ze źródłem danych sieci. Mogą też pozostawać w tyle. W takim przypadku aplikacja musi zostać zaktualizowana po powrocie do trybu online. Natomiast źródło danych z sieci może być opóźnione w stosunku do lokalnego źródła danych, dopóki aplikacja nie będzie w stanie go zaktualizować po przywróceniu połączenia. Domena i warstwy interfejsu aplikacji nie powinny nigdy utożsamiać się bezpośrednio z warstwą sieciową. Za komunikację z hostem (repository) i aktualizację lokalnego źródła danych odpowiada dostawca hostingu.

Ujawnianie zasobów

Lokalne i sieciowe źródła danych mogą się zasadniczo różnić pod względem sposobu, w jaki aplikacja może je odczytywać i zapisywać. Wykonywanie zapytań dotyczących lokalnego źródła danych może być szybkie i elastyczne, np. w przypadku zapytań SQL. Sieciowe źródła danych mogą natomiast działać wolno i ograniczać, na przykład podczas przyrostowego dostępu do zasobów REST według identyfikatora. W efekcie każde źródło danych musi mieć własną reprezentację na temat dostarczanych przez nie informacji. Lokalne źródło danych i sieciowe źródło danych może więc mieć własne modele.

Struktura katalogów poniżej odzwierciedla to działanie. Element AuthorEntity to identyfikator autora odczytywany z lokalnej bazy danych aplikacji, a NetworkAuthor – autor z serii w sieci:

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

Szczegółowe informacje dotyczące AuthorEntity i 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,
)

Warto zachować zarówno warstwę AuthorEntity, jak i element wewnętrzny NetworkAuthor w warstwie danych, a także udostępnić trzeci typ do wykorzystania przez warstwy zewnętrzne. Chroni to zewnętrzne warstwy przed drobnymi zmianami w lokalnych i sieciowych źródłach danych, które nie wpływają w istotny sposób na działanie aplikacji. Oto przykład:

/**
 * 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,
)

Model sieci może następnie zdefiniować metodę rozszerzenia, która będzie konwertować ją na model lokalny. Podobnie model lokalny ma taką, która przekształca ją w reprezentację zewnętrzną, jak pokazano poniżej:

/**
 * 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,
)

Odczyty

Odczyty to podstawowa operacja na danych aplikacji w przypadku aplikacji offline. Dlatego musisz zadbać o to, aby aplikacja mogła odczytywać dane i wyświetlać je, gdy tylko pojawią się nowe dane. Aplikacja, która może to robić, jest aplikacją reaktywną, ponieważ udostępnia interfejsy API do odczytu z typami obserwowalnymi.

We fragmencie kodu poniżej OfflineFirstTopicRepository zwraca Flows w przypadku wszystkich interfejsów API do odczytu. Dzięki temu może ona aktualizować swoje czytniki po otrzymaniu aktualizacji ze źródła danych z sieci. Inaczej mówiąc, umożliwia on wprowadzenie zmian OfflineFirstTopicRepository, gdy jego lokalne źródło danych zostanie unieważnione. Dlatego każdy czytnik OfflineFirstTopicRepository musi być przygotowany na zmiany w danych, które mogą zostać aktywowane po przywróceniu połączenia sieciowego do aplikacji. Dodatkowo OfflineFirstTopicRepository odczytuje dane bezpośrednio z lokalnego źródła danych. Może powiadamiać czytelników o zmianach danych tylko przez aktualizację lokalnego źródła danych.

class OfflineFirstTopicsRepository(
    private val topicDao: TopicDao,
    private val network: NiaNetworkDataSource,
) : TopicsRepository {

    override fun getTopicsStream(): Flow<List<Topic>> =
        topicDao.getTopicEntitiesStream()
            .map { it.map(TopicEntity::asExternalModel) }
}

Strategie obsługi błędów

W aplikacjach działających w trybie offline dostępne są wyjątkowe sposoby obsługi błędów w zależności od źródeł danych, w których się pojawiają. W kolejnych sekcjach opisujemy te strategie.

Lokalne źródło danych

Błędy podczas odczytu z lokalnego źródła danych powinny występować rzadko. Aby chronić czytelników przed błędami, użyj operatora catch w elemencie Flows, z którego odczytujący zbiera dane.

Aby użyć operatora catch w elemencie ViewModel, wykonaj te czynności:

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()) }
}

Źródło danych sieci

Jeśli przy odczytywaniu danych z sieciowego źródła danych wystąpią błędy, aplikacja musi użyć metody heurystycznej, by ponowić próbę pobrania danych. Typowe metody heurystyczne to:

Ponawianie wykładnicze

W przypadku wykładniczego ponowienia aplikacja podejmuje próby odczytu ze źródła danych sieciowych w rosnących odstępach czasu, aż to się uda, lub w innych warunkach, które wymagają zatrzymania.

Odczytywanie danych ze wzrastającym czasem ponowienia
Rysunek 2. Odczytywanie danych ze wzrastającym czasem do ponowienia

Kryteria oceny, czy aplikacja powinna nadal się wycofywać, to:

  • Rodzaj błędu wskazanego przez źródło danych sieci. Warto na przykład spróbować ponownie wykonać wywołania sieciowe, które zwracają błąd wskazujący na brak połączenia. I na odwrót: nie próbuj ponownie wysyłać żądań HTTP, które nie są autoryzowane, dopóki nie będą dostępne odpowiednie dane logowania.
  • Maksymalna dozwolona liczba ponownych prób.
Monitorowanie połączeń sieciowych

W tym podejściu żądania odczytu są umieszczane w kolejce, dopóki aplikacja nie ma pewności, że może połączyć się z sieciowym źródłem danych. Po ustanowieniu połączenia żądanie odczytu jest usuwane z kolejki, a odczyt danych i lokalne źródło danych są aktualizowane. Na Androidzie ta kolejka może być utrzymywana w bazie danych Room i opróżniana w ramach stałej pracy za pomocą WorkManagera.

Odczytywanie danych za pomocą monitorów sieci i kolejek
Rys. 3. Kolejki odczytu w ramach monitorowania sieci

Zapisy

Chociaż zalecanym sposobem odczytu danych w aplikacji działającej w trybie offline jest użycie typów obserwowalnych, odpowiednikiem dla interfejsów API zapisu są asynchroniczne interfejsy API, takie jak funkcje zawieszania. Pozwala to uniknąć zablokowania wątku interfejsu i ułatwia obsługę błędów, ponieważ zapis w aplikacjach działających offline może zakończyć się niepowodzeniem po przekroczeniu granic sieci.

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

We fragmencie kodu powyżej wybrany asynchroniczny interfejs API to Coroutines, ponieważ metoda powyżej zawiesza się.

Strategie tworzenia

Pisząc dane w aplikacjach offline, masz do wyboru 3 strategie. To, co wybierzesz, zależy od typu zapisywanych danych i wymagań aplikacji:

Zapisy tylko online

Podejmuje próbę zapisania danych na granicy sieci. Jeśli operacja się uda, zaktualizuj lokalne źródło danych. W przeciwnym razie wyślij wyjątek i pozwól elementowi wywołującemu udzielić właściwej odpowiedzi.

Zapisy tylko online
Rys. 4. Zapisy tylko online

Ta strategia jest często stosowana do zapisywania transakcji, które muszą mieć miejsce online w czasie zbliżonym do rzeczywistego. Może to być na przykład przelew bankowy. Zapisy mogą się kończyć niepowodzeniem, więc często konieczne jest zawiadomienie użytkownika o niepowodzeniu zapisu lub uniemożliwienie użytkownikowi próby zapisania danych. Oto kilka strategii, które możesz zastosować w takich sytuacjach:

  • Jeśli do zapisu danych aplikacja wymaga dostępu do internetu, może nie wyświetlać użytkownikowi interfejsu, który umożliwia zapisywanie danych, lub przynajmniej go wyłączyć.
  • Możesz użyć wyskakującego okienka, którego użytkownik nie może zamknąć, lub komunikatu tymczasowego, aby powiadomić go, że jest offline.

Zapisy w kolejce

Gdy masz obiekt, który chcesz zapisać, wstaw go do kolejki. Kontynuuj, aby opróżnić kolejkę ze wzrastającym czasem do wyłączenia, gdy aplikacja wróci do trybu online. Na Androidzie opróżnianie kolejki offline jest trwałą pracą, którą często przekazuje WorkManager.

Kolejki zapisu z ponownymi próbami
Rysunek 5. Kolejki zapisu z ponownymi próbami

To dobre rozwiązanie, jeśli:

  • Nie ma znaczenia, czy dane będą kiedykolwiek zapisywane w sieci.
  • Transakcja nie jest zależna od czasu.
  • Nie jest ważne, aby użytkownik został poinformowany o niepowodzeniu operacji.

Przypadki użycia tego podejścia obejmują zdarzenia analityczne i logowanie.

Leniwe pisanie

Najpierw zapisz dane w lokalnym źródle danych, a następnie umieść zapis w kolejce, aby powiadomić sieć jak najszybciej. Nie jest to proste, ponieważ przy powrocie aplikacji do trybu online mogą wystąpić konflikty między siecią a lokalnymi źródłami danych. Więcej szczegółów na ten temat znajdziesz w następnej sekcji.

Leniwe zapisy z monitorowaniem sieci
Rys. 6. Leniwe zapisywanie

To podejście jest właściwym wyborem, gdy dane są kluczowe dla aplikacji. Na przykład w aplikacji do tworzenia list zadań do wykonania w trybie offline ważne jest, aby wszystkie zadania dodawane przez użytkownika offline były przechowywane lokalnie, aby uniknąć ryzyka utraty danych.

Synchronizacja i rozwiązywanie konfliktów

Gdy aplikacja działająca w trybie offline przywróci połączenie, musi uzgodnić dane ze swojego lokalnego źródła danych z danymi w sieciowym źródle danych. Taki proces nazywa się synchronizacją. Aplikację można zsynchronizować ze swoim sieciowym źródłem danych na 2 główne sposoby:

  • Synchronizacja oparta na pobieraniu
  • Synchronizacja oparta na trybie push

Synchronizacja oparta na pobieraniu

W ramach synchronizacji pull aplikacja łączy się z siecią, aby na żądanie odczytać najnowsze dane aplikacji. Typowa heurystyka w przypadku tego podejścia jest związana z nawigacją, w której aplikacja pobiera dane tylko tuż przed ich pokazaniem użytkownikowi.

To podejście sprawdza się najlepiej, gdy aplikacja oczekuje na krótkie lub pośrednie okresy braku połączenia sieciowego. Dzieje się tak, ponieważ odświeżanie danych ma charakter oportunistyczny, a długie okresy braku połączenia zwiększają ryzyko, że użytkownik będzie próbował odwiedzać miejsca docelowe aplikacji z pamięcią podręczną, która jest nieaktualna lub pusta.

Synchronizacja oparta na pobieraniu
Rysunek 7. Synchronizacja oparta na pobieraniu

Weźmy aplikację, w której tokeny stron są używane do pobierania elementów z niekończącej się listy przewijanej dla określonego ekranu. Implementacja może też powoli komunikować się z siecią, zapisywać dane w lokalnym źródle danych, a następnie odczytywać je z lokalnego źródła, aby przekazać je użytkownikowi. W przypadku braku połączenia sieciowego repozytorium może zażądać danych z samego lokalnego źródła danych. To wzorzec używany przez bibliotekę stronicowania Jetpack z interfejsem 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
}

Zalety i wady synchronizacji metody pull znajdziesz w tabeli poniżej:

Zalety Wady
Wdrożenie jest stosunkowo łatwe. Podatne na intensywne korzystanie z danych. Dzieje się tak, ponieważ powtarzające się wizyty w miejscu docelowym nawigacji powodują niepotrzebne pobieranie niezmienionych informacji. Możesz temu zaradzić, stosując odpowiednie buforowanie. Możesz to robić w warstwie interfejsu za pomocą operatora cachedIn lub w warstwie sieciowej z pamięcią podręczną HTTP.
Dane, które nie są potrzebne, nie zostaną pobrane. Nie skaluje się dobrze w przypadku danych relacyjnych, ponieważ pobierany model musi być samowystarczający. Jeśli synchronizowany model zależy od pobierania innych modeli do wypełnienia, wspomniany wcześniej problem zużywania dużej ilości danych stanie się jeszcze poważniejszy. Ponadto może to powodować zależności między repozytoriami modelu nadrzędnego a repozytoriami modelu zagnieżdżonego.

Synchronizacja oparta na trybie push

Podczas synchronizacji w trybie push lokalne źródło danych stara się jak najdokładniej naśladować replikę zbioru danych sieciowych. Przy pierwszym uruchomieniu pobiera odpowiednią ilość danych, aby wyznaczyć punkt odniesienia, a potem wykorzystuje powiadomienia z serwera, aby ostrzegać o nieaktualnych danych.

Synchronizacja oparta na trybie push
Rysunek 8.

Po otrzymaniu nieaktualnego powiadomienia aplikacja łączy się z siecią, aby zaktualizować tylko te dane, które zostały oznaczone jako nieaktualne. Te zadania są przekazywane do Repository, który kontaktuje się z sieciowym źródłem danych i utrzymuje dane pobrane do lokalnego źródła danych. Repozytorium udostępnia swoje dane za pomocą typów obserwowalnych, więc czytelnicy będą otrzymywać powiadomienia o wszelkich zmianach.

class UserDataRepository(...) {

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

W tym podejściu aplikacja jest znacznie mniej zależna od sieciowego źródła danych i może działać bez niej przez dłuższy czas. Daje dostęp do odczytu i zapisu w trybie offline, ponieważ zakłada, że posiada najnowsze informacje ze źródła danych sieciowych lokalnie.

Zalety i wady synchronizacji w trybie push zostały opisane w tabeli poniżej:

Zalety Wady
Aplikacja może pozostawać offline bez ograniczeń czasowych. Obsługa wersji danych na potrzeby rozwiązania konfliktu nie jest prosta.
Minimalne użycie danych. Aplikacja pobiera tylko te dane, które zostały zmienione. Podczas synchronizacji musisz wziąć pod uwagę potencjalne problemy z zapisem.
Sprawdza się dobrze w przypadku danych relacyjnych. Każde repozytorium odpowiada tylko za pobieranie danych dla obsługiwanego modelu. Sieciowe źródło danych musi obsługiwać synchronizację.

Synchronizacja hybrydowa

Niektóre aplikacje wykorzystują hybrydowe metody pull lub push w zależności od danych. Na przykład aplikacja mediów społecznościowych może korzystać z synchronizacji opartej na pull, aby pobierać na żądanie następujący kanał użytkownika ze względu na dużą częstotliwość aktualizacji kanału. Ta sama aplikacja może używać synchronizacji push do przetwarzania danych o zalogowanym użytkowniku, w tym nazwy użytkownika, zdjęcia profilowego itp.

Wybór synchronizacji w trybie offline zależy od wymagań usługi i dostępnej infrastruktury technicznej.

Rozwiązanie konfliktu

Jeśli aplikacja zapisuje dane lokalnie w trybie offline, które są niewłaściwie dopasowane do źródła danych sieci, wystąpił konflikt, który musisz rozwiązać przed synchronizacją.

Rozwiązywanie konfliktów często wymaga obsługi wersji. Aplikacja musi prowadzić księgowość, by śledzić, kiedy wystąpiły zmiany. Dzięki temu może przekazywać metadane do sieciowego źródła danych. Sieć będzie odpowiedzialna za dostarczanie absolutnego źródła danych. W zależności od potrzeb aplikacji istnieje wiele strategii rozwiązywania konfliktów, które warto rozważyć. W przypadku aplikacji mobilnych popularnym podejściem jest „ostatni zapis wygrywa”.

Wygrane po ostatnim zapisie

W tym metodzie urządzenia dołączają metadane z sygnaturą czasową do danych zapisywanych w sieci. Gdy źródło danych sieci je otrzyma, odrzuca dane starsze niż obecny stan, a jednocześnie akceptuje te nowsze niż w bieżącym stanie.

Ostatni zapis wygrywa konflikt
Rys. 9: „Wygrywa ostatni zapis” Źródło wiarygodnych danych jest określane przez ostatnią encję, która je zapisała

W powyższej sytuacji oba urządzenia są offline i początkowo zsynchronizowane ze źródłem danych sieci. W trybie offline zapisują one dane lokalnie i zapisują czas, w którym zostały zapisane. Gdy oba te urządzenia ponownie są online i synchronizują się z sieciowym źródłem danych, konflikt rozwiązuje sieć, utrzymując dane z urządzenia B, ponieważ później je zapisało.

WorkManager w aplikacjach działających offline

W przypadku omówionych powyżej strategii odczytu i zapisu istniały 2 typowe narzędzia:

  • Kolejki
    • Odczyty: służy do opóźnienia odczytu do momentu uzyskania połączenia z siecią.
    • Zapisy: używane do opóźniania zapisu do momentu dostępności połączenia z siecią oraz do ponownego umieszczenia zapisu w kolejce w przypadku ponownych prób.
  • Monitory połączeń sieciowych
    • Odczyty: używany jako sygnał do opróżnienia kolejki odczytu po połączeniu aplikacji i w celu synchronizacji
    • Zapisy: używany jako sygnał do opróżnienia kolejki zapisu po połączeniu aplikacji i synchronizacji

Oba przypadki stanowią przykłady trwałej pracy, w której WorkManager świetnie sobie radzi. Na przykład w przykładowej aplikacji Now in Android (Now in Android) usługa WorkManager jest używana jako kolejka odczytu i monitor sieci podczas synchronizowania lokalnego źródła danych. Po uruchomieniu aplikacja wykonuje te działania:

  1. Zadbaj o to, aby synchronizacja odczytu została umieszczona w kolejce, aby zapewnić spójność między lokalnym źródłem danych a sieciowym źródłem danych.
  2. Opróżnij kolejkę synchronizacji do odczytu i rozpocznij synchronizację, gdy aplikacja będzie online.
  3. Wykonuje odczyt ze źródła danych sieci przy użyciu wykładniczego ponowienia.
  4. Utrwal wyniki odczytu w lokalnym źródle danych, rozwiązując wszelkie możliwe konflikty.
  5. Udostępniaj dane z lokalnego źródła danych do wykorzystania przez inne warstwy aplikacji.

Powyższy schemat przedstawia ten schemat:

Synchronizacja danych w aplikacji Now w Androidzie
Rysunek 10. Synchronizacja danych w aplikacji Now w Androidzie

Kolejkowanie zadań synchronizacji z WorkManagerem następuje przez określenie ich jako unikalnej pracy w 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
   }
}

Gdzie SyncWorker.startupSyncWork() jest zdefiniowany jako:


/**
 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()

W szczególności, obiekt Constraints zdefiniowany przez atrybut SyncConstraints wymaga, aby NetworkType miał wartość NetworkType.CONNECTED. Oznacza to, że przed uruchomieniem czeka na dostępność sieci.

Gdy sieć będzie dostępna, instancja robocza opróżni unikalną kolejkę pracy określoną przez SyncWorkName, przekazując ją do odpowiednich instancji Repository. Jeśli synchronizacja się nie powiedzie, metoda doWork() zwraca wartość Result.retry(). WorkManager automatycznie ponawia próbę synchronizacji ze wzrastającym czasem do ponowienia. W przeciwnym razie zwraca Result.success() podczas synchronizacji.

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()
    }
}

Próbki

Poniższe przykłady Google pokazują aplikacje działające głównie offline. Zapoznaj się z nimi, aby zastosować te wskazówki w praktyce: