Z tego modułu dowiesz się, jak korzystać z aplikacji Kotlin Coroutines w aplikacji na Androida. To nowy sposób na zarządzanie wątkami w tle, który może uprościć kod, ograniczając potrzebę wywołań zwrotnych. Kotwiny to funkcja Kotlin, która konwertuje asynchroniczne wywołania zwrotne dla długotrwałych zadań, takich jak dostęp do bazy danych lub sieci, na kod sekwencyjny.
Poniżej znajdziesz fragment kodu, który pozwoli Ci zorientować się, co będziesz robić.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
Kod wywołania zwrotnego zostanie przekonwertowany na kod sekwencyjny za pomocą odpowiednich algorytmów.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
Zaczniesz od istniejącej aplikacji utworzonej za pomocą komponentów architektury, która korzysta ze stylu wywołania zwrotnego do długotrwałych zadań.
Po zakończeniu tego ćwiczenia będziesz mieć możliwość korzystania z koordynów w celu wczytywania danych z sieci. Będziesz też mieć możliwość zintegrowania ich z aplikacją. Omówimy też sprawdzone metody dotyczące ich i nagrania testu z kodem, który służy do takich organizacji.
Wymagania wstępne
- Znajomość komponentów architektury
ViewModel
,LiveData
,Repository
iRoom
. - Doświadczenie w używaniu składni Kotlina, w tym funkcji rozszerzenia i lambda.
- Podstawowe informacje o używaniu wątków w Androidzie, w tym wątek główny, wątki w tle i wywołania zwrotne.
Co chcesz zrobić
- Kod połączenia napisany z koordynacjami i uzyskiwanie wyników.
- Użyj funkcji zawieszenia, aby utworzyć kod asynchroniczny sekwencyjny.
- Użyj elementów
launch
irunBlocking
, aby kontrolować działanie kodu. - Dowiedz się, jak przekształcać istniejące interfejsy API na odpowiednie schematy przy użyciu interfejsu
suspendCoroutine
. - Używaj kolumn z komponentami architektury.
- Poznaj sprawdzone metody testowania reguł.
Czego potrzebujesz
- Android Studio 3.5 (programowanie może działać z innymi wersjami, ale niektórych brakuje lub może ono wyglądać inaczej).
Jeśli podczas ćwiczeń z programowania napotkasz jakiekolwiek błędy (błędy w kodzie, błędy gramatyczne, niejasne słowa itp.), zgłoś je, klikając link Zgłoś błąd w lewym dolnym rogu ćwiczeń z programowania.
Pobierz kod
Aby pobrać cały kod do tych ćwiczeń z programowania, kliknij ten link:
... lub skopiuj repozytorium GitHub z wiersza poleceń przy użyciu następującego polecenia:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Najczęstszepytania
Najpierw zobaczmy, jak wygląda początkowa aplikacja przykładowa. Aby otworzyć przykładową aplikację w Android Studio, wykonaj te czynności.
- Jeśli pobrałeś plik ZIP
kotlin-coroutines
, rozpakuj go. - Otwórz projekt
coroutines-codelab
w Android Studio. - Wybierz moduł aplikacji
start
. - Kliknij przycisk Uruchom i wybierz emulator lub podłącz urządzenie z Androidem, które musi obsługiwać Androida Lollipop (minimalny obsługiwany pakiet SDK to 21). Powinien wyświetlić się ekran Kotlin Coroutines:
Ta aplikacja startowa korzysta z wątków, by zwiększać opóźnienie po naciśnięciu ekranu. Pobra też nowy tytuł z sieci i wyświetli go na ekranie. Spróbuj to zrobić teraz. Po krótkim czasie liczba i wartość wiadomości powinny się zmienić. W tym ćwiczeniu z programowania przekształcisz tę aplikację w korelacje.
Ta aplikacja używa komponentów architektury i oddziela kod interfejsu w MainActivity
od logiki aplikacji w MainViewModel
. Poświęć chwilę, aby zapoznać się ze strukturą projektu.
MainActivity
wyświetla interfejs użytkownika, rejestruje detektory kliknięć i może wyświetlać właściwośćSnackbar
. Przekazuje zdarzenia doMainViewModel
i aktualizuje ekran na podstawieLiveData
wMainViewModel
.MainViewModel
obsługuje wydarzenia w aplikacjionMainViewClicked
i będzie komunikować się z użytkownikiemMainActivity
za pomocąLiveData.
Executors
określa właściwośćBACKGROUND,
, która może uruchamiać działania w wątku w tle.TitleRepository
pobiera wyniki z sieci i zapisuje je w bazie danych.
Dodawanie algorytmów do projektu
Aby korzystać z korein w Kotlinie, musisz dołączyć bibliotekę coroutines-core
w pliku build.gradle (Module: app)
projektu. Te zadania z programowania zostały już wykonane przez Ciebie, więc nie musisz tego robić, aby ukończyć ćwiczenia.
Kohorty na Androida są dostępne jako główna biblioteka oraz rozszerzenia na Androida:
- kotlinx-corountines-core – główny interfejs do korzystania z koordynów w Kotlinie;
- kotlinx-coroutines-android – obsługa głównego wątku na Androidzie,
Aplikacja startowa zawiera już zależności w sekcji build.gradle.
Podczas tworzenia nowego projektu aplikacji musisz otworzyć aplikację build.gradle (Module: app)
i dodać do niej zależności.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
Na urządzeniach z Androidem główny wątek jest niezbędny, aby można było go zablokować. Główny wątek to pojedynczy wątek, który obsługuje wszystkie aktualizacje interfejsu użytkownika. Jest to też wątek, który wywołuje wszystkie moduły obsługi kliknięć i inne wywołania zwrotne interfejsu. W związku z tym musi ono działać płynnie, aby zapewnić użytkownikom jak najlepsze wrażenia.
Aby Twoja aplikacja była widoczna dla użytkownika, ale nie ma żadnych wstrzymanych wyświetleń, główny wątek musi aktualizować ekran co 16 ms lub dłużej, czyli około 60 klatek na sekundę. Wiele typowych zadań trwa dłużej, takich jak analizowanie dużych zbiorów danych JSON, zapisywanie danych w bazie danych czy pobieranie danych z sieci. Dlatego taki kod wywołania z głównego wątku może spowodować, że aplikacja będzie się wstrzymywać, zacinać, a nawet zatrzymywać. Jeśli zablokujesz wątek główny przez zbyt długi czas, aplikacja może ulec awarii, a w jej oknie pojawi się Aplikacja nie odpowiada.
Obejrzyj film poniżej, by dowiedzieć się, w jaki sposób Twoi współpracownicy potrzebują pomocy w rozwiązaniu tego problemu na Androidzie.
Wzorzec wywołania zwrotnego
Jednym ze sposobów wykonywania długotrwałych zadań bez blokowania głównego wątku jest wywołania zwrotne. Używając wywołań zwrotnych, możesz uruchamiać długotrwałe zadania w wątku. Po zakończeniu zadania wywoływane jest wywołanie zwrotne, które informuje o wyniku w głównym wątku.
Spójrz na przykładowy wzorzec wywołania zwrotnego.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
Kod jest oznaczony adnotacją @UiThread
, więc musi działać szybko, by wykonać go w wątku głównym. Oznacza to, że musi szybko powrócić, aby kolejna aktualizacja ekranu nie była opóźniona. Ponieważ działanie slowFetch
zajmuje kilka sekund, a nawet minut, główny wątek nie może czekać na wynik. Wywołanie zwrotne show(result)
umożliwia uruchomienie wywołania slowFetch
w wątku w tle i zwrócenie wyniku, gdy wynik będzie gotowy.
Usuwanie wywołań zwrotnych za pomocą algorytmów
Wywołanie zwrotne to świetny wzorzec, ale ma kilka wad. Kod, który intensywnie korzysta z wywołań zwrotnych, może stać się trudny do odczytania i trudny do rozważenia. Ponadto wywołania zwrotne nie zezwalają na korzystanie z niektórych funkcji językowych, takich jak wyjątki.
Kotlin pozwala przekonwertować kod oparty na wywołaniach na kod sekwencyjny. Kod sekwencyjny jest zazwyczaj bardziej czytelny, a nawet można w nich korzystać z funkcji językowych takich jak wyjątki.
Na koniec wykonaj dokładnie to samo: poczekaj, aż wynik będzie dostępny od długo trwającego zadania, i kontynuuj wykonywanie. ale w kodzie są bardzo różne.
Słowo kluczowe suspend
to sposób na określenie funkcji lub typu funkcji w Kotlin i przy jej użyciu. Gdy domena wywoła funkcję o nazwie suspend
, zamiast blokować tę funkcję, tak jak w przypadku normalnego wywołania funkcji, zawiesza wykonywanie jej, dopóki wynik nie jest gotowy, a następnie wznowiono miejsce, w którym zakończyło się wynikiem. Podczas oczekiwania na wynik jest odblokowywany wątek, w którym jest on uruchomiony, przez co mogą działać inne funkcje lub algorytmy.
Na przykład w poniższym kodzie funkcje makeNetworkRequest()
i slowFetch()
są funkcjami suspend
.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
Tak jak w przypadku wywołania zwrotnego, makeNetworkRequest
musi jak najszybciej wrócić z wątku głównego, ponieważ jest oznaczony jako @UiThread
. Oznacza to, że zazwyczaj nie może wywoływać metod blokowania takich jak slowFetch
. Oto słowo kluczowe suspend
, które ma w sobie magię.
W porównaniu z kodem zwrotnym kod coordine uzyskuje taki sam efekt, czyli odblokowanie bieżącego wątku z wykorzystaniem mniej kodu. Dzięki temu, że jest sekwencjonalny, można łatwo utworzyć kilka długotrwałych zadań bez tworzenia wielu wywołań zwrotnych. Na przykład kod, który pobiera wynik z 2 punktów końcowych sieci i zapisuje go w bazie danych, można zapisać jako funkcję w planach bez wywołań zwrotnych. W ten sposób:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
W następnej sekcji przedstawisz przykładowe aplikacje.
W tym ćwiczeniu napiszesz wiadomość, w której wyświetli się wiadomość z opóźnieniem. Aby rozpocząć, sprawdź, czy moduł start
jest otwarty w Android Studio.
Omówienie CoroutineScope
W Kotlin wszystkie algorytmy są uruchamiane w CoroutineScope
. Zakres kontroluje czas trwania współdecydowanych przez zadanie. Jeśli anulujesz zadanie w zakresie, zostaną anulowane wszystkie jego kopie rozpoczęte. Na Androidzie możesz użyć zakresu, aby anulować wszystkie uruchomione algorytmy, gdy na przykład użytkownik opuści Activity
lub Fragment
. Zakresy umożliwiają też określenie domyślnego dyspozytora. Dyspozytor określa, który wątek ma być realizowany.
W przypadku algorytmów uruchamianych w interfejsie zwykle poprawne jest uruchomienie ich w Dispatchers.Main
, który jest głównym wątkiem na Androidzie. Algorytm, który rozpoczął się Dispatchers.Main
, nie zablokuje głównego wątku po zawieszeniu. Zmienna ViewModel
niemal zawsze aktualizuje interfejs użytkownika w wątku głównym, więc rozpoczęcie sesji w głównym wątku pozwala zaoszczędzić dodatkowe przełączniki wątków. Współrzędny uruchomiony w wątku głównym może przełączać dyspozytorów w każdej chwili po jego rozpoczęciu. Na przykład inny dyspozytor może przeanalizować duży wynik JSON z głównego wątku.
Korzystanie z widoku viewModelScope
Biblioteka lifecycle-viewmodel-ktx
na Androida dodaje element CoroutineScope do modeli ViewView, które są skonfigurowane tak, by umożliwić uruchamianie algorytmów interfejsu. Aby użyć tej biblioteki, musisz ją uwzględnić w pliku build.gradle (Module: start)
projektu. Ten krok został już wykonany w programach ćwiczeń z programowania.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
Biblioteka dodaje viewModelScope
jako funkcję rozszerzenia klasy ViewModel
. Ten zakres jest powiązany z elementem Dispatchers.Main
i zostanie automatycznie anulowany, gdy ViewModel
zostanie wyczyszczony.
Przechodzenie z wątków na współrzędne
W sekcji MainViewModel.kt
znajdź następne zadanie do wykonania wraz z kodem:
GłównyWidokModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
Ten kod używa BACKGROUND ExecutorService
(zdefiniowanych w util/Executor.kt
) do działania w wątku w tle. Ponieważ sleep
blokuje bieżący wątek, spowoduje to zablokowanie interfejsu użytkownika, jeśli zostanie wywołany w głównym wątku. 1 sekundę po tym, jak użytkownik kliknie widok główny, zażąda wyświetlenia paska powiadomień.
Możesz to sprawdzić, usuwając BackGROUND z kodu i uruchamiając go ponownie. Wskaźnik wczytywania nie wyświetla się i wszystko jest w ciągu sekundy sekundowe.
GłównyWidokModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
Zastąp updateTaps
tym kodem, który robi to samo. Musisz zaimportować launch
i delay
.
GłównyWidokModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
Ten kod działa tak samo, odczekując sekundę przed wyświetleniem paska powiadomień. Istnieją jednak pewne istotne różnice:
viewModelScope.
launch
rozpocznie analizę wviewModelScope
. Oznacza to, że gdy zadanie przesłane doviewModelScope
zostanie anulowane, wszystkie kopie tego zadania/zakresu zostaną anulowane. Jeśli użytkownik opuścił aktywność przed zwróceniem gestudelay
, ten algorytm zostanie automatycznie anulowany, gdy w związku z tym zniszczymy model ViewModel po wywołaniu elementuonCleared
.- Ponieważ
viewModelScope
ma domyślnego dystrybutoraDispatchers.Main
, ten algorytm zostanie uruchomiony w głównym wątku. Później omówimy sposoby używania różnych wątków. - Funkcja
delay
jest funkcjąsuspend
. W Android Studio jest widoczna ikona na rynien. Mimo że ten algorytm działa w wątku głównym,delay
nie zablokuje go na sekundę. Dyspozytor zaplanuje wznowienie połączenia w ciągu jednej sekundy.
Śmiało, uruchom ją. Po kliknięciu widoku głównego pasek sekundy powinien pojawić się sekundę później.
W następnej sekcji omówimy, jak przetestować tę funkcję.
W ramach tego ćwiczenia napiszesz test dotyczący właśnie wpisanego kodu. To ćwiczenie pokazuje, jak przetestować współprogramy w Dispatchers.Main
przy użyciu biblioteki kotlinx-coroutines-test. W dalszej części tego ćwiczenia przeprowadzisz test, który wchodzi w bezpośrednią interakcję z korelacjami.
Przeglądanie dotychczasowego kodu
Otwórz folder MainViewModelTest.kt
w folderze androidTest
.
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
Reguła umożliwia uruchomienie kodu przed wykonaniem testu w jednostce JUnit i po nim. Aby umożliwić nam przetestowanie modelu MainViewModel w teście niedotyczącym urządzenia, wykorzystaliśmy 2 reguły:
InstantTaskExecutorRule
to reguła JUnit, która konfigurujeLiveData
synchronicznie każde zadanieMainCoroutineScopeRule
to niestandardowa reguła w tej bazie kodu, która służy do konfigurowania polaDispatchers.Main
o nazwieTestCoroutineDispatcher
ze źródłakotlinx-coroutines-test
. Dzięki temu testy będą mogły promować wirtualny zegar podczas testów, a kod będzie wykorzystywaćDispatchers.Main
do testów jednostkowych.
W metodzie setup
nowe wystąpienie elementu MainViewModel
jest tworzone za pomocą testów fałszywych – są to fałszywe implementacje sieci i bazy danych podane w kodzie początkowym, które ułatwiają pisanie testów bez użycia rzeczywistej sieci lub bazy danych.
W tym teście fałszywe treści są potrzebne tylko do spełnienia zależności MainViewModel
. W dalszej części tego modułu kodu zaktualizujesz fałszywe informacje, aby były pomocne dla współkoderów.
Napisz test sterujący współprogramami
Dodaj nowy test, który określi, że kliknięcia są aktualizowane sekundę po kliknięciu widoku głównego:
MainViewModelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
Zadzwonimy pod numer onMainViewClicked
, aby uruchomić utworzoną kopię. Ten test sprawdza, czy po kliknięciu opcji "dotknięcia" "0 tap" natychmiast po wywołaniu onMainViewClicked
1 sekunda zostanie zaktualizowana do "1 kliknięcia".
W ramach tego testu używany jest czas wirtualny, aby kontrolować uruchamianie danej aplikacji przez onMainViewClicked
. MainCoroutineScopeRule
umożliwia wstrzymywanie, wznawianie i kontrolowanie uruchamiania algorytmów Dispatchers.Main
. Wywołujemy teraz advanceTimeBy(1_000)
, co spowoduje, że główny dyspozytor natychmiast odbierze odpowiednie algorytmy, które mają zostać wznowione po sekundzie.
Ten test jest w pełni determinatywny, co oznacza, że zawsze działa tak samo. A ponieważ ma on pełną kontrolę nad uruchamianiem algorytmów Dispatchers.Main
w przypadku uruchamiania, nie musi czekać jednej sekundy na ustawienie tej wartości.
Uruchamianie istniejącego testu
- Kliknij prawym przyciskiem myszy nazwę zajęć
MainViewModelTest
w edytorze, aby otworzyć menu kontekstowe. - W menu kontekstowym wybierz Uruchom 'MainViewModelTest'
- W przypadku przyszłych uruchomień możesz wybrać tę konfigurację testową w konfiguracjach obok przycisku na pasku narzędzi. Domyślnie konfiguracja nazywa się MainViewModelTest.
Test powinien pojawić się. Jego uruchomienie powinno zająć mniej niż sekundę.
W następnym ćwiczeniu dowiesz się, jak przekonwertować dane z dotychczasowych interfejsów API wywołań zwrotnych do korzystania z koordynacji.
W tym kroku rozpoczniesz konwertowanie repozytorium na potrzeby współprogramów. Aby to zrobić, dodamy zapisy do pól ViewModel
, Repository
, Room
i Retrofit
.
Zanim zdecydujemy się na korzystanie z koordynatorów, warto dowiedzieć się, za co odpowiadają poszczególne części architektury.
MainDatabase
implementuje bazę danych, korzystając z pokoju, który zapisuje i wczytujeTitle
.MainNetwork
wdraża interfejs API sieci, który pobiera nowy tytuł. Wykorzystuje funkcję Retrofit do pobierania tytułów.Retrofit
jest tak ustawiony, aby losowo zwracać błędy lub symulować dane, ale zachowuje się tak, jakby wysyłał żądania sieci.TitleRepository
wdraża jeden interfejs API do pobierania lub odświeżania tytułu, łącząc dane z sieci i bazy danych.MainViewModel
reprezentuje stan ekranu i obsługuje zdarzenia. Dzięki temu repozytorium będzie odświeżać tytuł, gdy użytkownik dotknie ekranu.
Żądanie sieciowe jest wywoływane przez zdarzenia w interfejsie użytkownika i chcemy rozpocząć na ich podstawie rutynę, więc naturalnym miejscem na jej rozpoczęcie jest ViewModel
.
Wersja wywołania zwrotnego
Otwórz MainViewModel.kt
, by wyświetlić deklarację refreshTitle
.
GłównyWidokModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
Ta funkcja jest wywoływana za każdym razem, gdy użytkownik kliknie ekran, co spowoduje, że repozytorium odświeży tytuł i zapisze nowy w bazie danych.
Ta implementacja używa wywołania zwrotnego, aby wykonać kilka czynności:
- Przed uruchomieniem zapytania wyświetla się ikona wczytywania z atrybutem
_spinner.value = true
- Gdy uzyska wynik, usuwa wskaźnik ładowania z usługi
_spinner.value = false
- Jeśli pojawi się błąd, informuje pasek powiadomień o konieczności wyświetlenia i usunięcia wskaźnika postępu
Pamiętaj, że wywołanie zwrotne onCompleted
nie jest uznawane za wywołanie title
. Ponieważ wszystkie tytuły są zapisywane w bazie danych Room
, interfejs aktualizuje się do aktualnej wersji, obserwując wartość LiveData
(zaktualizowaną przez Room
).
W ramach zmian w korelacjach zachowamy to samo zachowanie. Warto użyć obserwowalnego źródła danych, np. bazy danych Room
, aby automatycznie aktualizować interfejs.
Wersja współprogramów
Przepiszmy refreshTitle
z koordynacjami.
Potrzebujemy go od razu, więc utwórzmy w naszym repozytorium pustą funkcję zawieszenia (TitleRespository.kt
). Zdefiniuj nową funkcję, która używa operatora suspend
do informowania Kotlin, że współpracuje z koordynacjami.
Repository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Gdy wykonasz te ćwiczenia z programowania, zaktualizujesz je, by korzystały z funkcji Retrofit i Room, aby pobrać nowy tytuł i zapisać go w bazie danych za pomocą odpowiednich algorytmów. Na razie wyda 500 milisekund na udaną pracę i będzie można kontynuować.
W MainViewModel
zamień wersję wywołania refreshTitle
na wersję, która uruchamia nową współdzielenie:
GłównyWidokModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Przejdźmy przez tę funkcję:
viewModelScope.launch {
Tak jak w przypadku współrzędnej, aby zaktualizować liczbę kliknięć, zacznij od uruchomienia nowej reguły w viewModelScope
. Zostanie użyte Dispatchers.Main
, co jest dobre. Mimo że refreshTitle
wysyła żądanie sieciowe i zapytanie z bazy danych, może używać algorytmów do udostępniania interfejsu bezpiecznego dla użytkownika. Oznacza to, że będzie można to łatwo wywołać z wątku głównego.
Używamy viewModelScope
, więc gdy użytkownik opuści ten ekran, zadanie rozpoczęte przez niego zostanie automatycznie anulowane. Oznacza to, że nie będzie wysyłać dodatkowych żądań sieciowych ani zapytań do bazy danych.
Kilka następnych wierszy kodu wywołuje funkcję refreshTitle
w repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Zanim cokolwiek zrobi, uruchamia wskaźnik wczytywania, a potem wywołuje funkcję refreshTitle
tak jak zwykłą funkcję. Ponieważ jednak refreshTitle
jest funkcją zawieszającą, jest wykonywana inaczej niż normalnie.
Nie musimy oddzwonić. Współrzędna zostanie zawieszona do chwili jej wznowienia przez usługę refreshTitle
. Choć wygląda jak zwykłe wywołanie funkcji blokowania, przed wznowieniem zapytania bez blokowania głównego wątku będzie automatycznie czekać, aż zapytanie sieci i bazy zostanie ukończone.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Wyjątki w zakresie funkcji zawieszenia działają tak samo jak błędy w zwykłych funkcjach. Jeśli w funkcji funkcji zawieszenia zobaczysz błąd, zostanie on wywołany. Choć są one wykonywane w nieco inny sposób, do zarządzania nimi możesz używać zwykłych bloków try-catch. Jest to przydatne, ponieważ pozwala korzystać z wbudowanej obsługi języka w obsłudze błędów, zamiast tworzyć niestandardową obsługę błędów dla każdego wywołania zwrotnego.
W przypadku wyjątku – zostanie ona domyślnie anulowana przez rodzica. Oznacza to, że razem z kilkoma zadaniami można łatwo anulować.
W końcowym bloku możemy sprawdzić, czy wskaźnik ładowania jest zawsze wyłączony po uruchomieniu zapytania.
Uruchom aplikację ponownie, wybierając konfigurację start, a następnie naciskając . Po kliknięciu w dowolnym miejscu zobaczysz wskaźnik wczytywania. Taki tytuł pozostanie taki sam, ponieważ nie mamy jeszcze połączenia z naszą siecią lub bazą danych.
W następnym ćwiczeniu zaktualizujesz repozytorium tak, aby działało.
Z tego ćwiczenia dowiesz się, jak zmienić wątek, w którym działa algorytm, by wdrożyć aktywną wersję elementu TitleRepository
.
Sprawdź istniejący kod wywołania zwrotnego w refreshTitle
Otwórz TitleRepository.kt
i sprawdź obecną implementację wywołania zwrotnego.
TitleRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
W metodzie TitleRepository.kt
metoda refreshTitleWithCallbacks
jest zaimplementowana za pomocą wywołania zwrotnego, aby powiadomić rozmówcę o stanie wczytywania i błędzie.
Ta funkcja wykonuje dość dużo czynności, by wdrożyć odświeżanie.
- Przełącz na inny wątek z użytkownikiem
BACKGROUND
ExecutorService
- Wyślij żą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 się powiedzie, zapisz plik w bazie danych za pomocą wywołania
insertTitle
i wywołaj metodęonCompleted()
. - Jeśli wynik się nie powiódł lub jest wyjątek, wywołaj metodę onError, by poinformować rozmówcę o niepowodzeniu odświeżania.
Ta implementacja oparta na wywołaniach zwrotnych jest główna – bezpieczna, ponieważ nie blokuje głównego wątku. Należy jednak użyć wywołania zwrotnego, aby poinformować rozmówcę o zakończeniu zadania. Oprócz tego wywołuje wywołania zwrotne w wątku, w którym funkcja BACKGROUND
się przełączyła.
Połączenia telefoniczne blokujące połączenia z koordynacjami
Bez wprowadzenia odpowiednich sieci lub bazy danych możemy ustawić ten kod jako główny, wykorzystując odpowiednie reguły. Pozwoli to nam wyeliminować wywołanie zwrotne i przekazać wynik z powrotem do wątku, w którym go początkowo wywołaliśmy.
Możesz używać tego wzorca zawsze wtedy, gdy chcesz zablokować lub bardzo intensywnie pracować nad procesem, takie jak sortowanie i filtrowanie dużej listy czy odczytywanie danych z dysku.
Do przełączania się między dyspozytorami używa się withContext
. Wywołanie withContext
przekierowuje do innego dyspozytora tylko w lambdzie, a następnie wraca do dyspozytora, który podał ten wynik w wyniku lambdy.
Domyślnie Kotlin współpracuje z 3 dyspozytorami: Main
, IO
i Default
. Dyspozytor jest zoptymalizowany pod kątem operacji wejścia-wyjścia, np. odczytu z sieci lub dysku, a domyślny dyspozytor jest zoptymalizowany pod kątem zadań wymagających dużej mocy procesora.
TitleRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
Ta implementacja wykorzystuje wywołania blokowania w sieci i bazie danych, ale nadal jest to nieco prostsze niż w przypadku wersji wywołania zwrotnego.
Ten kod nadal używa blokowania wywołań. Wywołanie execute()
i insertTitle(...)
zablokuje wątek, w którym pracuje ten algorytm. Jednak przełączając się na Dispatchers.IO
za pomocą withContext
, blokujemy jeden z wątków w dystrybucji zamówienia reklamowego. Współrzędna, która prawdopodobnie była uruchomiona w Dispatchers.Main
, zostanie zawieszona do zakończenia lambdy withContext
.
W porównaniu z wersją wywołania zwrotnego istnieją dwie ważne różnice:
withContext
zwraca wynik z dyspozytorem, który go wywołał. Tutaj jest toDispatchers.Main
. Wersja wywołania zwrotnego nazywa się wywołaniem zwrotnym w wątku w usłudze wykonawcyBACKGROUND
.- Rozmówca nie musi przekazywać wywołania do tej funkcji. Mogą polegać na zawieszeniu i wznowieniu, aby otrzymać wynik lub błąd.
Uruchom ponownie aplikację
Jeśli uruchomisz aplikację ponownie, zobaczysz, że nowa implementacja oparta na danych wczytuje dane z sieci.
W następnym kroku zintegrujesz współdzielenie z Room i Retrofit.
Aby kontynuować integrację kolumn, użyjemy funkcji zawieszania w stabilnych wersjach Pokój i Retrofit, a następnie uprościmy kod, który właśnie napisano, korzystając z funkcji zawieszenia.
Kohorty w pokoju
Najpierw otwórz MainDatabase.kt
i ustaw insertTitle
jako funkcję zawieszenia:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Gdy to zrobisz, pokój stanie się głównym zabezpieczeniem i zostanie automatycznie wykonane w wątku w tle. Oznacza to jednak, że zapytanie możesz wywołać tylko z poziomu współrzędu.
To wszystko, co musisz zrobić, aby używać kolumn w pokoju. Stylowo.
Koordynacje w projekcie retro
Teraz zobaczmy, jak zintegrować współprogramy z Retrofit. Otwórz MainNetwork.kt
i zmień fetchNextTitle
na funkcję zawieszenia.
MainNetwork.kt,
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Aby używać funkcji zawieszenia z Retrofit, musisz wykonać 2 rzeczy:
- Dodawanie modyfikatora zawieszenia do funkcji
- Usuń opakowanie
Call
z typu zwrotu. Tutaj zwracamyString
, ale możesz też zwrócić złożony typ w formacie JSON. Jeśli nadal chcesz zezwolić na dostęp do elementuResult
w stylu retrofit i cały czas, możesz zwrócićResult<String>
zamiastString
za pomocą funkcji zawieszenia.
Program Retrofit automatycznie ustawi funkcje zawieszenia głównie bezpieczne, dzięki czemu możesz je wywoływać bezpośrednio z poziomu usługi Dispatchers.Main
.
Korzystanie z sali i stylu retro
Teraz, gdy Room i Retrofit obsługują funkcje zawieszenia, możemy ich użyć w naszym repozytorium. Otwórz aplikację TitleRepository.kt
i sprawdź, jak użycie funkcji zawieszania interfejsu znacznie upraszcza logikę, nawet w przypadku wersji blokowanej:
TytułRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
To krócej o wiele krócej. Co się stało? Okazuje się, że korzystanie z zawieszenia i wznowienia umożliwia znacznie krótsze kodowanie. Dzięki niemu możesz korzystać z typów zwrotów, np. String
lub User
, zamiast Call
. To nic nie szkodzi, bo Retrofit
może wykonać żądanie sieciowe w wątku w tle i wznowić rutynę po zakończeniu wywołania.
Jeszcze lepiej pozbyliśmy się withContext
. Ponieważ funkcje Room i Retrofit zapewniają główne funkcje zawieszania, można bezpiecznie zarządzać asynchroniczną pracą zespołu Dispatchers.Main
.
Naprawianie błędów kompilacji
Przejście na współrzędne wiąże się ze zmianą podpisu funkcji, ponieważ nie można wywoływać funkcji zawieszenia z poziomu funkcji zwykłej. Po dodaniu modyfikatora suspend
w tym kroku wygenerowano kilka błędów kompilatora pokazującej, co się stanie, jeśli zmienisz funkcję, która ma być zawieszona w rzeczywistym projekcie.
Przejdź przez ten projekt i napraw błędy kompilatora, zmieniając funkcję zawieszenia. Oto szybkie rozwiązania dla każdego z nich:
TestingFakes.kt
Zaktualizuj fałszywe informacje testowe, by obsługiwały nowe modyfikatory zawieszenia.
TytułDaoFake
- Naciśnij alt-Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w obrębie heiranchi
Fake MainFake
- Naciśnij alt-Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w obrębie heiranchi
- Zastąp
fetchNextTitle
tą funkcją
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Naciśnij alt-Enter, aby dodać modyfikatory zawieszenia do wszystkich funkcji w obrębie heiranchi
- Zastąp
fetchNextTitle
tą funkcją
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Usuń funkcję
refreshTitleWithCallbacks
, ponieważ nie jest już używana.
Uruchamianie aplikacji
Uruchom ponownie aplikację. Po skompilowaniu zobaczysz, że wczytuje ona dane przy użyciu algorytmów, od modeli ViewView do Room i Retrofit.
Gratulacje! Udało Ci się całkowicie zamienić tę aplikację na współprogramy! Na koniec zobaczmy, jak przetestować to, co właśnie zrobiliśmy.
W tym ćwiczeniu napiszesz test, który bezpośrednio wywołuje funkcję suspend
.
Ponieważ interfejs refreshTitle
jest udostępniany jako publiczny interfejs API, jest testowany bezpośrednio, co wskazuje sposób wywoływania funkcji współprogramów z testów.
Oto funkcja funkcji refreshTitle
zaimplementowana w ostatnim ćwiczeniu:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Tworzenie testu wywołującego funkcję zawieszenia
Otwórz folder TitleRepositoryTest.kt
w folderze test
, który zawiera 2 TODOS.
Spróbuj wywołać refreshTitle
z pierwszego testu whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Ponieważ refreshTitle
jest funkcją suspend
, Kotlin nie wie, jak ją wywołać, chyba że jest współprogramowaną lub inną funkcją zawieszenia, a wystąpi błąd kompilatora, np. "Zawieś funkcję odświeżaniaTitleTitle powinno być wywoływane tylko z odpowiedniej lub innej funkcji zawieszenia."
Ten tester nie wie nic o algorytmach, więc nie możemy przeprowadzić tego testu w celu zawieszenia. launch
może mieć odpowiednią postać przy użyciu CoroutineScope
, tak jak w ViewModel
, ale przed testem trzeba przeprowadzić go do końca. Po zwróceniu funkcji testowej test się kończy. Kodowanie zaczynające się od launch
to kod asynchroniczny, który może zostać w przyszłości wykonany. Aby więc przetestować ten kod asynchroniczny, musisz jakoś przekazać testowi czas oczekiwania na zakończenie połączenia. Działanie launch
nie jest blokowane, co oznacza, że jest zwracane od razu i może po upływie czasu nadal wykonywać kod – nie można go użyć w testach. Przykład:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
Ten test czasem się nie powiedzie. Wywołanie funkcji launch
natychmiast powróci i zostanie wykonane w tym samym czasie co reszta przypadku testowego. Test nie jest w stanie sprawdzić, czy test refreshTitle
został już uruchomiony, a przy tym nie ma dowodów na to, że baza danych została zaktualizowana. A jeśli refreshTitle
zwrócił wyjątek, nie zostanie on uwzględniony w testowym stosie wywołań. Zostanie on przekierowany do modułu obsługi wyjątku typu GlobalScope
'.
Biblioteka kotlinx-coroutines-test
zawiera funkcję runBlockingTest
, która blokuje dane podczas wywoływania funkcji zawieszenia. Gdy runBlockingTest
wywołuje funkcję zawieszenia lub launches
nowy współprogram, uruchamia ją domyślnie. Można to potraktować jako sposób przekształcenia funkcji zawieszenia i koordynacji w normalne wywołania funkcji.
Dodatkowo runBlockingTest
będzie zwracać odrzucone wyjątki. Dzięki temu łatwiej jest sprawdzić, kiedy algorytm generuje wyjątek.
Przeprowadzanie testu na 1 kohorcie
Umieść wywołanie refreshTitle
w tagu runBlockingTest
i usuń kod GlobalScope.launch
z subject.refreshTitle().
TitleRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
W tym teście uwzględniane są fałszywe informacje wprowadzone w celu sprawdzenia, czy fragment „OK” jest wstawiony do bazy danych przez usługę refreshTitle
.
Gdy test wywoła polecenie runBlockingTest
, zostanie zablokowane do czasu zakończenia odpowiedniej procedury uruchomionej przez runBlockingTest
. Następnie wywołujemy metodę refreshTitle
za pomocą zwykłego mechanizmu zawieszenia i wznawiania, aby poczekać, aż wiersz bazy danych zostanie dodany do naszego fałszywego systemu.
Po zakończeniu testu runBlockingTest
zwracany jest zwrot.
Pisanie testu czasu oczekiwania
Chcemy dodać krótki czas oczekiwania do żądania sieci. Najpierw zapisz test, a potem przekrocz czas oczekiwania. Utwórz nowy test:
TitleRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
W tym teście wykorzystano fałszywą wartość MainNetworkCompletableFake
, która jest fałszywą nazwą sieci i ma na celu zawieszenie rozmówców do czasu kontynuowania testu. Gdy refreshTitle
spróbuje wysłać żądanie sieciowe, zostanie ono zawieszone na zawsze, ponieważ chcemy przetestować limity czasu.
Następnie uruchamia osobny algorytm o nazwie refreshTitle
. Jest to kluczowy moment testowania czasu oczekiwania, który powinien przypadać w innym przypadku niż runBlockingTest
. W ten sposób możemy wywołać kolejny wiersz (advanceTimeBy(5_000)
), który przesuwa się o 5 sekund i powoduje przekroczenie limitu czasu.
To jest pełny test limitu czasu, który zakończy się po wdrożeniu limitu czasu.
Uruchom go i sprawdź, co się stanie:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Jedną z funkcji runBlockingTest
jest to, że nie pozwala ona ujawnić kolumn po zakończeniu testu. Jeśli na koniec testu pojawią się niedokończone testy, takie jak te, których nie udało się wprowadzić, zakończy się to niepowodzeniem.
Dodawanie limitu czasu
Otwórz plik TitleRepository
i dodaj pięciosekundowy czas oczekiwania do pobrania sieci. Możesz to zrobić za pomocą funkcji withTimeout
:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Uruchom test. Wszystkie testy powinny zakończyć się powodzeniem.
W następnym ćwiczeniu dowiesz się, jak pisać funkcje związane z kolejnością za pomocą algorytmów współrzędnych.
W tym ćwiczeniu otrzymasz refaktoryzację refreshTitle
na MainViewModel
, by użyć funkcji ogólnego wczytywania danych. Dowiesz się z niego, jak tworzyć funkcje o wyższej kolejności, które korzystają z programów.
Obecna implementacja refreshTitle
działa, ale możemy utworzyć ogólną regułę wczytywania danych, która będzie zawsze pokazywać wskaźnik postępu. Może to być pomocne w bazie kodu, która wczytuje dane w odpowiedzi na kilka zdarzeń i chce, aby wskaźnik wczytywania ładował się systematycznie.
Przeglądanie aktualnej implementacji w każdym wierszu z wyjątkiem repository.refreshTitle()
zawiera powtarzające się błędy i błędy wyświetlania.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Korzystanie z kohorty w funkcjach wyższego poziomu
Dodaj ten kod do MainViewModel.kt
GłównyWidokModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Teraz zastosuj refaktory refreshTitle()
, by użyć tej funkcji wyższego rzędu.
GłównyWidokModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Wyodrębniając logikę związaną z wyświetlaniem wskaźnika wczytywania i pokazywanie błędów, uprościliśmy rzeczywisty kod niezbędny do wczytywania danych. Wskaźnik wczytywania lub wyświetlanie błędu to coś, co można łatwo uogólnić w przypadku każdego wczytywania danych. Rzeczywiste źródło danych i miejsce docelowe muszą być za każdym razem określone.
Aby utworzyć tę abstrakcję, launchDataLoad
przyjmuje argument block
będący zawieszoną lambdą. Zawieszanie lambdy pozwala wywołać funkcje zawieszenia. Właśnie w ten sposób Kotlin wdraża kreatory współpracy launch
i runBlocking
używane w tym ćwiczeniu z programowania.
// suspend lambda
block: suspend () -> Unit
Aby utworzyć lambdę zawieszoną, zacznij od słowa kluczowego suspend
. Deklarację wypełniają strzałki funkcji i typy Unit
.
Często nie trzeba deklarować własnych lambd wwieszeniowych, ale mogą one pomóc w tworzeniu abstrakcji takich, które wyrażają powtarzalną logikę.
W tym ćwiczeniu dowiesz się, jak używać kodu opartego na algorytmach WorkManager.
Co to jest WorkManager
Na urządzeniu z Androidem jest wiele opcji odroczonego działania w tle. To ćwiczenie pokazuje, jak zintegrować WorkManager z algorytmem współpracy. WorkManager to kompatybilna, elastyczna i prosta biblioteka do odroczonych zadań w tle. W takich przypadkach zalecamy korzystanie z WorkManagera.
WorkManager jest częścią Jet Jetpacka i komponentu Architektura przeznaczonego do pracy w tle, który wymaga kombinacji oportunistycznej i gwarantowanej wykonania. Wykonując tę operacji, WorkManager wykona swoje zadanie tak szybko, jak to możliwe. Gwarantowane wykonanie oznacza, że WorkManager zajmie się logiką uruchomienia Twojej pracy w różnych sytuacjach, nawet jeśli opuścisz aplikację.
Z tego względu WorkManager jest dobrym rozwiązaniem w przypadku zadań, które muszą zostać zakończone.
Oto przykłady zadań, w których dobrze działa WorkManager:
- Przesyłam logi
- stosowanie filtrów do obrazów i zapisywanie ich;
- Okresowa synchronizacja danych lokalnych z siecią
Korzystanie z kolumn aplikacji razem z WorkWork
WorkManager udostępnia różne implementacje podstawowej klasy ListanableWorker
przeznaczone do różnych przypadków użycia.
Najprostsza klasa instancji roboczych pozwala nam wykonać pewne działania synchroniczne przez WorkManager. Mimo to do tej pory udało nam się przekonwertować naszą bazę kodu na korzystanie z programów i funkcji zawieszania. Najlepiej jest użyć klasy CoroutineWorker
, która pozwala zdefiniować funkcję doWork()
jako funkcję zawieszenia.
Aby rozpocząć, otwórz aplikację RefreshMainDataWork
. Jest już przedłużona do CoroutineWorker
. Musisz wdrożyć doWork
.
W ramach funkcji suspend
doWork
wywołaj refreshTitle()
z repozytorium i zwróć odpowiedni wynik!
Gdy wykonasz TODO, kod będzie wyglądać tak:
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
CoroutineWorker.doWork()
należy do funkcji zawieszających. W przeciwieństwie do prostszej klasy Worker
ten kod nie działa w usłudze wykonawcy określonej w konfiguracji WorkManager, ale używa dyspozytora w grupie coroutineContext
(domyślnie Dispatchers.Default
).
Testowanie narzędzia CoroutineWorker
Żadna baza kodu nie powinna być kompletna bez testowania.
WorkManager udostępnia kilka sposobów testowania klas Worker
. Aby dowiedzieć się więcej o pierwotnej infrastrukturze testowej, zapoznaj się z dokumentacją.
W wersji 2.1 WorkManager wprowadziliśmy nowy zestaw interfejsów API, który obsługuje prostszy sposób testowania klas ListenableWorker
, dzięki czemu działa w ramach CoroutineWorker. W naszym kodzie wykorzystamy jeden z tych nowych interfejsów API: TestListenableWorkerBuilder
.
Aby dodać nowy test, zaktualizuj plik RefreshMainDataWorkTest
w folderze androidTest
.
Zawartość pliku:
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
Zanim przejdziemy do testu, informujemy firmę WorkManager
o fabryce, by umożliwić nam wstrzykiwanie fałszywej sieci.
Sam test używa TestListenableWorkerBuilder
do utworzenia naszego instancji roboczej, którą możemy uruchomić, wywołując metodę startWork()
.
WorkManager to tylko jeden z przykładów użycia algorytmów do upraszczania projektowania interfejsów API.
W tym ćwiczeniu omówiliśmy podstawowe zagadnienia, takie jak wykorzystanie algorytmów w Twojej aplikacji.
Omówione zagadnienia:
- Jak zintegrować aplikacje z Androidem z zadaniami UI i WorkManager, aby uprościć programowanie asynchroniczne?
- Jak używać algorytmów w ramach
ViewModel
do pobierania danych z sieci i zapisywania ich w bazie danych bez blokowania głównego wątku. - Jak anulować wszystkie algorytmy po zakończeniu operacji
ViewModel
.
W przypadku testowania kodu opartego na algorytmie współuczestniczył zarówno w teście testowania, jak i bezpośrednim wywoływaniu funkcji suspend
z testów.
Więcej informacji
Aby dowiedzieć się więcej o zaawansowanym korzystaniu z koordynów na urządzeniach z Androidem, zapoznaj się z artykułem „Advanced Coroutines with Kotlin Flow and LiveData"”.
Korynki te mają wiele funkcji, które nie zostały omówione w tym ćwiczeniu z programowania. Jeśli chcesz dowiedzieć się więcej o koordynacjach z Kotlin, przeczytaj przewodniki po Kotwicach publikowane przez JetBrains. Zobacz też "Zwiększ wydajność aplikacji dzięki Koordinom" aby uzyskać więcej wzorców wykorzystania współprogramów na Androidzie.