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:

  • viewModelScope jest predefiniowaną CoroutineScope, która jest dołączona do rozszerzeń ViewModel KTX. Zauważ, że wszystkie coroutines muszą działać w ascope. A CoroutineScope zarządza jedną lub więcej powiązanych korutyn.
  • launch jest funkcją, która tworzy korutynę i przekazuje wykonanie jej ciała funkcji do odpowiedniego dyspozytora.
  • Dispatchers.IO wskazuje, ż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ę login z warstwy View w głównym wątku.
  • launch tworzy nową korutynę, a żądanie sieciowe jest wykonywane niezależnie w wątku zarezerwowanym dla operacji wejścia/wyjścia.
  • Podczas działania korutyny funkcja login kontynuuje 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:

  • launch nie przyjmuje Dispatchers.IO parametru. Gdy nie przekazujesz Dispatcher do launch, wszelkie coroutiny uruchamiane zviewModelScope dział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 warstwy View w głównym wątku.
  • launch tworzy 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 bloku withContext w makeLoginRequest().
  • Po zakończeniu działania bloku withContext, coroutine w login() 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