Korutyna to wzorzec projektu równoczesności, którego można używać na Androidzie do upraszczania kodu uruchamianego asynchronicznie. Korutyny zostały dodane do Kotlin w wersji 1.3 i oparte na koncepcjach znanych z innych języków.
Na Androidzie współprogramy pomagają zarządzać długotrwałymi zadaniami, które mogą zablokować wątek główny i sprawić, że aplikacja przestanie odpowiadać. Ponad 50% profesjonalnych programistów, którzy korzystają z korekty, zauważyło wzrost produktywności. W tym temacie opisujemy, jak za pomocą współprogramów Kotlin rozwiązać te problemy, co pozwoli na pisanie bardziej przejrzystego i zwięzłego kodu aplikacji.
Funkcje
Coroutines to zalecane rozwiązanie do programowania asynchronicznego na Androidzie. Do najważniejszych funkcji należą:
- Lekka: w jednym wątku możesz uruchamiać wiele współprogramów dzięki obsłudze zawieszenia, które nie blokuje wątku, w którym działa współpraca. Zawieszenie pozwala zaoszczędzić pamięć, a jednocześnie obsługiwać wiele równoczesnych operacji.
- Mniej wycieków pamięci: używaj uporządkowanej równoczesności do uruchamiania operacji w zakresie.
- Wbudowana obsługa anulowania: informacje o anulowaniu są rozpowszechniane automatycznie w ramach hierarchii bieżącej.
- Integracja z Jetpackiem: wiele bibliotek Jetpack zawiera rozszerzenia, które zapewniają pełną obsługę współprogramów. Niektóre biblioteki udostępniają też własny zakres współbieżności, którego można używać na potrzeby uporządkowanej równoczesności.
Przegląd przykładów
Na podstawie przewodnika po architekturze aplikacji przykłady w tym temacie wysyłają żądanie sieciowe i zwracają wynik do wątku głównego, w którym aplikacja może wyświetlić wynik użytkownikowi.
W szczególności komponent Architektura ViewModel
wywołuje warstwę repozytorium w wątku głównym, aby aktywować żądanie sieciowe. W tym przewodniku objaśniamy różne rozwiązania, które wykorzystują współprace do utrzymywania odblokowanego wątku głównego.
ViewModel
zawiera zestaw rozszerzeń KTX, które współpracują bezpośrednio z korygentami. Te rozszerzenia to biblioteka lifecycle-viewmodel-ktx
, których używamy w tym przewodniku.
Informacje o zależności
Aby używać współprogramów w projekcie na Androida, dodaj tę zależność do pliku build.gradle
aplikacji:
Odlotowy
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
Wykonuję w wątku w tle
Żądanie sieciowe w wątku głównym powoduje oczekiwanie lub zablokowanie, aż otrzyma odpowiedź. Ponieważ wątek jest zablokowany, system operacyjny nie może wywołać onDraw()
. Może to spowodować zawieszenie aplikacji i wyświetlenie okna błędu ANR (Aplikacja nie odpowiada). Dla wygody użytkowników przeprowadźmy tę operację w wątku w tle.
Przyjrzyjmy się najpierw klasie Repository
, aby zobaczyć, jak wysyła żą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>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// 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 synchroniczny i blokuje wątek wywołujący. Do modelowania odpowiedzi na żądanie sieciowe mamy własną klasę Result
.
ViewModel
wywołuje żądanie sieciowe, gdy użytkownik kliknie na przykład 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 interfejsu użytkownika podczas wysyłania żądania sieciowego. Najprostszym rozwiązaniem, które pozwala przenieść wykonywanie z wątku głównego, jest utworzenie nowej współpracy 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)
}
}
}
Przeanalizujmy kod współprogramów w funkcji login
:
viewModelScope
to wstępnie zdefiniowanyCoroutineScope
, który jest dołączony do rozszerzeń KTXViewModel
. Pamiętaj, że wszystkie współprogramy muszą działać w zakresie.CoroutineScope
zarządza co najmniej 1 powiązaną współpracą.launch
to funkcja, która tworzy współpracę i wysyła wykonanie jej treści do odpowiedniego dyspozytora.Dispatchers.IO
wskazuje, że współprogram powinien zostać wykonany w wątku zarezerwowanym na potrzeby operacji wejścia-wyjścia.
Funkcja login
jest wykonywana w ten sposób:
- Aplikacja wywołuje funkcję
login
z warstwyView
w wątku głównym. launch
tworzy nową współpracę, a żądanie sieciowe jest wysyłane niezależnie od wątku zarezerwowanego na operacje wejścia-wyjścia.- Gdy współpraca jest uruchomiona, funkcja
login
kontynuuje wykonywanie i zwraca prawdopodobnie jeszcze przed zakończeniem żądania sieciowego. Dla uproszczenia odpowiedź sieci jest na razie ignorowana.
Ponieważ ta współpraca zaczyna się od viewModelScope
, jest wykonywana w zakresie ViewModel
. Jeśli ViewModel
zostanie zniszczony, ponieważ użytkownik opuści ekran, viewModelScope
zostanie automatycznie anulowane, a wszystkie uruchomione współprogramy również zostaną anulowane.
Problem w poprzednim przykładzie polega na tym, że wywołanie metody makeLoginRequest
musi pamiętać o jawnym przeniesieniu wykonania z wątku głównego. Sprawdźmy, jak możemy zmodyfikować Repository
, aby rozwiązać ten problem za nas.
Używanie współprogramów do zapewniania bezpieczeństwa
Funkcję uznajemy za główną, jeśli nie blokuje ona aktualizacji interfejsu w wątku głównym. Funkcja makeLoginRequest
nie jest bezpieczna w głównej mierze, ponieważ wywołanie funkcji makeLoginRequest
z wątku głównego blokuje interfejs użytkownika. Użyj funkcji withContext()
w bibliotece współprogramów, aby przenieść wykonanie współpracy 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 kodeksy do wątku wejścia-wyjścia, dzięki czemu funkcja wywołująca jest bezpieczna i umożliwia aktualizowanie interfejsu w razie potrzeby.
Oznaczenie makeLoginRequest
jest też oznaczone słowem kluczowym suspend
. To słowo kluczowe to sposób, w jaki Kotlin wymusi wywołanie funkcji z korutyny.
W poniższym przykładzie współpraca została utworzona w LoginViewModel
.
Gdy makeLoginRequest
przenosi wykonanie z wątku głównego, współpraca w funkcji login
może być teraz wykonywana 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
}
}
}
}
Zwróć uwagę, że nadal potrzebujesz współpracy, ponieważ makeLoginRequest
to funkcja suspend
, a wszystkie funkcje suspend
muszą być wykonywane w korektynie.
Ten kod różni się od poprzedniego przykładu login
pod kilkoma względami:
launch
nie przyjmuje parametruDispatchers.IO
. Jeśli nie przekażeszDispatcher
dolaunch
, wszystkie współprogramy uruchomione zviewModelScope
będą uruchamiane w wątku głównym.- Wynik żądania sieciowego jest teraz obsługiwany i wyświetla interfejs użytkownika o powodzeniu lub błędzie.
Funkcja logowania działa teraz w następujący sposób:
- Aplikacja wywołuje funkcję
login()
z warstwyView
w wątku głównym. launch
tworzy nową współpracę w wątku głównym i rozpoczyna wykonywanie.- W kopercie wywołanie
loginRepository.makeLoginRequest()
zawiesza dalsze jej wykonanie do momentu zakończenia działania blokuwithContext
wmakeLoginRequest()
. - Gdy blok
withContext
zakończy się, zgodnie z wynikiem żądania sieciowego współpraca wlogin()
zostanie wznowiona w wątku głównym.
Obsługa wyjątków
Aby obsługiwać wyjątki, które może zgłosić warstwa Repository
, użyj wbudowanej obsługi wyjątków Kotlin.
W tym przykładzie 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 obsługiwany w interfejsie jako błąd.
Dodatkowe zasoby współprogramów
Więcej informacji o współprogramach na Androidzie znajdziesz w artykule Zwiększanie wydajności aplikacji dzięki współprogramom Kotlin.
Więcej zasobów dotyczących współprogramów znajdziesz pod tymi linkami:
- Omówienie Kalendarza (JetBrains)
- Przewodnik po korektynie (JetBrains)
- Dodatkowe materiały dotyczące współprogramów i przepływu pracy Kotlin