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 ViewModel
Architektura 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.
ViewModel
Zawiera 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. ACoroutineScope
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 warstwyView
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 przyjmujeDispatchers.IO
parametru. Gdy nie przekazujeszDispatcher
dolaunch
, 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 warstwyView
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 blokuwithContext
wmakeLoginRequest()
. - 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