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
,Repository
iRoom
. - 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ń
launch
irunBlocking
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:
… 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.
- Jeśli pobrano plik ZIP
kotlin-coroutines
, rozpakuj go. - Otwórz
coroutines-codelab
projekt w Android Studio. - Wybierz moduł aplikacji
start
. - Kliknij przycisk
Uruchom 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.
MainActivity
wyświetla interfejs, rejestruje detektory kliknięć i może wyświetlać elementSnackbar
. Przekazuje zdarzenia doMainViewModel
i aktualizuje ekran na podstawieLiveData
wMainViewModel
.MainViewModel
obsługuje zdarzenia wonMainViewClicked
i komunikuje się zMainActivity
za pomocąLiveData.
.Executors
definiujeBACKGROUND,
, który może uruchamiać elementy w wątku w tle.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()
i 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ć launch
i delay
.
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:
viewModelScope.
launch
rozpocznie korutynę wviewModelScope
. Oznacza to, że gdy zadanie przekazane doviewModelScope
zostanie anulowane, wszystkie korutyny w tym zadaniu lub zakresie zostaną anulowane. Jeśli użytkownik opuścił aktywność przed zwróceniem wartościdelay
, ta korutyna zostanie automatycznie anulowana, gdy podczas niszczenia obiektu ViewModel zostanie wywołana funkcjaonCleared
.- Ponieważ
viewModelScope
ma domyślny dyspozytorDispatchers.Main
, ta korutyna zostanie uruchomiona w wątku głównym. Później zobaczymy, jak używać różnych wątków. - 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ł:
InstantTaskExecutorRule
to reguła JUnit, która konfigurujeLiveData
do synchronicznego wykonywania każdego zadania.MainCoroutineScopeRule
to niestandardowa reguła w tej bazie kodu, która konfigurujeDispatchers.Main
tak, aby używaćTestCoroutineDispatcher
zkotlinx-coroutines-test
. Umożliwia to testom przesuwanie wirtualnego zegara na potrzeby testowania, a kodowi – używanieDispatchers.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
- Kliknij prawym przyciskiem myszy nazwę klasy
MainViewModelTest
w edytorze, aby otworzyć menu kontekstowe. - W menu kontekstowym wybierz
Uruchom „MainViewModelTest”.
- W przyszłości możesz wybrać tę konfigurację testu w konfiguracjach obok przycisku
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
, Room
i Retrofit
współprogramy.
Zanim przełączymy je na korzystanie z korutyn, warto zrozumieć, za co odpowiada każda część architektury.
MainDatabase
implementuje bazę danych za pomocą biblioteki Room, która zapisuje i wczytujeTitle
.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.TitleRepository
implementuje pojedynczy interfejs API do pobierania lub odświeżania tytułu przez łączenie danych z sieci i bazy danych.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ć.
W 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ę refreshTitle
w repository
.
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śnij. 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))
}
}
}
W 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.
- Przełączanie się na inny wątek za pomocą klawiszy
BACKGROUND
ExecutorService
- Uruchom żądanie sieciowe
fetchNextTitle
za pomocą metody blokowaniaexecute()
. Spowoduje to uruchomienie żądania sieciowego w bieżącym wątku, w tym przypadku w jednym z wątków wBACKGROUND
. - Jeśli wynik jest pozytywny, zapisz go w bazie danych za pomocą polecenia
insertTitle
i wywołaj metodęonCompleted()
. - 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
, IO
i Default
. 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()
i 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:
withContext
zwraca wynik do wywołującego go dyspozytora, w tym przypadkuDispatchers.Main
. Wersja wywołania zwrotnego wywoływała wywołania zwrotne w wątku w usłudze wykonawczejBACKGROUND
.- 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:
- Dodaj do funkcji modyfikator suspend
- 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łnegoResult
, możesz zwrócićResult<String>
zamiastString
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
- Naciśnij Alt+Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w hierarchii.
MainNetworkFake
- Naciśnij Alt+Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w hierarchii.
- Zastąp
fetchNextTitle
tą funkcją
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Naciśnij Alt+Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w hierarchii.
- 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 launch
i runBlocking
, 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 Jetpack i komponentem 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”.