Korutyna to wzorzec współbieżności, którego możesz używać na Androidzie, aby uprościć kod wykonywany asynchronicznie. Korutyny zostały dodane do języka Kotlin w wersji 1.3 i są oparte na sprawdzonych koncepcjach z innych języków.
W Androidzie współprogramy pomagają zarządzać długotrwałymi zadaniami, które w przeciwnym razie mogłyby blokować główny wątek i sprawić, że aplikacja przestanie odpowiadać. Ponad 50% profesjonalnych deweloperów korzystających z korutyn zgłosiło wzrost produktywności. W tym artykule opisujemy, jak za pomocą korutyn w Kotlinie rozwiązać te problemy, aby pisać czystszy i bardziej zwięzły kod aplikacji.
Funkcje
Korutyny to zalecane przez nas rozwiązanie do programowania asynchronicznego na Androidzie. Warto zwrócić uwagę na te funkcje:
- Lekkie: na jednym wątku możesz uruchomić wiele współprogramów dzięki obsłudze zawieszenia, które nie blokuje wątku, w którym działa współprogram. Zawieszanie oszczędza pamięć w porównaniu z blokowaniem, a jednocześnie obsługuje wiele równoczesnych operacji.
- Mniej wycieków pamięci: używaj strukturalnego współbieżności do uruchamiania operacji w określonym zakresie.
- Wbudowana obsługa anulowania:Anulowanie jest automatycznie propagowane w hierarchii działających współprogramów.
- Integracja z Jetpackiem: wiele bibliotek Jetpacka zawiera rozszerzenia, które zapewniają pełną obsługę współprogramów. Niektóre biblioteki udostępniają też własny zakres współprogramu, którego możesz używać do strukturalnej równoczesności.
Przegląd przykładów
Zgodnie z przewodnikiem po architekturze aplikacji przykłady w tym temacie wysyłają żądanie sieciowe i zwracają wynik do głównego wątku, w którym aplikacja może następnie wyświetlić go użytkownikowi.
W szczególności komponent ViewModel architektury wywołuje warstwę repozytorium w głównym wątku, aby wywołać żądanie sieciowe. W tym przewodniku znajdziesz różne rozwiązania, które wykorzystują współprogramy do odblokowywania wątku głównego.
ViewModel zawiera zestaw rozszerzeń KTX, które działają bezpośrednio z korutynami. Te rozszerzenia to lifecycle-viewmodel-ktxbiblioteka, która jest używana w tym przewodniku.
Informacje o zależności
Aby używać współprogramów w projekcie aplikacji na Androida, dodaj do pliku build.gradle aplikacji tę zależność:
Odlotowe
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
Wykonywanie w wątku w tle
Wysyłanie żądania sieciowego w głównym wątku powoduje jego oczekiwanie, czyli blokowanie, aż do otrzymania odpowiedzi. Wątek jest zablokowany, więc system operacyjny nie może wywołać funkcji onDraw(), co powoduje zawieszenie aplikacji i może prowadzić do wyświetlenia okna „Aplikacja nie odpowiada” (ANR). Aby zwiększyć komfort użytkowników, wykonajmy tę operację w wątku w tle.
Najpierw przyjrzyjmy się klasie Repository i zobaczmy, jak wysyła ona żądanie sieciowe:
sealed class Result<out R> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // Function that makes the network request, blocking the current thread fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { val url = URL(loginUrl) (url.openConnection() as? HttpURLConnection)?.run { requestMethod = "POST" setRequestProperty("Content-Type", "application/json; utf-8") setRequestProperty("Accept", "application/json") doOutput = true outputStream.write(jsonBody.toByteArray()) return Result.Success(responseParser.parse(inputStream)) } return Result.Error(Exception("Cannot open HttpURLConnection")) } }
makeLoginRequest jest synchroniczna i blokuje wątek wywołujący. Aby modelować odpowiedź na żądanie sieciowe, mamy własną klasę Result.
ViewModel wywołuje żądanie sieciowe, gdy użytkownik kliknie np. przycisk:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } }
W poprzednim kodzie LoginViewModel blokuje wątek UI podczas wysyłania żądania sieciowego. Najprostszym rozwiązaniem, które pozwala przenieść wykonywanie operacji z wątku głównego, jest utworzenie nowego współprogramu i wykonanie żądania sieciowego w wątku wejścia/wyjścia:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine to move the execution off the UI thread viewModelScope.launch(Dispatchers.IO) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } } }
Przyjrzyjmy się kodowi korutyn w funkcji login:
viewModelScopeto wstępnie zdefiniowanyCoroutineScope, który jest dołączony doViewModelrozszerzeń KTX. Pamiętaj, że wszystkie korutyny muszą działać w zakresie.CoroutineScopezarządza co najmniej 1 powiązaną korutyną.launchto funkcja, która tworzy współprogram i przekazuje wykonanie jej treści do odpowiedniego dyspozytora.Dispatchers.IOoznacza, że ten współprogram powinien być wykonywany w wątku zarezerwowanym dla operacji wejścia-wyjścia.
Funkcja login jest wykonywana w ten sposób:
- Aplikacja wywołuje funkcję
loginz warstwyVieww głównym wątku. launchtworzy nową współprogramową, a żądanie sieciowe jest wysyłane niezależnie w wątku zarezerwowanym dla operacji wejścia/wyjścia.- Podczas działania współprogramu funkcja
loginkontynuuje wykonywanie i zwraca wartość, być może zanim żądanie sieciowe zostanie zakończone. Pamiętaj, że dla uproszczenia odpowiedź sieci jest na razie ignorowana.
Ponieważ ten współprogram jest uruchamiany za pomocą viewModelScope, jest wykonywany w zakresie ViewModel. Jeśli ViewModel zostanie zniszczony, ponieważ użytkownik opuści ekran, viewModelScope zostanie automatycznie anulowany, a wszystkie uruchomione współprogramy również zostaną anulowane.
Jednym z problemów z poprzednim przykładem jest to, że wszystko, co wywołuje funkcję
makeLoginRequest, musi pamiętać o wyraźnym przeniesieniu wykonania z głównego wątku. Zobaczmy, jak możemy zmodyfikować Repository, aby rozwiązać ten problem.
Używanie korutyn w celu zapewnienia bezpieczeństwa wątku głównego
Funkcję uznajemy za bezpieczną dla wątku głównego, jeśli nie blokuje aktualizacji interfejsu w wątku głównym. Funkcja makeLoginRequest nie jest bezpieczna dla wątku głównego, ponieważ wywołanie jej
makeLoginRequest z wątku głównego blokuje interfejs. Użyj funkcji
withContext() z biblioteki współprogramów, aby przenieść wykonanie współprogramu do innego wątku:
class LoginRepository( // ... ) { // ... suspend fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { // Move the execution of the coroutine to the I/O dispatcher return withContext(Dispatchers.IO) { // Blocking network request code } } }
withContext(Dispatchers.IO) przenosi wykonanie współprogramu do wątku wejścia-wyjścia, dzięki czemu nasza funkcja wywołująca jest bezpieczna dla wątku głównego i umożliwia aktualizowanie interfejsu w razie potrzeby.
makeLoginRequest jest też oznaczony słowem kluczowym suspend. Ten słowo kluczowe to sposób, w jaki Kotlin wymusza wywoływanie funkcji z poziomu korutyny.
W tym przykładzie współprogram jest tworzony w LoginViewModel.
Gdy makeLoginRequest przenosi wykonanie poza wątek główny, współprogram w funkcji login może być teraz wykonywany w wątku głównym:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine on the UI thread viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" // Make the network call and suspend execution until it finishes val result = loginRepository.makeLoginRequest(jsonBody) // Display result of the network request to the user when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
Pamiętaj, że w tym przypadku nadal potrzebna jest korutyna, ponieważ makeLoginRequest jest funkcją suspend, a wszystkie funkcje suspend muszą być wykonywane w korutynie.
Ten kod różni się od poprzedniego przykładu login na kilka sposobów:
- Funkcja
launchnie przyjmuje parametruDispatchers.IO. Jeśli nie przekażeszDispatcherdolaunch, wszystkie współprogramy uruchomione zviewModelScopebędą działać w wątku głównym. - Wynik żądania sieci jest teraz obsługiwany w celu wyświetlenia interfejsu informującego o sukcesie lub niepowodzeniu.
Funkcja logowania jest teraz wykonywana w ten sposób:
- Aplikacja wywołuje funkcję
login()z warstwyVieww głównym wątku. launchtworzy nowy współprogram w głównym wątku i rozpoczyna jego wykonywanie.- W współprogramie wywołanie funkcji
loginRepository.makeLoginRequest()now zawiesza dalsze wykonanie współprogramu do momentu, ażwithContextblok wmakeLoginRequest()zakończy działanie. - Gdy blok
withContextzakończy działanie, współprogram wlogin()wznawia wykonywanie w głównym wątku z wynikiem żądania sieciowego.
Obsługa wyjątków
Aby obsługiwać wyjątki, które może zgłaszać warstwa Repository, użyj wbudowanej obsługi wyjątków w języku Kotlin.
W przykładzie poniżej używamy bloku try-catch:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" val result = try { loginRepository.makeLoginRequest(jsonBody) } catch (e: Exception) { Result.Error(Exception("Network request failed")) } when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
W tym przykładzie każdy nieoczekiwany wyjątek zgłoszony przez wywołanie makeLoginRequest() jest traktowany jako błąd w interfejsie.
Dodatkowe materiały o korutynach
Więcej informacji o współprogramach na Androidzie znajdziesz w artykule Zwiększanie wydajności aplikacji za pomocą współprogramów Kotlin.
Więcej informacji o korutynach znajdziesz w tych artykułach: