Korzystanie z Kotlin Coroutines w aplikacji na Androida

Z tego modułu dowiesz się, jak korzystać z aplikacji Kotlin Coroutines w aplikacji na Androida. To nowy sposób na zarządzanie wątkami w tle, który może uprościć kod, ograniczając potrzebę wywołań zwrotnych. Kotwiny to funkcja Kotlin, która konwertuje asynchroniczne wywołania zwrotne dla długotrwałych zadań, takich jak dostęp do bazy danych lub sieci, na kod sekwencyjny.

Poniżej znajdziesz fragment kodu, który pozwoli Ci zorientować się, co będziesz robić.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

Kod wywołania zwrotnego zostanie przekonwertowany na kod sekwencyjny za pomocą odpowiednich algorytmów.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

Zaczniesz od istniejącej aplikacji utworzonej za pomocą komponentów architektury, która korzysta ze stylu wywołania zwrotnego do długotrwałych zadań.

Po zakończeniu tego ćwiczenia będziesz mieć możliwość korzystania z koordynów w celu wczytywania danych z sieci. Będziesz też mieć możliwość zintegrowania ich z aplikacją. Omówimy też sprawdzone metody dotyczące ich i nagrania testu z kodem, który służy do takich organizacji.

Wymagania wstępne

  • Znajomość komponentów architektury ViewModel, LiveData, Repository i Room.
  • Doświadczenie w używaniu składni Kotlina, w tym funkcji rozszerzenia i lambda.
  • Podstawowe informacje o używaniu wątków w Androidzie, w tym wątek główny, wątki w tle i wywołania zwrotne.

Co chcesz zrobić

  • Kod połączenia napisany z koordynacjami i uzyskiwanie wyników.
  • Użyj funkcji zawieszenia, aby utworzyć kod asynchroniczny sekwencyjny.
  • Użyj elementów launch i runBlocking, aby kontrolować działanie kodu.
  • Dowiedz się, jak przekształcać istniejące interfejsy API na odpowiednie schematy przy użyciu interfejsu suspendCoroutine.
  • Używaj kolumn z komponentami architektury.
  • Poznaj sprawdzone metody testowania reguł.

Czego potrzebujesz

  • Android Studio 3.5 (programowanie może działać z innymi wersjami, ale niektórych brakuje lub może ono wyglądać inaczej).

Jeśli podczas ćwiczeń z programowania napotkasz jakiekolwiek błędy (błędy w kodzie, błędy gramatyczne, niejasne słowa itp.), zgłoś je, klikając link Zgłoś błąd w lewym dolnym rogu ćwiczeń z programowania.

Pobierz kod

Aby pobrać cały kod do tych ćwiczeń z programowania, kliknij ten link:

Pobierz aplikację Zip

... lub skopiuj repozytorium GitHub z wiersza poleceń przy użyciu następującego polecenia:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Najczęstszepytania

Najpierw zobaczmy, jak wygląda początkowa aplikacja przykładowa. Aby otworzyć przykładową aplikację w Android Studio, wykonaj te czynności.

  1. Jeśli pobrałeś plik ZIP kotlin-coroutines, rozpakuj go.
  2. Otwórz projekt coroutines-codelab w Android Studio.
  3. Wybierz moduł aplikacji start.
  4. Kliknij przycisk wykonaj.pngUruchom i wybierz emulator lub podłącz urządzenie z Androidem, które musi obsługiwać Androida Lollipop (minimalny obsługiwany pakiet SDK to 21). Powinien wyświetlić się ekran Kotlin Coroutines:

Ta aplikacja startowa korzysta z wątków, by zwiększać opóźnienie po naciśnięciu ekranu. Pobra też nowy tytuł z sieci i wyświetli go na ekranie. Spróbuj to zrobić teraz. Po krótkim czasie liczba i wartość wiadomości powinny się zmienić. W tym ćwiczeniu z programowania przekształcisz tę aplikację w korelacje.

Ta aplikacja używa komponentów architektury i oddziela kod interfejsu w MainActivity od logiki aplikacji w MainViewModel. Poświęć chwilę, aby zapoznać się ze strukturą projektu.

  1. MainActivity wyświetla interfejs użytkownika, rejestruje detektory kliknięć i może wyświetlać właściwość Snackbar. Przekazuje zdarzenia do MainViewModel i aktualizuje ekran na podstawie LiveData w MainViewModel.
  2. MainViewModel obsługuje wydarzenia w aplikacji onMainViewClicked i będzie komunikować się z użytkownikiem MainActivity za pomocą LiveData.
  3. Executors określa właściwość BACKGROUND,, która może uruchamiać działania w wątku w tle.
  4. TitleRepository pobiera wyniki z sieci i zapisuje je w bazie danych.

Dodawanie algorytmów do projektu

Aby korzystać z korein w Kotlinie, musisz dołączyć bibliotekę coroutines-core w pliku build.gradle (Module: app) projektu. Te zadania z programowania zostały już wykonane przez Ciebie, więc nie musisz tego robić, aby ukończyć ćwiczenia.

Kohorty na Androida są dostępne jako główna biblioteka oraz rozszerzenia na Androida:

  • kotlinx-corountines-core – główny interfejs do korzystania z koordynów w Kotlinie;
  • kotlinx-coroutines-android – obsługa głównego wątku na Androidzie,

Aplikacja startowa zawiera już zależności w sekcji build.gradle.Podczas tworzenia nowego projektu aplikacji musisz otworzyć aplikację build.gradle (Module: app) i dodać do niej zależności.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Na urządzeniach z Androidem główny wątek jest niezbędny, aby można było go zablokować. Główny wątek to pojedynczy wątek, który obsługuje wszystkie aktualizacje interfejsu użytkownika. Jest to też wątek, który wywołuje wszystkie moduły obsługi kliknięć i inne wywołania zwrotne interfejsu. W związku z tym musi ono działać płynnie, aby zapewnić użytkownikom jak najlepsze wrażenia.

Aby Twoja aplikacja była widoczna dla użytkownika, ale nie ma żadnych wstrzymanych wyświetleń, główny wątek musi aktualizować ekran co 16 ms lub dłużej, czyli około 60 klatek na sekundę. Wiele typowych zadań trwa dłużej, takich jak analizowanie dużych zbiorów danych JSON, zapisywanie danych w bazie danych czy pobieranie danych z sieci. Dlatego taki kod wywołania z głównego wątku może spowodować, że aplikacja będzie się wstrzymywać, zacinać, a nawet zatrzymywać. Jeśli zablokujesz wątek główny przez zbyt długi czas, aplikacja może ulec awarii, a w jej oknie pojawi się Aplikacja nie odpowiada.

Obejrzyj film poniżej, by dowiedzieć się, w jaki sposób Twoi współpracownicy potrzebują pomocy w rozwiązaniu tego problemu na Androidzie.

Wzorzec wywołania zwrotnego

Jednym ze sposobów wykonywania długotrwałych zadań bez blokowania głównego wątku jest wywołania zwrotne. Używając wywołań zwrotnych, możesz uruchamiać długotrwałe zadania w wątku. Po zakończeniu zadania wywoływane jest wywołanie zwrotne, które informuje o wyniku w głównym wątku.

Spójrz na przykładowy wzorzec wywołania zwrotnego.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

Kod jest oznaczony adnotacją @UiThread, więc musi działać szybko, by wykonać go w wątku głównym. Oznacza to, że musi szybko powrócić, aby kolejna aktualizacja ekranu nie była opóźniona. Ponieważ działanie slowFetch zajmuje kilka sekund, a nawet minut, główny wątek nie może czekać na wynik. Wywołanie zwrotne show(result) umożliwia uruchomienie wywołania slowFetch w wątku w tle i zwrócenie wyniku, gdy wynik będzie gotowy.

Usuwanie wywołań zwrotnych za pomocą algorytmów

Wywołanie zwrotne to świetny wzorzec, ale ma kilka wad. Kod, który intensywnie korzysta z wywołań zwrotnych, może stać się trudny do odczytania i trudny do rozważenia. Ponadto wywołania zwrotne nie zezwalają na korzystanie z niektórych funkcji językowych, takich jak wyjątki.

Kotlin pozwala przekonwertować kod oparty na wywołaniach na kod sekwencyjny. Kod sekwencyjny jest zazwyczaj bardziej czytelny, a nawet można w nich korzystać z funkcji językowych takich jak wyjątki.

Na koniec wykonaj dokładnie to samo: poczekaj, aż wynik będzie dostępny od długo trwającego zadania, i kontynuuj wykonywanie. ale w kodzie są bardzo różne.

Słowo kluczowe suspend to sposób na określenie funkcji lub typu funkcji w Kotlin i przy jej użyciu. Gdy domena wywoła funkcję o nazwie suspend, zamiast blokować tę funkcję, tak jak w przypadku normalnego wywołania funkcji, zawiesza wykonywanie jej, dopóki wynik nie jest gotowy, a następnie wznowiono miejsce, w którym zakończyło się wynikiem. Podczas oczekiwania na wynik jest odblokowywany wątek, w którym jest on uruchomiony, przez co mogą działać inne funkcje lub algorytmy.

Na przykład w poniższym kodzie funkcje makeNetworkRequest() i slowFetch() są funkcjami suspend.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

Tak jak w przypadku wywołania zwrotnego, makeNetworkRequest musi jak najszybciej wrócić z wątku głównego, ponieważ jest oznaczony jako @UiThread. Oznacza to, że zazwyczaj nie może wywoływać metod blokowania takich jak slowFetch. Oto słowo kluczowe suspend, które ma w sobie magię.

W porównaniu z kodem zwrotnym kod coordine uzyskuje taki sam efekt, czyli odblokowanie bieżącego wątku z wykorzystaniem mniej kodu. Dzięki temu, że jest sekwencjonalny, można łatwo utworzyć kilka długotrwałych zadań bez tworzenia wielu wywołań zwrotnych. Na przykład kod, który pobiera wynik z 2 punktów końcowych sieci i zapisuje go w bazie danych, można zapisać jako funkcję w planach bez wywołań zwrotnych. W ten sposób:

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

W następnej sekcji przedstawisz przykładowe aplikacje.

W tym ćwiczeniu napiszesz wiadomość, w której wyświetli się wiadomość z opóźnieniem. Aby rozpocząć, sprawdź, czy moduł start jest otwarty w Android Studio.

Omówienie CoroutineScope

W Kotlin wszystkie algorytmy są uruchamiane w CoroutineScope. Zakres kontroluje czas trwania współdecydowanych przez zadanie. Jeśli anulujesz zadanie w zakresie, zostaną anulowane wszystkie jego kopie rozpoczęte. Na Androidzie możesz użyć zakresu, aby anulować wszystkie uruchomione algorytmy, gdy na przykład użytkownik opuści Activity lub Fragment. Zakresy umożliwiają też określenie domyślnego dyspozytora. Dyspozytor określa, który wątek ma być realizowany.

W przypadku algorytmów uruchamianych w interfejsie zwykle poprawne jest uruchomienie ich w Dispatchers.Main, który jest głównym wątkiem na Androidzie. Algorytm, który rozpoczął się Dispatchers.Main, nie zablokuje głównego wątku po zawieszeniu. Zmienna ViewModel niemal zawsze aktualizuje interfejs użytkownika w wątku głównym, więc rozpoczęcie sesji w głównym wątku pozwala zaoszczędzić dodatkowe przełączniki wątków. Współrzędny uruchomiony w wątku głównym może przełączać dyspozytorów w każdej chwili po jego rozpoczęciu. Na przykład inny dyspozytor może przeanalizować duży wynik JSON z głównego wątku.

Korzystanie z widoku viewModelScope

Biblioteka lifecycle-viewmodel-ktx na Androida dodaje element CoroutineScope do modeli ViewView, które są skonfigurowane tak, by umożliwić uruchamianie algorytmów interfejsu. Aby użyć tej biblioteki, musisz ją uwzględnić w pliku build.gradle (Module: start) projektu. Ten krok został już wykonany w programach ćwiczeń z programowania.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

Biblioteka dodaje viewModelScope jako funkcję rozszerzenia klasy ViewModel. Ten zakres jest powiązany z elementem Dispatchers.Main i zostanie automatycznie anulowany, gdy ViewModel zostanie wyczyszczony.

Przechodzenie z wątków na współrzędne

W sekcji MainViewModel.kt znajdź następne zadanie do wykonania wraz z kodem:

GłównyWidokModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

Ten kod używa BACKGROUND ExecutorService (zdefiniowanych w util/Executor.kt) do działania w wątku w tle. Ponieważ sleep blokuje bieżący wątek, spowoduje to zablokowanie interfejsu użytkownika, jeśli zostanie wywołany w głównym wątku. 1 sekundę po tym, jak użytkownik kliknie widok główny, zażąda wyświetlenia paska powiadomień.

Możesz to sprawdzić, usuwając BackGROUND z kodu i uruchamiając go ponownie. Wskaźnik wczytywania nie wyświetla się i wszystko jest w ciągu sekundy sekundowe.

GłównyWidokModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

Zastąp updateTaps tym kodem, który robi to samo. Musisz zaimportować launch i delay.

GłównyWidokModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

Ten kod działa tak samo, odczekując sekundę przed wyświetleniem paska powiadomień. Istnieją jednak pewne istotne różnice:

  1. viewModelScope.launch rozpocznie analizę w viewModelScope. Oznacza to, że gdy zadanie przesłane do viewModelScope zostanie anulowane, wszystkie kopie tego zadania/zakresu zostaną anulowane. Jeśli użytkownik opuścił aktywność przed zwróceniem gestu delay, ten algorytm zostanie automatycznie anulowany, gdy w związku z tym zniszczymy model ViewModel po wywołaniu elementu onCleared.
  2. Ponieważ viewModelScope ma domyślnego dystrybutora Dispatchers.Main, ten algorytm zostanie uruchomiony w głównym wątku. Później omówimy sposoby używania różnych wątków.
  3. Funkcja delay jest funkcją suspend. W Android Studio jest widoczna ikona na rynien. Mimo że ten algorytm działa w wątku głównym, delay nie zablokuje go na sekundę. Dyspozytor zaplanuje wznowienie połączenia w ciągu jednej sekundy.

Śmiało, uruchom ją. Po kliknięciu widoku głównego pasek sekundy powinien pojawić się sekundę później.

W następnej sekcji omówimy, jak przetestować tę funkcję.

W ramach tego ćwiczenia napiszesz test dotyczący właśnie wpisanego kodu. To ćwiczenie pokazuje, jak przetestować współprogramy w Dispatchers.Main przy użyciu biblioteki kotlinx-coroutines-test. W dalszej części tego ćwiczenia przeprowadzisz test, który wchodzi w bezpośrednią interakcję z korelacjami.

Przeglądanie dotychczasowego kodu

Otwórz folder MainViewModelTest.kt w folderze androidTest.

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

Reguła umożliwia uruchomienie kodu przed wykonaniem testu w jednostce JUnit i po nim. Aby umożliwić nam przetestowanie modelu MainViewModel w teście niedotyczącym urządzenia, wykorzystaliśmy 2 reguły:

  1. InstantTaskExecutorRule to reguła JUnit, która konfiguruje LiveData synchronicznie każde zadanie
  2. MainCoroutineScopeRule to niestandardowa reguła w tej bazie kodu, która służy do konfigurowania pola Dispatchers.Main o nazwie TestCoroutineDispatcher ze źródła kotlinx-coroutines-test. Dzięki temu testy będą mogły promować wirtualny zegar podczas testów, a kod będzie wykorzystywać Dispatchers.Main do testów jednostkowych.

W metodzie setup nowe wystąpienie elementu MainViewModel jest tworzone za pomocą testów fałszywych – są to fałszywe implementacje sieci i bazy danych podane w kodzie początkowym, które ułatwiają pisanie testów bez użycia rzeczywistej sieci lub bazy danych.

W tym teście fałszywe treści są potrzebne tylko do spełnienia zależności MainViewModel. W dalszej części tego modułu kodu zaktualizujesz fałszywe informacje, aby były pomocne dla współkoderów.

Napisz test sterujący współprogramami

Dodaj nowy test, który określi, że kliknięcia są aktualizowane sekundę po kliknięciu widoku głównego:

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

Zadzwonimy pod numer onMainViewClicked, aby uruchomić utworzoną kopię. Ten test sprawdza, czy po kliknięciu opcji "dotknięcia" "0 tap" natychmiast po wywołaniu onMainViewClicked 1 sekunda zostanie zaktualizowana do "1 kliknięcia".

W ramach tego testu używany jest czas wirtualny, aby kontrolować uruchamianie danej aplikacji przez onMainViewClicked. MainCoroutineScopeRule umożliwia wstrzymywanie, wznawianie i kontrolowanie uruchamiania algorytmów Dispatchers.Main. Wywołujemy teraz advanceTimeBy(1_000), co spowoduje, że główny dyspozytor natychmiast odbierze odpowiednie algorytmy, które mają zostać wznowione po sekundzie.

Ten test jest w pełni determinatywny, co oznacza, że zawsze działa tak samo. A ponieważ ma on pełną kontrolę nad uruchamianiem algorytmów Dispatchers.Main w przypadku uruchamiania, nie musi czekać jednej sekundy na ustawienie tej wartości.

Uruchamianie istniejącego testu

  1. Kliknij prawym przyciskiem myszy nazwę zajęć MainViewModelTest w edytorze, aby otworzyć menu kontekstowe.
  2. W menu kontekstowym wybierz wykonaj.pngUruchom 'MainViewModelTest'
  3. W przypadku przyszłych uruchomień możesz wybrać tę konfigurację testową w konfiguracjach obok przycisku wykonaj.png na pasku narzędzi. Domyślnie konfiguracja nazywa się MainViewModelTest.

Test powinien pojawić się. Jego uruchomienie powinno zająć mniej niż sekundę.

W następnym ćwiczeniu dowiesz się, jak przekonwertować dane z dotychczasowych interfejsów API wywołań zwrotnych do korzystania z koordynacji.

W tym kroku rozpoczniesz konwertowanie repozytorium na potrzeby współprogramów. Aby to zrobić, dodamy zapisy do pól ViewModel, Repository, Room i Retrofit.

Zanim zdecydujemy się na korzystanie z koordynatorów, warto dowiedzieć się, za co odpowiadają poszczególne części architektury.

  1. MainDatabase implementuje bazę danych, korzystając z pokoju, który zapisuje i wczytuje Title.
  2. MainNetwork wdraża interfejs API sieci, który pobiera nowy tytuł. Wykorzystuje funkcję Retrofit do pobierania tytułów. Retrofit jest tak ustawiony, aby losowo zwracać błędy lub symulować dane, ale zachowuje się tak, jakby wysyłał żądania sieci.
  3. TitleRepository wdraża jeden interfejs API do pobierania lub odświeżania tytułu, łącząc dane z sieci i bazy danych.
  4. MainViewModel reprezentuje stan ekranu i obsługuje zdarzenia. Dzięki temu repozytorium będzie odświeżać tytuł, gdy użytkownik dotknie ekranu.

Żądanie sieciowe jest wywoływane przez zdarzenia w interfejsie użytkownika i chcemy rozpocząć na ich podstawie rutynę, więc naturalnym miejscem na jej rozpoczęcie jest ViewModel.

Wersja wywołania zwrotnego

Otwórz MainViewModel.kt, by wyświetlić deklarację refreshTitle.

GłównyWidokModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

Ta funkcja jest wywoływana za każdym razem, gdy użytkownik kliknie ekran, co spowoduje, że repozytorium odświeży tytuł i zapisze nowy w bazie danych.

Ta implementacja używa wywołania zwrotnego, aby wykonać kilka czynności:

  • Przed uruchomieniem zapytania wyświetla się ikona wczytywania z atrybutem _spinner.value = true
  • Gdy uzyska wynik, usuwa wskaźnik ładowania z usługi _spinner.value = false
  • Jeśli pojawi się błąd, informuje pasek powiadomień o konieczności wyświetlenia i usunięcia wskaźnika postępu

Pamiętaj, że wywołanie zwrotne onCompleted nie jest uznawane za wywołanie title. Ponieważ wszystkie tytuły są zapisywane w bazie danych Room, interfejs aktualizuje się do aktualnej wersji, obserwując wartość LiveData (zaktualizowaną przez Room).

W ramach zmian w korelacjach zachowamy to samo zachowanie. Warto użyć obserwowalnego źródła danych, np. bazy danych Room, aby automatycznie aktualizować interfejs.

Wersja współprogramów

Przepiszmy refreshTitle z koordynacjami.

Potrzebujemy go od razu, więc utwórzmy w naszym repozytorium pustą funkcję zawieszenia (TitleRespository.kt). Zdefiniuj nową funkcję, która używa operatora suspend do informowania Kotlin, że współpracuje z koordynacjami.

Repository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

Gdy wykonasz te ćwiczenia z programowania, zaktualizujesz je, by korzystały z funkcji Retrofit i Room, aby pobrać nowy tytuł i zapisać go w bazie danych za pomocą odpowiednich algorytmów. Na razie wyda 500 milisekund na udaną pracę i będzie można kontynuować.

W MainViewModel zamień wersję wywołania refreshTitle na wersję, która uruchamia nową współdzielenie:

GłównyWidokModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Przejdźmy przez tę funkcję:

viewModelScope.launch {

Tak jak w przypadku współrzędnej, aby zaktualizować liczbę kliknięć, zacznij od uruchomienia nowej reguły w viewModelScope. Zostanie użyte Dispatchers.Main, co jest dobre. Mimo że refreshTitle wysyła żądanie sieciowe i zapytanie z bazy danych, może używać algorytmów do udostępniania interfejsu bezpiecznego dla użytkownika. Oznacza to, że będzie można to łatwo wywołać z wątku głównego.

Używamy viewModelScope, więc gdy użytkownik opuści ten ekran, zadanie rozpoczęte przez niego zostanie automatycznie anulowane. Oznacza to, że nie będzie wysyłać dodatkowych żądań sieciowych ani zapytań do bazy danych.

Kilka następnych wierszy kodu wywołuje funkcję refreshTitle w repository.

try {
    _spinner.value = true
    repository.refreshTitle()
}

Zanim cokolwiek zrobi, uruchamia wskaźnik wczytywania, a potem wywołuje funkcję refreshTitle tak jak zwykłą funkcję. Ponieważ jednak refreshTitle jest funkcją zawieszającą, jest wykonywana inaczej niż normalnie.

Nie musimy oddzwonić. Współrzędna zostanie zawieszona do chwili jej wznowienia przez usługę refreshTitle. Choć wygląda jak zwykłe wywołanie funkcji blokowania, przed wznowieniem zapytania bez blokowania głównego wątku będzie automatycznie czekać, aż zapytanie sieci i bazy zostanie ukończone.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Wyjątki w zakresie funkcji zawieszenia działają tak samo jak błędy w zwykłych funkcjach. Jeśli w funkcji funkcji zawieszenia zobaczysz błąd, zostanie on wywołany. Choć są one wykonywane w nieco inny sposób, do zarządzania nimi możesz używać zwykłych bloków try-catch. Jest to przydatne, ponieważ pozwala korzystać z wbudowanej obsługi języka w obsłudze błędów, zamiast tworzyć niestandardową obsługę błędów dla każdego wywołania zwrotnego.

W przypadku wyjątku – zostanie ona domyślnie anulowana przez rodzica. Oznacza to, że razem z kilkoma zadaniami można łatwo anulować.

W końcowym bloku możemy sprawdzić, czy wskaźnik ładowania jest zawsze wyłączony po uruchomieniu zapytania.

Uruchom aplikację ponownie, wybierając konfigurację start, a następnie naciskając wykonaj.png. Po kliknięciu w dowolnym miejscu zobaczysz wskaźnik wczytywania. Taki tytuł pozostanie taki sam, ponieważ nie mamy jeszcze połączenia z naszą siecią lub bazą danych.

W następnym ćwiczeniu zaktualizujesz repozytorium tak, aby działało.

Z tego ćwiczenia dowiesz się, jak zmienić wątek, w którym działa algorytm, by wdrożyć aktywną wersję elementu TitleRepository.

Sprawdź istniejący kod wywołania zwrotnego w refreshTitle

Otwórz TitleRepository.kt i sprawdź obecną implementację wywołania zwrotnego.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

W metodzie TitleRepository.kt metoda refreshTitleWithCallbacks jest zaimplementowana za pomocą wywołania zwrotnego, aby powiadomić rozmówcę o stanie wczytywania i błędzie.

Ta funkcja wykonuje dość dużo czynności, by wdrożyć odświeżanie.

  1. Przełącz na inny wątek z użytkownikiem BACKGROUND ExecutorService
  2. Wyślij żądanie sieciowe fetchNextTitle za pomocą metody blokowania execute(). Spowoduje to uruchomienie żądania sieciowego w bieżącym wątku, w tym przypadku w jednym z wątków w BACKGROUND.
  3. Jeśli wynik się powiedzie, zapisz plik w bazie danych za pomocą wywołania insertTitle i wywołaj metodę onCompleted().
  4. Jeśli wynik się nie powiódł lub jest wyjątek, wywołaj metodę onError, by poinformować rozmówcę o niepowodzeniu odświeżania.

Ta implementacja oparta na wywołaniach zwrotnych jest główna – bezpieczna, ponieważ nie blokuje głównego wątku. Należy jednak użyć wywołania zwrotnego, aby poinformować rozmówcę o zakończeniu zadania. Oprócz tego wywołuje wywołania zwrotne w wątku, w którym funkcja BACKGROUND się przełączyła.

Połączenia telefoniczne blokujące połączenia z koordynacjami

Bez wprowadzenia odpowiednich sieci lub bazy danych możemy ustawić ten kod jako główny, wykorzystując odpowiednie reguły. Pozwoli to nam wyeliminować wywołanie zwrotne i przekazać wynik z powrotem do wątku, w którym go początkowo wywołaliśmy.

Możesz używać tego wzorca zawsze wtedy, gdy chcesz zablokować lub bardzo intensywnie pracować nad procesem, takie jak sortowanie i filtrowanie dużej listy czy odczytywanie danych z dysku.

Do przełączania się między dyspozytorami używa się withContext. Wywołanie withContext przekierowuje do innego dyspozytora tylko w lambdzie, a następnie wraca do dyspozytora, który podał ten wynik w wyniku lambdy.

Domyślnie Kotlin współpracuje z 3 dyspozytorami: Main, IO i Default. Dyspozytor jest zoptymalizowany pod kątem operacji wejścia-wyjścia, np. odczytu z sieci lub dysku, a domyślny dyspozytor jest zoptymalizowany pod kątem zadań wymagających dużej mocy procesora.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

Ta implementacja wykorzystuje wywołania blokowania w sieci i bazie danych, ale nadal jest to nieco prostsze niż w przypadku wersji wywołania zwrotnego.

Ten kod nadal używa blokowania wywołań. Wywołanie execute() i insertTitle(...) zablokuje wątek, w którym pracuje ten algorytm. Jednak przełączając się na Dispatchers.IO za pomocą withContext, blokujemy jeden z wątków w dystrybucji zamówienia reklamowego. Współrzędna, która prawdopodobnie była uruchomiona w Dispatchers.Main, zostanie zawieszona do zakończenia lambdy withContext.

W porównaniu z wersją wywołania zwrotnego istnieją dwie ważne różnice:

  1. withContext zwraca wynik z dyspozytorem, który go wywołał. Tutaj jest to Dispatchers.Main. Wersja wywołania zwrotnego nazywa się wywołaniem zwrotnym w wątku w usłudze wykonawcy BACKGROUND.
  2. Rozmówca nie musi przekazywać wywołania do tej funkcji. Mogą polegać na zawieszeniu i wznowieniu, aby otrzymać wynik lub błąd.

Uruchom ponownie aplikację

Jeśli uruchomisz aplikację ponownie, zobaczysz, że nowa implementacja oparta na danych wczytuje dane z sieci.

W następnym kroku zintegrujesz współdzielenie z Room i Retrofit.

Aby kontynuować integrację kolumn, użyjemy funkcji zawieszania w stabilnych wersjach Pokój i Retrofit, a następnie uprościmy kod, który właśnie napisano, korzystając z funkcji zawieszenia.

Kohorty w pokoju

Najpierw otwórz MainDatabase.kt i ustaw insertTitle jako funkcję zawieszenia:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Gdy to zrobisz, pokój stanie się głównym zabezpieczeniem i zostanie automatycznie wykonane w wątku w tle. Oznacza to jednak, że zapytanie możesz wywołać tylko z poziomu współrzędu.

To wszystko, co musisz zrobić, aby używać kolumn w pokoju. Stylowo.

Koordynacje w projekcie retro

Teraz zobaczmy, jak zintegrować współprogramy z Retrofit. Otwórz MainNetwork.kt i zmień fetchNextTitle na funkcję zawieszenia.

MainNetwork.kt,

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Aby używać funkcji zawieszenia z Retrofit, musisz wykonać 2 rzeczy:

  1. Dodawanie modyfikatora zawieszenia do funkcji
  2. Usuń opakowanie Call z typu zwrotu. Tutaj zwracamy String, ale możesz też zwrócić złożony typ w formacie JSON. Jeśli nadal chcesz zezwolić na dostęp do elementu Result w stylu retrofit i cały czas, możesz zwrócić Result<String> zamiast String za pomocą funkcji zawieszenia.

Program Retrofit automatycznie ustawi funkcje zawieszenia głównie bezpieczne, dzięki czemu możesz je wywoływać bezpośrednio z poziomu usługi Dispatchers.Main.

Korzystanie z sali i stylu retro

Teraz, gdy Room i Retrofit obsługują funkcje zawieszenia, możemy ich użyć w naszym repozytorium. Otwórz aplikację TitleRepository.kt i sprawdź, jak użycie funkcji zawieszania interfejsu znacznie upraszcza logikę, nawet w przypadku wersji blokowanej:

TytułRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

To krócej o wiele krócej. Co się stało? Okazuje się, że korzystanie z zawieszenia i wznowienia umożliwia znacznie krótsze kodowanie. Dzięki niemu możesz korzystać z typów zwrotów, np. String lub User, zamiast Call. To nic nie szkodzi, bo Retrofit może wykonać żądanie sieciowe w wątku w tle i wznowić rutynę po zakończeniu wywołania.

Jeszcze lepiej pozbyliśmy się withContext. Ponieważ funkcje Room i Retrofit zapewniają główne funkcje zawieszania, można bezpiecznie zarządzać asynchroniczną pracą zespołu Dispatchers.Main.

Naprawianie błędów kompilacji

Przejście na współrzędne wiąże się ze zmianą podpisu funkcji, ponieważ nie można wywoływać funkcji zawieszenia z poziomu funkcji zwykłej. Po dodaniu modyfikatora suspend w tym kroku wygenerowano kilka błędów kompilatora pokazującej, co się stanie, jeśli zmienisz funkcję, która ma być zawieszona w rzeczywistym projekcie.

Przejdź przez ten projekt i napraw błędy kompilatora, zmieniając funkcję zawieszenia. Oto szybkie rozwiązania dla każdego z nich:

TestingFakes.kt

Zaktualizuj fałszywe informacje testowe, by obsługiwały nowe modyfikatory zawieszenia.

TytułDaoFake

  1. Naciśnij alt-Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w obrębie heiranchi

Fake MainFake

  1. Naciśnij alt-Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w obrębie heiranchi
  2. Zastąp fetchNextTitle tą funkcją
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Naciśnij alt-Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w obrębie heiranchi
  2. Zastąp fetchNextTitle tą funkcją
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Usuń funkcję refreshTitleWithCallbacks, ponieważ nie jest już używana.

Uruchamianie aplikacji

Uruchom ponownie aplikację. Po skompilowaniu zobaczysz, że wczytuje ona dane przy użyciu algorytmów, od modeli ViewView do Room i Retrofit.

Gratulacje! Udało Ci się całkowicie zamienić tę aplikację na współprogramy! Na koniec zobaczmy, jak przetestować to, co właśnie zrobiliśmy.

W tym ćwiczeniu napiszesz test, który bezpośrednio wywołuje funkcję suspend.

Ponieważ interfejs refreshTitle jest udostępniany jako publiczny interfejs API, jest testowany bezpośrednio, co wskazuje sposób wywoływania funkcji współprogramów z testów.

Oto funkcja funkcji refreshTitle zaimplementowana w ostatnim ćwiczeniu:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Tworzenie testu wywołującego funkcję zawieszenia

Otwórz folder TitleRepositoryTest.kt w folderze test, który zawiera 2 TODOS.

Spróbuj wywołać refreshTitle z pierwszego testu whenRefreshTitleSuccess_insertsRows.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

Ponieważ refreshTitle jest funkcją suspend, Kotlin nie wie, jak ją wywołać, chyba że jest współprogramowaną lub inną funkcją zawieszenia, a wystąpi błąd kompilatora, np. "Zawieś funkcję odświeżaniaTitleTitle powinno być wywoływane tylko z odpowiedniej lub innej funkcji zawieszenia."

Ten tester nie wie nic o algorytmach, więc nie możemy przeprowadzić tego testu w celu zawieszenia. launch może mieć odpowiednią postać przy użyciu CoroutineScope, tak jak w ViewModel, ale przed testem trzeba przeprowadzić go do końca. Po zwróceniu funkcji testowej test się kończy. Kodowanie zaczynające się od launch to kod asynchroniczny, który może zostać w przyszłości wykonany. Aby więc przetestować ten kod asynchroniczny, musisz jakoś przekazać testowi czas oczekiwania na zakończenie połączenia. Działanie launch nie jest blokowane, co oznacza, że jest zwracane od razu i może po upływie czasu nadal wykonywać kod – nie można go użyć w testach. Przykład:

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

Ten test czasem się nie powiedzie. Wywołanie funkcji launch natychmiast powróci i zostanie wykonane w tym samym czasie co reszta przypadku testowego. Test nie jest w stanie sprawdzić, czy test refreshTitle został już uruchomiony, a przy tym nie ma dowodów na to, że baza danych została zaktualizowana. A jeśli refreshTitle zwrócił wyjątek, nie zostanie on uwzględniony w testowym stosie wywołań. Zostanie on przekierowany do modułu obsługi wyjątku typu GlobalScope&#39.

Biblioteka kotlinx-coroutines-test zawiera funkcję runBlockingTest, która blokuje dane podczas wywoływania funkcji zawieszenia. Gdy runBlockingTest wywołuje funkcję zawieszenia lub launches nowy współprogram, uruchamia ją domyślnie. Można to potraktować jako sposób przekształcenia funkcji zawieszenia i koordynacji w normalne wywołania funkcji.

Dodatkowo runBlockingTest będzie zwracać odrzucone wyjątki. Dzięki temu łatwiej jest sprawdzić, kiedy algorytm generuje wyjątek.

Przeprowadzanie testu na 1 kohorcie

Umieść wywołanie refreshTitle w tagu runBlockingTest i usuń kod GlobalScope.launch z subject.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

W tym teście uwzględniane są fałszywe informacje wprowadzone w celu sprawdzenia, czy fragment „OK” jest wstawiony do bazy danych przez usługę refreshTitle.

Gdy test wywoła polecenie runBlockingTest, zostanie zablokowane do czasu zakończenia odpowiedniej procedury uruchomionej przez runBlockingTest. Następnie wywołujemy metodę refreshTitle za pomocą zwykłego mechanizmu zawieszenia i wznawiania, aby poczekać, aż wiersz bazy danych zostanie dodany do naszego fałszywego systemu.

Po zakończeniu testu runBlockingTest zwracany jest zwrot.

Pisanie testu czasu oczekiwania

Chcemy dodać krótki czas oczekiwania do żądania sieci. Najpierw zapisz test, a potem przekrocz czas oczekiwania. Utwórz nowy test:

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

W tym teście wykorzystano fałszywą wartość MainNetworkCompletableFake, która jest fałszywą nazwą sieci i ma na celu zawieszenie rozmówców do czasu kontynuowania testu. Gdy refreshTitle spróbuje wysłać żądanie sieciowe, zostanie ono zawieszone na zawsze, ponieważ chcemy przetestować limity czasu.

Następnie uruchamia osobny algorytm o nazwie refreshTitle. Jest to kluczowy moment testowania czasu oczekiwania, który powinien przypadać w innym przypadku niż runBlockingTest. W ten sposób możemy wywołać kolejny wiersz (advanceTimeBy(5_000)), który przesuwa się o 5 sekund i powoduje przekroczenie limitu czasu.

To jest pełny test limitu czasu, który zakończy się po wdrożeniu limitu czasu.

Uruchom go i sprawdź, co się stanie:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

Jedną z funkcji runBlockingTest jest to, że nie pozwala ona ujawnić kolumn po zakończeniu testu. Jeśli na koniec testu pojawią się niedokończone testy, takie jak te, których nie udało się wprowadzić, zakończy się to niepowodzeniem.

Dodawanie limitu czasu

Otwórz plik TitleRepository i dodaj pięciosekundowy czas oczekiwania do pobrania sieci. Możesz to zrobić za pomocą funkcji withTimeout:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Uruchom test. Wszystkie testy powinny zakończyć się powodzeniem.

W następnym ćwiczeniu dowiesz się, jak pisać funkcje związane z kolejnością za pomocą algorytmów współrzędnych.

W tym ćwiczeniu otrzymasz refaktoryzację refreshTitle na MainViewModel, by użyć funkcji ogólnego wczytywania danych. Dowiesz się z niego, jak tworzyć funkcje o wyższej kolejności, które korzystają z programów.

Obecna implementacja refreshTitle działa, ale możemy utworzyć ogólną regułę wczytywania danych, która będzie zawsze pokazywać wskaźnik postępu. Może to być pomocne w bazie kodu, która wczytuje dane w odpowiedzi na kilka zdarzeń i chce, aby wskaźnik wczytywania ładował się systematycznie.

Przeglądanie aktualnej implementacji w każdym wierszu z wyjątkiem repository.refreshTitle() zawiera powtarzające się błędy i błędy wyświetlania.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Korzystanie z kohorty w funkcjach wyższego poziomu

Dodaj ten kod do MainViewModel.kt

GłównyWidokModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Teraz zastosuj refaktory refreshTitle(), by użyć tej funkcji wyższego rzędu.

GłównyWidokModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

Wyodrębniając logikę związaną z wyświetlaniem wskaźnika wczytywania i pokazywanie błędów, uprościliśmy rzeczywisty kod niezbędny do wczytywania danych. Wskaźnik wczytywania lub wyświetlanie błędu to coś, co można łatwo uogólnić w przypadku każdego wczytywania danych. Rzeczywiste źródło danych i miejsce docelowe muszą być za każdym razem określone.

Aby utworzyć tę abstrakcję, launchDataLoad przyjmuje argument block będący zawieszoną lambdą. Zawieszanie lambdy pozwala wywołać funkcje zawieszenia. Właśnie w ten sposób Kotlin wdraża kreatory współpracy launch i runBlocking używane w tym ćwiczeniu z programowania.

// suspend lambda

block: suspend () -> Unit

Aby utworzyć lambdę zawieszoną, zacznij od słowa kluczowego suspend. Deklarację wypełniają strzałki funkcji i typy Unit.

Często nie trzeba deklarować własnych lambd wwieszeniowych, ale mogą one pomóc w tworzeniu abstrakcji takich, które wyrażają powtarzalną logikę.

W tym ćwiczeniu dowiesz się, jak używać kodu opartego na algorytmach WorkManager.

Co to jest WorkManager

Na urządzeniu z Androidem jest wiele opcji odroczonego działania w tle. To ćwiczenie pokazuje, jak zintegrować WorkManager z algorytmem współpracy. WorkManager to kompatybilna, elastyczna i prosta biblioteka do odroczonych zadań w tle. W takich przypadkach zalecamy korzystanie z WorkManagera.

WorkManager jest częścią Jet Jetpacka i komponentu Architektura przeznaczonego do pracy w tle, który wymaga kombinacji oportunistycznej i gwarantowanej wykonania. Wykonując tę operacji, WorkManager wykona swoje zadanie tak szybko, jak to możliwe. Gwarantowane wykonanie oznacza, że WorkManager zajmie się logiką uruchomienia Twojej pracy w różnych sytuacjach, nawet jeśli opuścisz aplikację.

Z tego względu WorkManager jest dobrym rozwiązaniem w przypadku zadań, które muszą zostać zakończone.

Oto przykłady zadań, w których dobrze działa WorkManager:

  • Przesyłam logi
  • stosowanie filtrów do obrazów i zapisywanie ich;
  • Okresowa synchronizacja danych lokalnych z siecią

Korzystanie z kolumn aplikacji razem z WorkWork

WorkManager udostępnia różne implementacje podstawowej klasy ListanableWorker przeznaczone do różnych przypadków użycia.

Najprostsza klasa instancji roboczych pozwala nam wykonać pewne działania synchroniczne przez WorkManager. Mimo to do tej pory udało nam się przekonwertować naszą bazę kodu na korzystanie z programów i funkcji zawieszania. Najlepiej jest użyć klasy CoroutineWorker, która pozwala zdefiniować funkcję doWork() jako funkcję zawieszenia.

Aby rozpocząć, otwórz aplikację RefreshMainDataWork. Jest już przedłużona do CoroutineWorker. Musisz wdrożyć doWork.

W ramach funkcji suspend doWork wywołaj refreshTitle() z repozytorium i zwróć odpowiedni wynik!

Gdy wykonasz TODO, kod będzie wyglądać tak:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

CoroutineWorker.doWork() należy do funkcji zawieszających. W przeciwieństwie do prostszej klasy Worker ten kod nie działa w usłudze wykonawcy określonej w konfiguracji WorkManager, ale używa dyspozytora w grupie coroutineContext (domyślnie Dispatchers.Default).

Testowanie narzędzia CoroutineWorker

Żadna baza kodu nie powinna być kompletna bez testowania.

WorkManager udostępnia kilka sposobów testowania klas Worker. Aby dowiedzieć się więcej o pierwotnej infrastrukturze testowej, zapoznaj się z dokumentacją.

W wersji 2.1 WorkManager wprowadziliśmy nowy zestaw interfejsów API, który obsługuje prostszy sposób testowania klas ListenableWorker, dzięki czemu działa w ramach CoroutineWorker. W naszym kodzie wykorzystamy jeden z tych nowych interfejsów API: TestListenableWorkerBuilder.

Aby dodać nowy test, zaktualizuj plik RefreshMainDataWorkTest w folderze androidTest.

Zawartość pliku:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Zanim przejdziemy do testu, informujemy firmę WorkManager o fabryce, by umożliwić nam wstrzykiwanie fałszywej sieci.

Sam test używa TestListenableWorkerBuilder do utworzenia naszego instancji roboczej, którą możemy uruchomić, wywołując metodę startWork().

WorkManager to tylko jeden z przykładów użycia algorytmów do upraszczania projektowania interfejsów API.

W tym ćwiczeniu omówiliśmy podstawowe zagadnienia, takie jak wykorzystanie algorytmów w Twojej aplikacji.

Omówione zagadnienia:

  • Jak zintegrować aplikacje z Androidem z zadaniami UI i WorkManager, aby uprościć programowanie asynchroniczne?
  • Jak używać algorytmów w ramach ViewModel do pobierania danych z sieci i zapisywania ich w bazie danych bez blokowania głównego wątku.
  • Jak anulować wszystkie algorytmy po zakończeniu operacji ViewModel.

W przypadku testowania kodu opartego na algorytmie współuczestniczył zarówno w teście testowania, jak i bezpośrednim wywoływaniu funkcji suspend z testów.

Więcej informacji

Aby dowiedzieć się więcej o zaawansowanym korzystaniu z koordynów na urządzeniach z Androidem, zapoznaj się z artykułem „Advanced Coroutines with Kotlin Flow and LiveData"”.

Korynki te mają wiele funkcji, które nie zostały omówione w tym ćwiczeniu z programowania. Jeśli chcesz dowiedzieć się więcej o koordynacjach z Kotlin, przeczytaj przewodniki po Kotwicach publikowane przez JetBrains. Zobacz też "Zwiększ wydajność aplikacji dzięki Koordinom" aby uzyskać więcej wzorców wykorzystania współprogramów na Androidzie.