Kotlin na Androidzie

Korutyna to wzorzec współbieżności, którego możesz używać na Androidzie, aby uprościć kod wykonywany asynchronicznie. Korutyny zostały dodane do języka Kotlin 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ć główny wątek i sprawić, że aplikacja przestanie odpowiadać. Ponad 50% profesjonalnych deweloperów korzystających z korutyn zgłosiło wzrost produktywności. W tym artykule opisujemy, jak za pomocą korutyn w Kotlinie rozwiązać te problemy, aby pisać czystszy i bardziej zwięzły kod aplikacji.

Funkcje

Korutyny to zalecane przez nas rozwiązanie do programowania asynchronicznego na Androidzie. Warto zwrócić uwagę na te funkcje:

  • Lekkie: na jednym wątku możesz uruchomić wiele współprogramów dzięki obsłudze zawieszenia, które 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 równoczesnych operacji.
  • Mniej wycieków pamięci: używaj strukturalnego współbieżności do uruchamiania operacji w określonym zakresie.
  • Wbudowana obsługa anulowania:Anulowanie jest automatycznie propagowane w hierarchii działających współprogramów.
  • Integracja z Jetpackiem: wiele bibliotek Jetpacka zawiera rozszerzenia, które zapewniają pełną obsługę współprogramów. Niektóre biblioteki udostępniają też własny zakres współprogramu, którego możesz używać do strukturalnej równoczesności.

Przegląd przykładów

Zgodnie z przewodnikiem po architekturze aplikacji przykłady w tym temacie wysyłają żądanie sieciowe i zwracają wynik do głównego wątku, w którym aplikacja może następnie wyświetlić go użytkownikowi.

W szczególności komponent ViewModel architektury wywołuje warstwę repozytorium w głównym wątku, aby wywołać żądanie sieciowe. W tym przewodniku znajdziesz różne rozwiązania, które wykorzystują współprogramy do odblokowywania wątku głównego.

ViewModel zawiera zestaw rozszerzeń KTX, które działają bezpośrednio z korutynami. Te rozszerzenia to lifecycle-viewmodel-ktxbiblioteka, która jest używana w tym przewodniku.

Informacje o zależności

Aby używać współprogramów w projekcie aplikacji na Androida, dodaj do pliku build.gradle aplikacji tę zależność:

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

Wysyłanie żądania sieciowego w głównym wątku powoduje jego oczekiwanie, czyli blokowanie, aż do otrzymania odpowiedzi. Wątek jest zablokowany, więc 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 zwiększyć komfort użytkowników, wykonajmy 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 wywołuje żą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 UI podczas wysyłania żądania sieciowego. Najprostszym rozwiązaniem, które pozwala przenieść wykonywanie operacji 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 korutyn w funkcji login:

  • viewModelScope to wstępnie zdefiniowany CoroutineScope, który jest dołączony do ViewModel rozszerzeń KTX. Pamiętaj, że wszystkie korutyny muszą działać w zakresie. CoroutineScope zarządza co najmniej 1 powiązaną korutyną.
  • launch to funkcja, która tworzy współprogram i przekazuje wykonanie jej treści do odpowiedniego dyspozytora.
  • Dispatchers.IO oznacza, ż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ę login z warstwy View w głównym wątku.
  • launch tworzy nową współprogramową, a żądanie sieciowe jest wysyłane niezależnie w wątku zarezerwowanym dla operacji wejścia/wyjścia.
  • Podczas działania współprogramu funkcja login kontynuuje wykonywanie i zwraca wartość, być może zanim żądanie sieciowe zostanie zakończone. Pamiętaj, że dla uproszczenia odpowiedź sieci jest na razie ignorowana.

Ponieważ ten współprogram jest uruchamiany za pomocą viewModelScope, jest wykonywany w zakresie ViewModel. Jeśli ViewModel zostanie zniszczony, ponieważ użytkownik opuści ekran, viewModelScope zostanie automatycznie anulowany, a wszystkie uruchomione współprogramy również zostaną anulowane.

Jednym z problemów z poprzednim przykładem jest to, że wszystko, co wywołuje funkcję makeLoginRequest, musi pamiętać o wyraźnym przeniesieniu wykonania z głównego wątku. Zobaczmy, jak możemy zmodyfikować Repository, aby rozwiązać ten problem.

Używanie korutyn w celu zapewnienia bezpieczeństwa wątku głównego

Funkcję uznajemy za bezpieczną dla wątku głównego, jeśli nie blokuje aktualizacji interfejsu w wątku głównym. Funkcja makeLoginRequest nie jest bezpieczna dla wątku głównego, ponieważ wywołanie jej makeLoginRequest z wątku głównego blokuje interfejs. Użyj funkcji withContext() z biblioteki współprogramów, aby przenieść wykonanie współprogramu 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 współprogramu do wątku wejścia-wyjścia, dzięki czemu nasza funkcja wywołująca jest bezpieczna dla wątku głównego i umożliwia aktualizowanie interfejsu w razie potrzeby.

makeLoginRequest jest też oznaczony słowem kluczowym suspend. Ten słowo kluczowe to sposób, w jaki Kotlin wymusza wywoływanie funkcji z poziomu korutyny.

W tym przykładzie współprogram jest tworzony w LoginViewModel. Gdy makeLoginRequest przenosi wykonanie poza wątek główny, 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 w tym przypadku nadal potrzebna jest korutyna, ponieważ makeLoginRequest jest funkcją suspend, a wszystkie funkcje suspend muszą być wykonywane w korutynie.

Ten kod różni się od poprzedniego przykładu login na kilka sposobów:

  • Funkcja launch nie przyjmuje parametru Dispatchers.IO. Jeśli nie przekażesz Dispatcher do launch, wszystkie współprogramy uruchomione z viewModelScope będą działać w wątku głównym.
  • Wynik żądania sieci jest teraz obsługiwany w celu wyświetlenia interfejsu informującego o sukcesie lub niepowodzeniu.

Funkcja logowania jest teraz wykonywana w ten sposób:

  • Aplikacja wywołuje funkcję login() z warstwy View w głównym wątku.
  • launch tworzy nowy współprogram w głównym wątku i rozpoczyna jego wykonywanie.
  • W współprogramie wywołanie funkcji loginRepository.makeLoginRequest() now zawiesza dalsze wykonanie współprogramu do momentu, aż withContext blok w makeLoginRequest() zakończy działanie.
  • Gdy blok withContext zakończy działanie, współprogram w login() wznawia wykonywanie w głównym wątku 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 języku Kotlin. W przykładzie poniżej 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 traktowany jako błąd w interfejsie.

Dodatkowe materiały o korutynach

Więcej informacji o współprogramach na Androidzie znajdziesz w artykule Zwiększanie wydajności aplikacji za pomocą współprogramów Kotlin.

Więcej informacji o korutynach znajdziesz w tych artykułach: