Współprogram to wzorzec projektowy współbieżności, którego możesz używać w Androidzie, aby uprościć kod wykonywany asynchronicznie. Współprogramy zostały dodane do Kotlina 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ć wątek główny i powodować, że aplikacja przestanie odpowiadać. Ponad 50% profesjonalnych deweloperów korzystających ze współprogramów zgłosiło wzrost produktywności. Z tego artykułu dowiesz się, jak używać współprogramów Kotlina do rozwiązywania tych problemów, co pozwoli Ci pisać czystszy i bardziej zwięzły kod aplikacji.
Funkcje
Współprogramy to zalecane przez nas rozwiązanie do programowania asynchronicznego w Androidzie. Do najważniejszych funkcji należą:
- Lekkość: dzięki obsłudze zawieszania możesz uruchamiać wiele współprogramów w jednym wątku, co 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 współbieżnych operacji.
- Mniej wycieków pamięci: używaj strukturalnej współbieżności aby uruchamiać operacje w zakresie.
- Wbudowana obsługa anulowania: Anulowanie jest automatycznie propagowane w hierarchii uruchomionych współprogramów.
- Integracja z Jetpack: 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ółprogramów, którego możesz używać do strukturalnej współbieżności.
Omówienie przykładów
Na podstawie przewodnika po architekturze aplikacji przykłady w tym artykule wysyłają żądanie sieciowe i zwracają wynik do wątku głównego, w którym aplikacja może wyświetlić go użytkownikowi.
W szczególności komponent architektury ViewModel
wywołuje warstwę repozytorium w wątku głównym, aby
uruchomić żądanie sieciowe. W tym przewodniku omawiamy różne rozwiązania, które używają współprogramów, aby wątek główny nie był zablokowany.
ViewModel zawiera zestaw rozszerzeń KTX, które działają bezpośrednio ze współprogramami. Te rozszerzenia to
lifecycle-viewmodel-ktx biblioteka, która jest używana
w tym przewodniku.
Informacje o zależnościach
Aby używać współprogramów w projekcie na Androida, dodaj tę zależność do pliku build.gradle aplikacji:
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
Wysłanie żądania sieciowego w wątku głównym powoduje, że wątek ten czeka lub blokuje się,
aż otrzyma odpowiedź. Ponieważ wątek jest zablokowany, 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 zapewnić lepsze wrażenia użytkownikom, uruchomimy 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 uruchamia żą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 interfejsu podczas wysyłania żądania sieciowego. Najprostszym rozwiązaniem, aby przenieść wykonanie 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 współprogramów w funkcji login:
viewModelScopeto predefiniowanyCoroutineScope, który jest dołączony do rozszerzeń KTXViewModel. Pamiętaj, że wszystkie współprogramy muszą działać w zakresie.CoroutineScopezarządza co najmniej 1 powiązanym współprogramem.launchto funkcja, która tworzy współprogram i wysyła wykonanie jego treści funkcji do odpowiedniego dyspozytora.Dispatchers.IOwskazuje, ż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 wątku głównym. launchtworzy nowy współprogram, 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ź sieciowa jest na razie ignorowana.
Ponieważ ten współprogram jest uruchamiany za pomocą viewModelScope, jest on wykonywany w zakresie ViewModel. Jeśli ViewModel zostanie zniszczony, ponieważ użytkownik opuszcza ekran, viewModelScope zostanie automatycznie anulowany, a wszystkie działające współprogramy również zostaną anulowane.
Jednym z problemów z poprzednim przykładem jest to, że wszystko, co wywołuje makeLoginRequest, musi pamiętać o wyraźnym przeniesieniu wykonania z wątku głównego. Zobaczmy, jak możemy zmodyfikować Repository, aby rozwiązać ten problem.
Używanie współprogramów do zapewnienia bezpieczeństwa w wątku głównym
Funkcję uważamy za bezpieczną w wątku głównym , gdy nie blokuje ona aktualizacji interfejsu w wątku głównym. Funkcja makeLoginRequest nie jest bezpieczna w wątku głównym, ponieważ wywołanie makeLoginRequest z wątku głównego blokuje interfejs. Aby przenieść wykonanie współprogramu do innego wątku, użyj funkcji withContext() z biblioteki współprogramów:
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 funkcja wywołująca jest bezpieczna w wątku głównym, a interfejs może się aktualizować w razie potrzeby.
makeLoginRequest jest też oznaczona słowem kluczowym suspend. To słowo kluczowe jest sposobem Kotlina na wymuszenie wywołania funkcji z poziomu współprogramu.
W tym przykładzie współprogram jest tworzony w LoginViewModel.
Ponieważ makeLoginRequest przenosi wykonanie z wątku głównego, 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 współprogram jest tu nadal potrzebny, ponieważ makeLoginRequest jest funkcją suspend, a wszystkie funkcje suspend muszą być wykonywane w współprogramie.
Ten kod różni się od poprzedniego przykładu login na kilka sposobów:
launchnie przyjmuje parametruDispatchers.IO. Jeśli nie przekażeszDispatcherdolaunch, wszystkie współprogramy uruchomione zviewModelScopebędą działać w wątku głównym.- Wynik żądania sieciowego jest teraz obsługiwany w celu wyświetlenia interfejsu informującego o powodzeniu lub niepowodzeniu.
Funkcja logowania jest teraz wykonywana w ten sposób:
- Aplikacja wywołuje funkcję
login()z warstwyVieww wątku głównym. launchtworzy nowy współprogram w wątku głównym, a współprogram rozpoczyna wykonywanie.- W ramach współprogramu wywołanie
loginRepository.makeLoginRequest()teraz zawiesza dalsze wykonywanie współprogramu do czasu zakończenia działania blokuwithContextwmakeLoginRequest(). - Po zakończeniu bloku
withContextwspółprogram wlogin()wznawia wykonywanie w wątku głównym 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 Kotlinie
.
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 jako błąd w interfejsie.
Dodatkowe materiały o współprogramach
Aby uzyskać bardziej szczegółowe informacje o współprogramach w Androidzie, przeczytaj artykuł Zwiększanie wydajności aplikacji za pomocą współprogramów Kotlina.
Więcej materiałów o współprogramach znajdziesz pod tymi linkami:
- Omówienie współprogramów (JetBrains)
- Przewodnik po współprogramach (JetBrains)
- Dodatkowe materiały o współprogramach i przepływie w Kotlinie