Kotlin coroutines na Androida
Korutyna jest wzorcem projektowym współbieżności, którego można użyć na Androidzie, aby uprościć kod, który wykonuje się asynchronicznie.Korutyny zostały dodane do Kotlina w wersji 1.3 i są oparte na ustalonych koncepcjach z innych języków.
Na Androidzie, korutyny pomagają zarządzać długo trwającymi zadaniami, które w przeciwnym razie mogłyby zablokować główny wątek i spowodować, że aplikacja przestanie reagować.Ponad 50% profesjonalnych programistów, którzy używają coroutines, zgłosiło zwiększenie produktywności.Ten temat opisuje, jak można użyć Kotlin coroutines do rozwiązania tych problemów, umożliwiając pisanie czystszego i bardziej zwięzłego kodu aplikacji.
Features
Coroutines jest naszym zalecanym rozwiązaniem do programowania asynchronicznego na Androida. Godne uwagi cechy obejmują następujące elementy:
- Lekkość: Możesz uruchomić wiele coroutines na jednym wątku dzięki wsparciu dla zawieszania, które nie blokuje wątku, w którym działa coroutine. Zawieszanie oszczędza pamięć w stosunku do blokowania, jednocześnie obsługując wiele współbieżnych operacji.
- Mniej wycieków pamięci: Usestructured concurrency do uruchamiania operacji w obrębie zakresu.
- Wbudowana obsługa anulowania:Anulowanie jest propagowane automatycznie przez hierarchię uruchomionych coroutine.
- Integracja z Jetpack: Wiele bibliotek Jetpack zawiera rozszerzenia, które zapewniają pełną obsługę coroutines. Niektóre biblioteki dostarczają również własne zakresy korutyn, które można wykorzystać do strukturalnej współbieżności.
Przegląd przykładów
W oparciu o Przewodnik po architekturze aplikacji, przykłady w tym temacie wykonują żądanie sieciowe i zwracają wynik do głównego wątku, gdzie aplikacja może następnie wyświetlić wynik użytkownikowi.
Szczegółowo, komponent ViewModelArchitektura wywołuje warstwę repozytorium w głównym wątku, aby wyzwolić żądanie sieciowe. W tym przewodniku prześledzimy różne rozwiązania, które wykorzystując coroutines utrzymują główny wątek odblokowany.
ViewModelZawiera zestaw rozszerzeń KTX, które pracują bezpośrednio z coroutines. Te rozszerzenia sąlifecycle-viewmodel-ktx biblioteką i są używane w tym przewodniku.
Informacja o zależnościach
Aby użyć coroutines w projekcie Androida, dodaj następującą zależność do pliku aplikacji:
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'}
Wykonywanie w wątku tła
Wykonanie żądania sieciowego w głównym wątku powoduje, że wątek ten czeka lub blokuje się, dopóki nie otrzyma odpowiedzi. Ponieważ wątek jest zablokowany, system operacyjny nie jest w stanie wywołać onDraw(), co powoduje zawieszenie aplikacji i potencjalnie prowadzi do okna dialogowego Application Not Responding (ANR). Dla lepszego doświadczenia uruchommy tę operację w wątku tła.
Pierw przyjrzyjmy się naszej klasie Repository i zobaczmy, w jaki sposób wykonuje 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>()}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 synchroniczna i blokuje wątek wywołujący. Aby zamodelować odpowiedź na żądanie sieciowe, mamy własną klasę Result.
Klasa ViewModel wywołuje żądanie sieciowe, gdy użytkownik kliknie, na przykład, na 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 wykonywania żądania sieciowego. Najprostszym rozwiązaniem jest utworzenie nowej korutyny i wykonanie żądania sieciowego w wątku I/O:
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 korutyny w funkcji login:
-
viewModelScopejest predefiniowanąCoroutineScope, która jest dołączona do rozszerzeńViewModelKTX. Zauważ, że wszystkie coroutines muszą działać w ascope. ACoroutineScopezarządza jedną lub więcej powiązanych korutyn. -
launchjest funkcją, która tworzy korutynę i przekazuje wykonanie jej ciała funkcji do odpowiedniego dyspozytora. -
Dispatchers.IOwskazuje, że ta korutyna powinna być wykonywana w wątku zarezerwowanym dla operacji wejścia/wyjścia.
Funkcja login jest wykonywana w następujący sposób:
- Aplikacja wywołuje funkcję
loginz warstwyVieww głównym wątku. -
launchtworzy nową korutynę, a żądanie sieciowe jest wykonywane niezależnie w wątku zarezerwowanym dla operacji wejścia/wyjścia. - Podczas działania korutyny funkcja
loginkontynuuje wykonywanie i powraca, być może przed zakończeniem żądania sieciowego. Zauważ, że dla uproszczenia odpowiedź sieciowa jest na razie ignorowana.
Ponieważ ta korutyna jest uruchamiana za pomocą viewModelScope, jest ona wykonywana w zakresie ViewModel. Jeśli ViewModel zostanie zniszczone, ponieważ użytkownik odchodzi od ekranu, viewModelScope jest automatycznie anulowane, a wszystkie działające procedury są również anulowane.
Jednym z problemów poprzedniego przykładu jest to, że wszystko, co wywołujemakeLoginRequest musi pamiętać o wyraźnym przeniesieniu wykonania poza główny wątek. Zobaczmy, jak możemy zmodyfikować Repository, aby rozwiązać ten problem za nas.
Use coroutines for main-safety
Uważamy, że funkcja jest main-safe, gdy nie blokuje aktualizacji UI w głównym wątku. Funkcja makeLoginRequest nie jest main-safe, ponieważ wywołaniemakeLoginRequest z głównego wątku blokuje UI. Użyj funkcjiwithContext() z biblioteki coroutines, aby przenieść wykonanie coroutine 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 coroutine do wątku I/O, czyniąc naszą funkcję wywołującą main-safe i umożliwiając UI toupdate w razie potrzeby.
makeLoginRequest jest również oznaczone słowem kluczowym suspend. To słowo kluczowe jest sposobem Kotlina na wymuszenie, aby funkcja była wywoływana z wnętrza coroutine.
W poniższym przykładzie coroutine jest tworzony w LoginViewModel.Ponieważ makeLoginRequest przenosi wykonanie poza główny wątek, coroutine w funkcji login może być teraz wykonywany w głównym wątku:
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 } } }}
Zauważ, że coroutine jest nadal potrzebny tutaj, ponieważ makeLoginRequest jest funkcją suspend, a wszystkie funkcje suspend muszą być wykonywane w coroutine.
Ten kod różni się od poprzedniego login przykładu na kilka sposobów:
-
launchnie przyjmujeDispatchers.IOparametru. Gdy nie przekazujeszDispatcherdolaunch, wszelkie coroutiny uruchamiane zviewModelScopedziałają w głównym wątku. - Wynik żądania sieciowego jest teraz obsługiwany w celu wyświetlenia interfejsu UI awarii następcy.
Funkcja logowania wykonuje się teraz w następujący sposób:
- Aplikacja wywołuje funkcję
login()z warstwyVieww głównym wątku. -
launchtworzy nową korutynę do wykonania żądania sieciowego w głównym wątku, a korutyna rozpoczyna wykonywanie. - Wewnątrz coroutine, wywołanie
loginRepository.makeLoginRequest()now zawiesza dalsze wykonywanie coroutine do czasu zakończenia działania blokuwithContextwmakeLoginRequest(). - Po zakończeniu działania bloku
withContext, coroutine wlogin()wznawia wykonywanie na głównym wątku z wynikiem żądania sieciowego.
Obsługa wyjątków
Aby obsłużyć wyjątki, które może rzucić warstwa Repository, należy skorzystać z wbudowanej w Kotlin obsługi wyjątków.W poniższym przykładzie używamy bloku try-catch:
class LoginViewModel( private val loginRepository: LoginRepository): ViewModel() { fun makeLoginRequest(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 rzucony przez makeLoginRequest()wywołanie jest obsługiwany jako błąd w UI.
.
Leave a Reply