Używanie w aplikacji na Androida Kotlin Coroutines

Z tego laboratorium dowiesz się, jak używać korutyn w Kotlinie w aplikacji na Androida. Jest to nowy sposób zarządzania wątkami w tle, który może uprościć kod, ponieważ zmniejsza potrzebę stosowania wywołań zwrotnych. Korutyny to funkcja Kotlin, która przekształca asynchroniczne wywołania zwrotne w przypadku długotrwałych zadań, takich jak dostęp do bazy danych lub sieci, w sekwencyjny kod.

Oto fragment kodu, który pomoże Ci zrozumieć, co będziesz robić.

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

Kod oparty na wywołaniach zwrotnych zostanie przekonwertowany na kod sekwencyjny za pomocą korutyn.

// 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 do długotrwałych zadań używa stylu wywołania zwrotnego.

Po ukończeniu tego laboratorium zdobędziesz wystarczającą wiedzę, aby używać w aplikacji współprogramów do wczytywania danych z sieci i integrować je z aplikacją. Poznasz też sprawdzone metody dotyczące współprogramów oraz sposób pisania testów kodu, który ich używa.

Wymagania wstępne

  • Znajomość komponentów architektury ViewModel, LiveData, RepositoryRoom.
  • Znajomość składni Kotlina, w tym funkcji rozszerzeń i wyrażeń lambda.
  • Podstawowa wiedza na temat korzystania z wątków na Androidzie, w tym wątku głównym, wątków w tle i wywołań zwrotnych.

Co musisz zrobić

  • Wywołaj kod napisany z użyciem korutyn i uzyskaj wyniki.
  • Używaj funkcji zawieszających, aby kod asynchroniczny był wykonywany sekwencyjnie.
  • Za pomocą ustawień launchrunBlocking możesz kontrolować sposób wykonywania kodu.
  • Poznaj techniki przekształcania istniejących interfejsów API w korutyny za pomocą suspendCoroutine.
  • Używaj korutyn z komponentami architektury.
  • Poznaj sprawdzone metody testowania korutyn.

Czego potrzebujesz

  • Android Studio 3.5 (ćwiczenia mogą działać w innych wersjach, ale niektóre elementy mogą być niedostępne lub wyglądać inaczej).

Jeśli podczas wykonywania tego laboratorium napotkasz jakiekolwiek problemy (błędy w kodzie, błędy gramatyczne, niejasne sformułowania itp.), zgłoś je, klikając link Zgłoś błąd w lewym dolnym rogu laboratorium.

Pobieranie kodu

Aby pobrać cały kod do tego laboratorium, kliknij ten link:

Pobierz plik ZIP

… lub sklonuj repozytorium GitHub z wiersza poleceń, używając tego polecenia:

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

Najczęstsze pytania

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

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

Ta aplikacja startowa używa wątków do zwiększania liczby z niewielkim opóźnieniem po naciśnięciu ekranu. Pobierze też z sieci nowy tytuł i wyświetli go na ekranie. Spróbuj teraz. Po krótkim czasie powinna się zmienić liczba i wiadomość. W tym ćwiczeniu przekształcisz tę aplikację, aby korzystała z korutyn.

Ta aplikacja używa komponentów architektury do oddzielenia kodu interfejsu w MainActivity od logiki aplikacji w MainViewModel. Poświęć chwilę na zapoznanie się ze strukturą projektu.

  1. MainActivity wyświetla interfejs, rejestruje detektory kliknięć i może wyświetlać element Snackbar. Przekazuje zdarzenia do MainViewModel i aktualizuje ekran na podstawie LiveDataMainViewModel.
  2. MainViewModel obsługuje zdarzenia w onMainViewClicked i komunikuje się z MainActivity za pomocą LiveData..
  3. Executors definiuje BACKGROUND,, który może uruchamiać elementy w wątku w tle.
  4. TitleRepository pobiera wyniki z sieci i zapisuje je w bazie danych.

Dodawanie do projektu współprogramów

Aby używać w Kotlinie współprogramów, musisz dołączyć bibliotekę coroutines-core do pliku build.gradle (Module: app) w projekcie. W projektach ćwiczeń z programowania zostało to już zrobione, więc nie musisz tego robić, aby ukończyć ćwiczenie.

Korutyny na Androidzie są dostępne jako biblioteka podstawowa i rozszerzenia specyficzne dla Androida:

  • kotlinx-coroutines-core  – główny interfejs do korzystania z korutyn w Kotlinie.
  • kotlinx-coroutines-android  – obsługa głównego wątku Androida w korutynach.

Aplikacja startowa zawiera już zależności w build.gradle.Podczas tworzenia nowego projektu aplikacji musisz otworzyć build.gradle (Module: app) i dodać do projektu zależności dotyczące współprogramów.

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

Na Androidzie należy unikać blokowania głównego wątku. Wątek główny to pojedynczy wątek, który obsługuje wszystkie aktualizacje interfejsu. Jest to też wątek, który wywołuje wszystkie procedury obsługi kliknięć i inne wywołania zwrotne interfejsu. Dlatego musi działać bez zarzutu, aby zapewnić użytkownikom jak najlepsze wrażenia.

Aby aplikacja wyświetlała się użytkownikowi bez widocznych przerw, wątek główny musi aktualizować ekran co 16 ms lub częściej, czyli około 60 klatek na sekundę. Wiele typowych zadań trwa dłużej, np. parsowanie dużych zbiorów danych JSON, zapisywanie danych w bazie danych czy pobieranie danych z sieci. Dlatego wywoływanie takiego kodu z wątku głównego może spowodować wstrzymanie, zacinanie się lub nawet zawieszenie aplikacji. Jeśli zablokujesz wątek główny na zbyt długo, aplikacja może się nawet zawiesić i wyświetlić okno Aplikacja nie odpowiada.

Z filmu poniżej dowiesz się, jak współprogramy rozwiązują ten problem w Androidzie, wprowadzając bezpieczeństwo wątku głównego.

Wzorzec wywołania zwrotnego

Jednym ze sposobów wykonywania długotrwałych zadań bez blokowania wątku głównego są wywołania zwrotne. Za pomocą wywołań zwrotnych możesz uruchamiać długotrwałe zadania w wątku w tle. Po zakończeniu zadania wywoływane jest wywołanie zwrotne, aby poinformować Cię o wyniku w głównym wątku.

Zapoznaj się z przykładem wzorca 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
}

Ponieważ ten kod jest oznaczony adnotacją @UiThread, musi działać wystarczająco szybko, aby można go było wykonać w wątku głównym. Oznacza to, że musi ona bardzo szybko zwrócić wynik, aby nie opóźnić kolejnej aktualizacji ekranu. Jednak ponieważ wykonanie operacji slowFetch zajmuje sekundy, a nawet minuty, wątek główny nie może czekać na wynik. Wywołanie zwrotne show(result) umożliwia uruchomienie funkcji slowFetch w wątku w tle i zwrócenie wyniku, gdy będzie gotowy.

Używanie korutyn do usuwania wywołań zwrotnych

Wywołania zwrotne to świetny wzorzec, ale ma on kilka wad. Kod, który w dużym stopniu wykorzystuje wywołania zwrotne, może stać się trudny do odczytania i zrozumienia. Poza tym wywołania zwrotne nie umożliwiają korzystania z niektórych funkcji języka, takich jak wyjątki.

Korutyny Kotlin umożliwiają przekształcenie kodu opartego na wywołaniach zwrotnych w kod sekwencyjny. Kod napisany sekwencyjnie jest zwykle łatwiejszy do odczytania i może nawet korzystać z funkcji języka, takich jak wyjątki.

Ostatecznie robią dokładnie to samo: czekają, aż wynik długotrwałego zadania będzie dostępny, i kontynuują wykonywanie. W kodzie wyglądają jednak zupełnie inaczej.

Słowo kluczowe suspend w języku Kotlin służy do oznaczania funkcji lub typu funkcji dostępnych dla korutyn. Gdy korutyna wywołuje funkcję oznaczoną jako suspend, zamiast blokować się do momentu, aż ta funkcja zwróci wartość (jak w przypadku zwykłego wywołania funkcji), zawiesza wykonanie do momentu, aż wynik będzie gotowy, a następnie wznawia działanie w miejscu, w którym zostało przerwane, z wynikiem. Gdy jest zawieszona w oczekiwaniu na wynik, odblokowuje wątek, w którym działa , aby mogły działać inne funkcje lub korutyny.

Na przykład w poniższym kodzie makeNetworkRequest()slowFetch() to funkcje 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 { ... }

Podobnie jak w przypadku wersji wywołania zwrotnego funkcja makeNetworkRequest musi natychmiast zwrócić wartość z wątku głównego, ponieważ jest oznaczona jako @UiThread. Oznacza to, że zwykle nie może wywoływać metod blokowania, takich jak slowFetch. W tym miejscu słowo kluczowe suspend zaczyna działać.

W porównaniu z kodem opartym na wywołaniach zwrotnych kod z korutynami osiąga ten sam efekt odblokowania bieżącego wątku przy użyciu mniejszej ilości kodu. Dzięki sekwencyjnemu stylowi łatwo jest połączyć 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 napisać jako funkcję w korutynach 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 wprowadzisz do przykładowej aplikacji współprogramy.

W tym ćwiczeniu napiszesz korutynę, która po upływie określonego czasu wyświetli komunikat. Na początek upewnij się, że moduł start jest otwarty w Android Studio.

Czym jest CoroutineScope

W Kotlinie wszystkie korutyny działają w ramach CoroutineScope. Zakres kontroluje czas życia korutyn za pomocą zadania. Gdy anulujesz zadanie w zakresie, anulujesz wszystkie korutyny uruchomione w tym zakresie. Na Androidzie możesz użyć zakresu, aby anulować wszystkie działające coroutines, np. gdy użytkownik opuści Activity lub Fragment. Zakresy umożliwiają też określenie domyślnego dyspozytora. Dyspozytor określa, który wątek uruchamia korutynę.

W przypadku korutyn uruchamianych przez interfejs użytkownika zwykle prawidłowe jest uruchamianie ich w Dispatchers.Main, czyli w głównym wątku na Androidzie. Korutyna uruchomiona w Dispatchers.Main nie będzie blokować głównego wątku podczas zawieszenia. ViewModel Korutyna prawie zawsze aktualizuje interfejs na wątku głównym, więc uruchamianie korutyn na wątku głównym pozwala uniknąć dodatkowych przełączeń wątków. Korutyna uruchomiona w wątku głównym może w dowolnym momencie po uruchomieniu przełączyć dyspozytora. Może na przykład użyć innego dyspozytora do przeanalizowania dużego wyniku JSON poza głównym wątkiem.

Korzystanie z viewModelScope

Biblioteka AndroidX lifecycle-viewmodel-ktx dodaje do ViewModeli CoroutineScope skonfigurowany tak, aby uruchamiać korutyny związane z interfejsem. Aby używać tej biblioteki, musisz ją uwzględnić w pliku build.gradle (Module: start) projektu. Ten krok został już wykonany w projektach z samouczka.

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

Biblioteka dodaje viewModelScope jako funkcję rozszerzającą klasy ViewModel. Ten zakres jest powiązany z Dispatchers.Main i zostanie automatycznie anulowany po wyczyszczeniu ViewModel.

Przechodzenie z wątków na korutyny

W MainViewModel.kt znajdź kolejny komentarz TODO wraz z tym kodem:

MainViewModel.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 funkcji BACKGROUND ExecutorService (zdefiniowanej w util/Executor.kt) do uruchamiania w wątku w tle. Funkcja sleep blokuje bieżący wątek, więc jeśli zostanie wywołana w wątku głównym, interfejs użytkownika zostanie zamrożony. Sekundę po kliknięciu głównego widoku użytkownik wysyła żądanie wyświetlenia paska z informacją.

Możesz to sprawdzić, usuwając z kodu słowo BACKGROUND i ponownie go uruchamiając. Spinner ładowania nie będzie się wyświetlać, a wszystko „przeskoczy” do stanu końcowego sekundę później.

MainViewModel.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 opartym na korutynach, który robi to samo. Musisz zaimportować launchdelay.

MainViewModel.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 robi to samo, czekając sekundę przed wyświetleniem paska z informacją. Istnieją jednak pewne ważne różnice:

  1. viewModelScope.launch rozpocznie korutynę w viewModelScope. Oznacza to, że gdy zadanie przekazane do viewModelScope zostanie anulowane, wszystkie korutyny w tym zadaniu lub zakresie zostaną anulowane. Jeśli użytkownik opuścił aktywność przed zwróceniem wartości delay, ta korutyna zostanie automatycznie anulowana, gdy podczas niszczenia obiektu ViewModel zostanie wywołana funkcja onCleared.
  2. Ponieważ viewModelScope ma domyślny dyspozytor Dispatchers.Main, ta korutyna zostanie uruchomiona w wątku głównym. Później zobaczymy, jak używać różnych wątków.
  3. Funkcja delay jest funkcją suspend. W Android Studio jest to oznaczone ikoną  w lewym marginesie. Mimo że ta korutyna działa w wątku głównym, delay nie zablokuje go na sekundę. Zamiast tego dyspozytor zaplanuje wznowienie korutyny za sekundę przy następnej instrukcji.

Uruchom go. Gdy klikniesz widok główny, po sekundzie powinien pojawić się pasek informacyjny.

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

W tym ćwiczeniu napiszesz test do napisanego przed chwilą kodu. Z tego ćwiczenia dowiesz się, jak testować coroutines działające na Dispatchers.Main za pomocą biblioteki kotlinx-coroutines-test. W dalszej części tego ćwiczenia z programowania zaimplementujesz test, który bezpośrednio wchodzi w interakcję z korutynami.

Sprawdź dotychczasowy kod

Otwórz plik 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 to sposób na uruchamianie kodu przed i po wykonaniu testu w JUnit. Aby umożliwić testowanie klasy MainViewModel poza urządzeniem, używamy 2 reguł:

  1. InstantTaskExecutorRule to reguła JUnit, która konfiguruje LiveData do synchronicznego wykonywania każdego zadania.
  2. MainCoroutineScopeRule to niestandardowa reguła w tej bazie kodu, która konfiguruje Dispatchers.Main tak, aby używać TestCoroutineDispatcherkotlinx-coroutines-test. Umożliwia to testom przesuwanie wirtualnego zegara na potrzeby testowania, a kodowi – używanie Dispatchers.Main w testach jednostkowych.

W metodzie setup tworzona jest nowa instancja MainViewModel przy użyciu testowych obiektów zastępczych. Są to fałszywe implementacje sieci i bazy danych udostępnione w kodzie początkowym, które pomagają pisać testy bez używania prawdziwej sieci lub bazy danych.

W tym teście obiekty zastępcze są potrzebne tylko do spełnienia zależności MainViewModel. W dalszej części tego modułu zaktualizujesz obiekty zastępcze, aby obsługiwały współprogramy.

Pisanie testu, który kontroluje coroutines

Dodaj nowy test, który sprawdzi, czy 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")
}

Wywołanie onMainViewClicked spowoduje uruchomienie utworzonej przez nas korutyny. Ten test sprawdza, czy po wywołaniu funkcji onMainViewClicked tekst dotknięć pozostaje „0 dotknięć”, a po sekundzie zmienia się na „1 dotknięcie”.

Ten test wykorzystuje czas wirtualny do kontrolowania wykonywania korutyny uruchomionej przez onMainViewClicked. Symbol MainCoroutineScopeRule umożliwia wstrzymywanie, wznawianie i kontrolowanie wykonywania korutyn uruchamianych w Dispatchers.Main. W tym przypadku wywołujemy advanceTimeBy(1_000), co spowoduje, że główny dyspozytor natychmiast wykona korutyny, które mają zostać wznowione po 1 sekundzie.

Ten test jest w pełni deterministyczny, co oznacza, że zawsze będzie wykonywany w ten sam sposób. Ponieważ ma pełną kontrolę nad wykonywaniem korutyn uruchamianych na Dispatchers.Main, nie musi czekać sekundy na ustawienie wartości.

Uruchamianie istniejącego testu

  1. Kliknij prawym przyciskiem myszy nazwę klasy MainViewModelTest w edytorze, aby otworzyć menu kontekstowe.
  2. W menu kontekstowym wybierz execute.pngUruchom „MainViewModelTest”.
  3. W przyszłości możesz wybrać tę konfigurację testu w konfiguracjach obok przycisku execute.png na pasku narzędzi. Domyślnie konfiguracja będzie się nazywać MainViewModelTest.

Test powinien zakończyć się powodzeniem. Powinno to zająć znacznie mniej niż sekundę.

W następnym ćwiczeniu dowiesz się, jak przekształcić istniejące interfejsy API wywołania zwrotnego, aby korzystać z korutyn.

W tym kroku zaczniesz przekształcać repozytorium, aby używać w nim współprogramów. W tym celu dodamy do funkcji ViewModel, Repository, RoomRetrofit współprogramy.

Zanim przełączymy je na korzystanie z korutyn, warto zrozumieć, za co odpowiada każda część architektury.

  1. MainDatabase implementuje bazę danych za pomocą biblioteki Room, która zapisuje i wczytuje Title.
  2. MainNetwork implementuje interfejs API sieci, który pobiera nowy tytuł. Do pobierania tytułów używa biblioteki Retrofit. Retrofit jest skonfigurowany tak, aby losowo zwracać błędy lub dane symulacyjne, ale poza tym działa tak, jakby wysyłał prawdziwe żądania sieciowe.
  3. TitleRepository implementuje pojedynczy interfejs API do pobierania lub odświeżania tytułu przez łączenie danych z sieci i bazy danych.
  4. MainViewModel reprezentuje stan ekranu i obsługuje zdarzenia. Poinformuje repozytorium, że ma odświeżyć tytuł, gdy użytkownik kliknie ekran.

Żądanie sieciowe jest wywoływane przez zdarzenia interfejsu, a my chcemy uruchamiać na ich podstawie korutynę, więc naturalnym miejscem do rozpoczęcia korzystania z korutyn jest ViewModel.

Wersja z oddzwonieniem

Otwórz MainViewModel.kt, aby zobaczyć deklarację refreshTitle.

MainViewModel.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. Powoduje ona odświeżenie tytułu w repozytorium i zapisanie nowego tytułu w bazie danych.

Ta implementacja używa wywołania zwrotnego do wykonania kilku czynności:

  • Przed rozpoczęciem zapytania wyświetla ikonę wczytywania z symbolem _spinner.value = true.
  • Gdy otrzyma wynik, usuwa spinner wczytywania za pomocą _spinner.value = false.
  • Jeśli wystąpi błąd, wyświetli pasek powiadomień i usunie spinner.

Pamiętaj, że do funkcji zwrotnej onCompleted nie jest przekazywany argument title. Wszystkie tytuły zapisujemy w bazie danych Room, więc interfejs aktualizuje się do bieżącego tytułu, obserwując LiveData, który jest aktualizowany przez Room.

W aktualizacji dotyczącej współprogramów zachowamy dokładnie to samo działanie. Dobrym rozwiązaniem jest używanie źródła danych, które można obserwować, np. Room bazy danych, aby automatycznie aktualizować interfejs.

Wersja z korutynami

Przepiszmy refreshTitle za pomocą korutyn.

Ponieważ będziemy jej od razu potrzebować, utwórzmy w naszym repozytorium pustą funkcję zawieszającą (TitleRespository.kt). Zdefiniuj nową funkcję, która używa operatora suspend, aby poinformować Kotlin, że współpracuje z korutynami.

TitleRepository.kt

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

Po ukończeniu tego laboratorium zaktualizujesz kod, aby używać Retrofit i Room do pobierania nowego tytułu i zapisywania go w bazie danych za pomocą współprogramów. Na razie będzie przez 500 milisekund udawać, że coś robi, a potem będzie kontynuować.

MainViewModel zastąp wersję wywołania zwrotnego funkcji refreshTitle wersją, która uruchamia nową korutynę:

MainViewModel.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
       }
   }
}

Przyjrzyjmy się tej funkcji:

viewModelScope.launch {

Podobnie jak w przypadku korutyny do aktualizowania liczby kliknięć, zacznij od uruchomienia nowej korutyny w viewModelScope. Zostanie użyta wartość Dispatchers.Main, co jest w porządku. Mimo że funkcja refreshTitle wysyła żądanie sieciowe i zapytanie do bazy danych, może używać korutyn do udostępniania interfejsu bezpiecznego dla wątku głównego. Oznacza to, że można ją bezpiecznie wywoływać z głównego wątku.

Ponieważ używamy viewModelScope, gdy użytkownik opuści ten ekran, praca rozpoczęta przez tę korutynę zostanie automatycznie anulowana. Oznacza to, że nie będzie wysyłać dodatkowych żądań sieciowych ani zapytań do bazy danych.

Kolejne wiersze kodu wywołują funkcję refreshTitlerepository.

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

Zanim ta korutyna wykona jakiekolwiek działanie, uruchamia spinner ładowania, a potem wywołuje funkcję refreshTitle tak samo jak zwykłą funkcję. Ponieważ jednak refreshTitle jest funkcją zawieszającą, wykonuje się inaczej niż zwykła funkcja.

Nie musimy przekazywać wywołania zwrotnego. Korutyna zostanie zawieszona, dopóki nie zostanie wznowiona przez refreshTitle. Chociaż wygląda jak zwykłe wywołanie funkcji blokującej, automatycznie czeka, aż zapytanie do sieci i bazy danych zostanie zakończone, a następnie wznawia działanie bez blokowania głównego wątku.

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

Wyjątki w funkcjach zawieszających działają tak samo jak błędy w zwykłych funkcjach. Jeśli w funkcji zawieszającej wystąpi błąd, zostanie on przekazany do wywołującego. Mimo że działają one zupełnie inaczej, możesz używać zwykłych bloków try/catch do ich obsługi. Jest to przydatne, ponieważ umożliwia korzystanie z wbudowanej obsługi błędów w języku zamiast tworzenia niestandardowej obsługi błędów dla każdego wywołania zwrotnego.

Jeśli z korutyny zostanie zgłoszony wyjątek, domyślnie spowoduje to anulowanie jej elementu nadrzędnego. Oznacza to, że łatwo jest anulować kilka powiązanych zadań jednocześnie.

W bloku finally możemy zadbać o to, aby po uruchomieniu zapytania spinner był zawsze wyłączony.

Uruchom ponownie aplikację, wybierając konfigurację start, a następnie naciśnijexecute.png. Po kliknięciu dowolnego miejsca powinien pojawić się wskaźnik ładowania. Tytuł pozostanie bez zmian, ponieważ nie mamy jeszcze połączenia z naszą siecią ani bazą danych.

W następnym ćwiczeniu zaktualizujesz repozytorium, aby faktycznie wykonywało pracę.

Z tego ćwiczenia dowiesz się, jak przełączyć wątek, w którym działa korutyna, aby wdrożyć działającą wersję TitleRepository.

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

Otwórz TitleRepository.kt i sprawdź dotychczasową implementację opartą na wywołaniach zwrotnych.

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

TitleRepository.kt metoda refreshTitleWithCallbacks jest implementowana z wywołaniem zwrotnym, aby przekazywać stan wczytywania i błędu do wywołującego.

Aby odświeżyć dane, ta funkcja wykonuje kilka czynności.

  1. Przełączanie się na inny wątek za pomocą klawiszy BACKGROUND ExecutorService
  2. Uruchom żą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 jest pozytywny, zapisz go w bazie danych za pomocą polecenia insertTitle i wywołaj metodę onCompleted().
  4. Jeśli wynik nie jest pozytywny lub wystąpił wyjątek, wywołaj metodę onError, aby poinformować wywołującego o nieudanym odświeżeniu.

Ta implementacja oparta na wywołaniach zwrotnych jest bezpieczna dla wątku głównego, ponieważ nie blokuje go. Musi jednak użyć wywołania zwrotnego, aby poinformować dzwoniącego o zakończeniu pracy. Wywołuje też wywołania zwrotne w wątku BACKGROUND, na który się przełączył.

Blokowanie wywołań z korutyn

Bez wprowadzania do sieci lub bazy danych współprogramów możemy sprawić, że ten kod będzie bezpieczny dla wątku głównego, używając współprogramów. Dzięki temu pozbędziemy się wywołania zwrotnego i będziemy mogli przekazać wynik z powrotem do wątku, który je pierwotnie wywołał.

Możesz użyć tego wzorca, gdy musisz wykonać blokujące lub wymagające dużego obciążenia procesora zadanie w ramach korutyny, np. posortować i odfiltrować dużą listę lub odczytać dane z dysku.

Aby przełączać się między dowolnymi dyspozytorami, korutyny używają withContext. Wywołanie withContext przełącza się na innego dyspozytora tylko w przypadku funkcji lambda, a potem wraca do dyspozytora, który ją wywołał, z wynikiem tej funkcji lambda.

Domyślnie w przypadku Kotlin Coroutines dostępne są 3 dispatchery: Main, IODefault. Dispatcher IO jest zoptymalizowany pod kątem operacji wejścia/wyjścia, takich jak odczytywanie danych z sieci lub dysku, a dispatcher Default jest zoptymalizowany pod kątem zadań wymagających dużej mocy obliczeniowej 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 używa wywołań blokujących dla sieci i bazy danych, ale jest nieco prostsza niż wersja z wywołaniem zwrotnym.

Ten kod nadal blokuje połączenia. Wywołanie funkcji execute()insertTitle(...) spowoduje zablokowanie wątku, w którym działa ta korutyna. Przełączając się jednak na Dispatchers.IO za pomocą withContext, blokujemy jeden z wątków w dispatcherze wejścia/wyjścia. Korutyna, która wywołała tę funkcję, prawdopodobnie działająca na Dispatchers.Main, zostanie zawieszona do czasu zakończenia działania lambdy withContext.

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

  1. withContext zwraca wynik do wywołującego go dyspozytora, w tym przypadku Dispatchers.Main. Wersja wywołania zwrotnego wywoływała wywołania zwrotne w wątku w usłudze wykonawczej BACKGROUND.
  2. Wywołujący nie musi przekazywać do tej funkcji wywołania zwrotnego. Aby uzyskać wynik lub błąd, mogą korzystać z funkcji wstrzymywania i wznawiania.

Ponowne uruchomienie aplikacji

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

W następnym kroku zintegrujesz korutyny z bibliotekami Room i Retrofit.

Aby kontynuować integrację z korutynami, użyjemy obsługi funkcji zawieszania w stabilnej wersji Room i Retrofit, a następnie znacznie uprościmy napisany przed chwilą kod, korzystając z funkcji zawieszania.

Korutyny w Room

Najpierw otwórz MainDatabase.kt i ustaw insertTitle jako funkcję zawieszającą:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

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

W takim przypadku Room automatycznie przekształci zapytanie w bezpieczne dla wątku głównego i wykona je w wątku w tle. Oznacza to jednak również, że to zapytanie można wywołać tylko w obrębie korutyny.

To wszystko, co musisz zrobić, aby używać w Roomie współprogramów. Całkiem sprytne.

Korutyny w Retrofit

Teraz zobaczmy, jak zintegrować korutyny z Retrofit. Otwórz MainNetwork.kt i zmień fetchNextTitle na funkcję wstrzymania.

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 zawieszania z Retrofit, musisz wykonać 2 czynności:

  1. Dodaj do funkcji modyfikator suspend
  2. Usuń otoczkę Call z typu zwracanego. W tym przypadku zwracamy wartość String, ale możesz też zwrócić złożony typ oparty na formacie JSON. Jeśli nadal chcesz zapewnić dostęp do pełnego Result, możesz zwrócić Result<String> zamiast String z funkcji zawieszania.

Retrofit automatycznie sprawi, że funkcje zawieszania będą bezpieczne dla wątku głównego, dzięki czemu będzie można je wywoływać bezpośrednio z Dispatchers.Main.

Korzystanie z bibliotek Room i Retrofit

Room i Retrofit obsługują teraz funkcje zawieszania, więc możemy ich używać w naszym repozytorium. Otwórz TitleRepository.kt i zobacz, jak używanie funkcji zawieszających znacznie upraszcza logikę, nawet w porównaniu z wersją blokującą:

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

Wow, to jest dużo krótsze. Co się stało? Okazuje się, że korzystanie z zawieszania i wznawiania pozwala znacznie skrócić kod. Retrofit umożliwia używanie tutaj typów zwracanych, takich jak String lub obiekt User, zamiast Call. Jest to bezpieczne, ponieważ w funkcji zawieszającej Retrofit może uruchomić żądanie sieciowe w wątku w tle i wznowić działanie korutyny po zakończeniu wywołania.

Co więcej, pozbyliśmy się withContext. Zarówno Room, jak i Retrofit udostępniają funkcje zawieszania bezpieczne dla wątku głównego, więc można bezpiecznie zarządzać tą pracą asynchroniczną z poziomu Dispatchers.Main.

Naprawianie błędów kompilatora

Przejście na korutyny wiąże się ze zmianą sygnatury funkcji, ponieważ nie można wywołać funkcji zawieszającej z funkcji zwykłej. Gdy w tym kroku dodasz modyfikator suspend, pojawi się kilka błędów kompilatora, które pokazują, co by się stało, gdybyś w prawdziwym projekcie zmienił funkcję na zawieszającą.

Przejrzyj projekt i napraw błędy kompilatora, zmieniając funkcję na utworzoną funkcję zawieszającą. Oto szybkie rozwiązania w każdym z tych przypadków:

TestingFakes.kt

Zaktualizuj testowe obiekty zastępcze, aby obsługiwały nowe modyfikatory zawieszenia.

TitleDaoFake

  1. Naciśnij Alt+Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w hierarchii.

MainNetworkFake

  1. Naciśnij Alt+Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w hierarchii.
  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 hierarchii.
  2. Zastąp fetchNextTitle tą funkcją
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

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

Uruchom aplikację

Uruchom ponownie aplikację. Po skompilowaniu zobaczysz, że dane są wczytywane za pomocą korutyn na całej ścieżce od ViewModel do Room i Retrofit.

Gratulacje! Aplikacja korzysta już w pełni z korutyn. Na koniec powiemy, jak sprawdzić, czy wszystko działa prawidłowo.

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

Ponieważ refreshTitle jest udostępniana jako publiczny interfejs API, będzie testowana bezpośrednio, co pokaże, jak wywoływać funkcje współprogramów z testów.

Oto funkcja refreshTitle, którą zaimplementowano 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)
   }
}

Napisz test, który wywołuje funkcję zawieszania

Otwórz plik TitleRepositoryTest.kt w folderze test, który zawiera 2 elementy TODO.

Spróbuj zadzwonić pod numer refreshTitle z pierwszego urządzenia testowego 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ć, z wyjątkiem korutyny lub innej funkcji zawieszającej. Pojawi się błąd kompilatora, np. „Funkcję zawieszającą refreshTitle należy wywoływać tylko z korutyny lub innej funkcji zawieszającej”.

Program uruchamiający testy nie wie nic o korutynach, więc nie możemy przekształcić tego testu w funkcję zawieszającą. Możemy launch korutynę za pomocą CoroutineScope, tak jak w ViewModel, ale testy muszą uruchamiać korutyny do końca, zanim zwrócą wynik. Po zwróceniu wartości przez funkcję testu test się kończy. Korutyny uruchamiane za pomocą launch to kod asynchroniczny, który może zostać wykonany w przyszłości. Aby przetestować kod asynchroniczny, musisz mieć możliwość poinformowania testu, aby poczekał na zakończenie działania korutyny. Funkcja launch jest wywołaniem nieblokującym, co oznacza, że zwraca wartość od razu i może kontynuować działanie korutyny po zwróceniu wartości przez funkcję. Nie można jej używać w testach. Na 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 czasami się nie powiedzie. Wywołanie funkcji launch zostanie natychmiast zwrócone i wykonane w tym samym czasie co reszta przypadku testowego. Test nie ma możliwości sprawdzenia, czy funkcja refreshTitle została już uruchomiona, a wszelkie stwierdzenia, takie jak sprawdzenie, czy baza danych została zaktualizowana, byłyby niestabilne. Jeśli funkcja refreshTitle zgłosi wyjątek, nie zostanie on zgłoszony w stosie wywołań testu. Zamiast tego zostanie przekazany do procedury obsługi niewykrytych wyjątków w GlobalScope.

Biblioteka kotlinx-coroutines-test ma funkcję runBlockingTest, która blokuje wywołania funkcji zawieszania. Gdy funkcja runBlockingTest wywołuje funkcję zawieszania lub launches nową współprogram, domyślnie wykonuje ją natychmiast. Możesz traktować to jako sposób na przekształcenie funkcji zawieszających i korutyn w zwykłe wywołania funkcji.

Dodatkowo runBlockingTest ponownie zgłosi nieobsłużone wyjątki. Ułatwia to testowanie, kiedy korutyna zgłasza wyjątek.

Wdrażanie testu z 1 korutyną

Owiń wywołanie refreshTitle za pomocą runBlockingTest i usuń otoczkę 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")
}

Ten test wykorzystuje podane elementy zastępcze, aby sprawdzić, czy refreshTitle wstawia do bazy danych wartość „OK”.

Gdy test wywoła funkcję runBlockingTest, zostanie zablokowany do czasu zakończenia korutyny uruchomionej przez funkcję runBlockingTest. Następnie, gdy w funkcji wywołujemy refreshTitle, używa ona zwykłego mechanizmu zawieszania i wznawiania, aby poczekać, aż wiersz bazy danych zostanie dodany do naszej fałszywej bazy.

Po zakończeniu działania funkcji testowej runBlockingTest zwraca wartość.

Tworzenie testu limitu czasu

Chcemy dodać do żądania sieciowego krótki limit czasu. Najpierw napiszmy test, a potem zaimplementujmy limit czasu. Tworzenie nowego testu:

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

Ten test korzysta z podanego fałszywego numeru MainNetworkCompletableFake, który jest fałszywym numerem sieciowym zaprojektowanym tak, aby zawieszać połączenia do momentu, gdy test zostanie wznowiony. Gdy refreshTitle spróbuje wysłać żądanie sieciowe, będzie ono zawieszone na zawsze, ponieważ chcemy przetestować limity czasu.

Następnie uruchamia osobną korutynę, aby wywołać funkcję refreshTitle. Jest to kluczowy element testowania limitów czasu. Limit czasu powinien wystąpić w innej korutynie niż ta, którą tworzy runBlockingTest. W ten sposób możemy wywołać następny wiersz, advanceTimeBy(5_000), co spowoduje przesunięcie czasu o 5 sekund i przekroczenie limitu czasu przez drugą korutynę.

Jest to test pełnego limitu czasu, który zostanie zaliczony po wdrożeniu limitu czasu.

Uruchom go teraz i zobacz, co się stanie:

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

Jedną z funkcji runBlockingTest jest to, że po zakończeniu testu nie pozwala na wyciek współprogramów. Jeśli na końcu testu będą jakieś niedokończone korutyny, np. nasza korutyna launch, test zakończy się niepowodzeniem.

Dodawanie limitu czasu

Otwórz TitleRepository i dodaj 5-sekundowy limit czasu do pobierania danych z 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)
   }
}

Przeprowadź test. Po uruchomieniu testów wszystkie powinny zakończyć się powodzeniem.

W następnym ćwiczeniu dowiesz się, jak pisać funkcje wyższego rzędu za pomocą korutyn.

W tym ćwiczeniu przeprowadzisz refaktoryzację funkcji refreshTitle w pliku MainViewModel, aby używać ogólnej funkcji wczytywania danych. Dowiesz się z niego, jak tworzyć funkcje wyższego rzędu, które korzystają z korutyn.

Obecna implementacja refreshTitle działa, ale możemy utworzyć ogólną procedurę współbieżną wczytywania danych, która zawsze wyświetla spinner. Może to być przydatne w przypadku bazy kodu, która wczytuje dane w odpowiedzi na kilka zdarzeń i chce mieć pewność, że spinner ładowania jest wyświetlany w sposób ciągły.

W obecnej implementacji każda linia z wyjątkiem repository.refreshTitle() jest kodem standardowym, który służy do wyświetlania spinnera i błędów.

// 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
       }
   }
}

Używanie korutyn w funkcjach wyższego rzędu

Dodaj ten kod do pliku MainViewModel.kt

MainViewModel.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 zmień kod refreshTitle(), aby używać tej funkcji wyższego rzędu.

MainViewModel.kt

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

Dzięki wyodrębnieniu logiki wyświetlania wskaźnika ładowania i błędów uprościliśmy kod potrzebny do wczytywania danych. Wyświetlanie spinnera lub błędu można łatwo uogólnić na dowolne wczytywanie danych, natomiast rzeczywiste źródło i miejsce docelowe danych trzeba określać za każdym razem.

Aby utworzyć tę abstrakcję, funkcja launchDataLoad przyjmuje argument block, który jest funkcją lambda zawieszającą. Funkcja lambda zawieszenia umożliwia wywoływanie funkcji zawieszenia. W ten sposób Kotlin implementuje konstruktory korutyn launchrunBlocking, których używaliśmy w tym ćwiczeniu.

// suspend lambda

block: suspend () -> Unit

Aby utworzyć funkcję lambda zawieszającą, zacznij od słowa kluczowego suspend. Strzałka funkcji i typ zwracany Unit uzupełniają deklarację.

Nie musisz często deklarować własnych funkcji zawieszających, ale mogą one być przydatne do tworzenia abstrakcji, takich jak ta, która obejmuje powtarzającą się logikę.

Z tego ćwiczenia dowiesz się, jak używać kodu opartego na korutynach w usłudze WorkManager.

Czym jest WorkManager

Na Androidzie jest wiele opcji odraczalnej pracy w tle. W tym ćwiczeniu dowiesz się, jak zintegrować WorkManager z korutynami. WorkManager to zgodna, elastyczna i prosta biblioteka do odraczania pracy w tle. W przypadku tych zastosowań na Androidzie zalecamy korzystanie z WorkManagera.

WorkManager jest częścią Androida Jetpackkomponentem architektury do pracy w tle, która wymaga połączenia oportunistycznego i gwarantowanego wykonania. Oznacza to, że WorkManager wykona pracę w tle, gdy tylko będzie to możliwe. Gwarantowane wykonanie oznacza, że WorkManager zajmie się logiką uruchamiania zadania w różnych sytuacjach, nawet jeśli opuścisz aplikację.

Dlatego WorkManager to dobry wybór w przypadku zadań, które muszą zostać ostatecznie wykonane.

Przykłady zadań, do których warto użyć WorkManagera:

  • Przesyłanie logów
  • Stosowanie filtrów do obrazów i zapisywanie obrazu
  • okresowe synchronizowanie danych lokalnych z siecią,

Używanie korutyn z WorkManagerem

WorkManager udostępnia różne implementacje swojej klasy bazowej ListanableWorker dla różnych przypadków użycia.

Najprostsza klasa Worker umożliwia wykonywanie przez WorkManager pewnych operacji synchronicznych. Jednak po przekształceniu bazy kodu tak, aby korzystała z korutyn i funkcji zawieszających, najlepszym sposobem używania WorkManagera jest klasa CoroutineWorker, która umożliwia zdefiniowanie naszej doWork()funkcji jako funkcji zawieszającej.

Aby rozpocząć, otwórz RefreshMainDataWork. Rozszerza już CoroutineWorker, a Ty musisz zaimplementować doWork.

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

Po wykonaniu zadania 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()
   }
}

Pamiętaj, że CoroutineWorker.doWork() to funkcja zawieszająca. W przeciwieństwie do prostszej klasy Worker ten kod NIE jest wykonywany na obiekcie Executor określonym w konfiguracji WorkManagera, ale zamiast tego używa dyspozytora w elemencie coroutineContext (domyślnie Dispatchers.Default).

Testowanie klasy CoroutineWorker

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

WorkManager udostępnia kilka różnych sposobów testowania klas Worker. Więcej informacji o pierwotnej infrastrukturze testowej znajdziesz w dokumentacji.

WorkManager w wersji 2.1 wprowadza nowy zestaw interfejsów API, które ułatwiają testowanie klas ListenableWorker, a w konsekwencji także CoroutineWorker. W naszym kodzie użyjemy jednego 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 WorkManager o fabryce, abyśmy mogli wstrzyknąć fałszywą sieć.

Sam test używa funkcji TestListenableWorkerBuilder do utworzenia procesu roboczego, który możemy następnie uruchomić, wywołując metodę startWork().

WorkManager to tylko jeden z przykładów, jak za pomocą współprogramów można uprościć projektowanie interfejsów API.

W tym ćwiczeniu omówiliśmy podstawy, które pozwolą Ci zacząć używać w aplikacji współprogramów.

Omówiliśmy:

  • Jak zintegrować korutyny z aplikacjami na Androida zarówno z interfejsu, jak i z zadań WorkManager, aby uprościć programowanie asynchroniczne.
  • Jak używać korutyn w ViewModel do pobierania danych z sieci i zapisywania ich w bazie danych bez blokowania głównego wątku.
  • Dowiesz się też, jak anulować wszystkie korutyny po zakończeniu działania funkcji ViewModel.

W przypadku testowania kodu opartego na korutynach omówiliśmy zarówno testowanie zachowania, jak i bezpośrednie wywoływanie funkcji suspend z testów.

Więcej informacji

Więcej informacji o zaawansowanym użyciu współprogramów na Androidzie znajdziesz w samouczku „Zaawansowane współprogramy z użyciem Kotlin Flow i LiveData”.

Korutyny w Kotlinie mają wiele funkcji, które nie zostały omówione w tym laboratorium. Jeśli chcesz dowiedzieć się więcej o korutynach w Kotlinie, przeczytaj przewodniki po korutynach opublikowane przez JetBrains. Więcej wzorców użycia coroutines w Androidzie znajdziesz też w artykule „Poprawianie wydajności aplikacji za pomocą coroutines w Kotlinie”.