Kotlin na Androidzie

Korutyna to wzorzec projektu równoczesności, którego można używać na Androidzie do upraszczania kodu uruchamianego asynchronicznie. Korutyny zostały dodane do Kotlin w wersji 1.3 i oparte na koncepcjach znanych z innych języków.

Na Androidzie współprogramy pomagają zarządzać długotrwałymi zadaniami, które mogą zablokować wątek główny i sprawić, że aplikacja przestanie odpowiadać. Ponad 50% profesjonalnych programistów, którzy korzystają z korekty, zauważyło wzrost produktywności. W tym temacie opisujemy, jak za pomocą współprogramów Kotlin rozwiązać te problemy, co pozwoli na pisanie bardziej przejrzystego i zwięzłego kodu aplikacji.

Funkcje

Coroutines to zalecane rozwiązanie do programowania asynchronicznego na Androidzie. Do najważniejszych funkcji należą:

  • Lekka: w jednym wątku możesz uruchamiać wiele współprogramów dzięki obsłudze zawieszenia, które nie blokuje wątku, w którym działa współpraca. Zawieszenie pozwala zaoszczędzić pamięć, a jednocześnie obsługiwać wiele równoczesnych operacji.
  • Mniej wycieków pamięci: używaj uporządkowanej równoczesności do uruchamiania operacji w zakresie.
  • Wbudowana obsługa anulowania: informacje o anulowaniu są rozpowszechniane automatycznie w ramach hierarchii bieżącej.
  • Integracja z Jetpackiem: 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ółbieżności, którego można używać na potrzeby uporządkowanej równoczesności.

Przegląd przykładów

Na podstawie przewodnika po architekturze aplikacji przykłady w tym temacie wysyłają żądanie sieciowe i zwracają wynik do wątku głównego, w którym aplikacja może wyświetlić wynik użytkownikowi.

W szczególności komponent Architektura ViewModel wywołuje warstwę repozytorium w wątku głównym, aby aktywować żądanie sieciowe. W tym przewodniku powtarzamy różne rozwiązania, które wykorzystują współprace, aby utrzymywać wątek główny w odblokowaniu.

ViewModel zawiera zestaw rozszerzeń KTX, które współpracują bezpośrednio z korygentami. Te rozszerzenia to biblioteka lifecycle-viewmodel-ktx, których używamy w tym przewodniku.

Informacje o zależności

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

Odlotowy

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Wykonuję w wątku w tle

Żądanie sieciowe w wątku głównym powoduje oczekiwanie lub zablokowanie, aż otrzyma odpowiedź. Ponieważ wątek jest zablokowany, system operacyjny nie może wywołać onDraw(). Może to spowodować zawieszenie aplikacji i wyświetlenie okna błędu ANR (Aplikacja nie odpowiada). Dla wygody użytkowników przeprowadźmy tę operację w wątku w tle.

Przyjrzyjmy się najpierw klasie Repository, aby zobaczyć, jak wysyła żą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 synchroniczny i blokuje wątek wywołujący. Do modelowania odpowiedzi na żądanie sieciowe mamy własną klasę Result.

ViewModel wywołuje żądanie sieciowe, gdy użytkownik kliknie na przykład 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 użytkownika podczas wysyłania żądania sieciowego. Najprostszym rozwiązaniem, które pozwala przenieść wykonywanie z wątku głównego, jest utworzenie nowej współpracy 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)
        }
    }
}

Przeanalizujmy kod współprogramów w funkcji login:

  • viewModelScope to wstępnie zdefiniowany CoroutineScope, który jest dołączony do rozszerzeń KTX ViewModel. Pamiętaj, że wszystkie współprogramy muszą działać w zakresie. CoroutineScope zarządza co najmniej 1 powiązaną współpracą.
  • launch to funkcja, która tworzy współpracę i wysyła wykonanie jej treści do odpowiedniego dyspozytora.
  • Dispatchers.IO wskazuje, że współprogram powinien zostać wykonany w wątku zarezerwowanym na potrzeby operacji wejścia-wyjścia.

Funkcja login jest wykonywana w ten sposób:

  • Aplikacja wywołuje funkcję login z warstwy View w wątku głównym.
  • launch tworzy nową współpracę, a żądanie sieciowe jest wysyłane niezależnie od wątku zarezerwowanego na operacje wejścia-wyjścia.
  • Gdy współpraca jest uruchomiona, funkcja login kontynuuje wykonywanie i zwraca prawdopodobnie jeszcze przed zakończeniem żądania sieciowego. Dla uproszczenia odpowiedź sieci jest na razie ignorowana.

Ponieważ ta współpraca zaczyna się od viewModelScope, jest wykonywana w zakresie ViewModel. Jeśli ViewModel zostanie zniszczony, ponieważ użytkownik opuści ekran, viewModelScope zostanie automatycznie anulowane, a wszystkie uruchomione współprogramy również zostaną anulowane.

Problem w poprzednim przykładzie polega na tym, że wywołanie metody makeLoginRequest musi pamiętać o jawnym przeniesieniu wykonania z wątku głównego. Sprawdźmy, jak możemy zmodyfikować Repository, aby rozwiązać ten problem za nas.

Używanie współprogramów do zapewniania bezpieczeństwa

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

Oznaczenie makeLoginRequest jest też oznaczone słowem kluczowym suspend. To słowo kluczowe to sposób, w jaki Kotlin wymusi wywołanie funkcji z korutyny.

W poniższym przykładzie współpraca została utworzona w LoginViewModel. Gdy makeLoginRequest przenosi wykonanie z wątku głównego, współpraca w funkcji login może być teraz wykonywana 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
            }
        }
    }
}

Zwróć uwagę, że nadal potrzebujesz współpracy, ponieważ makeLoginRequest to funkcja suspend, a wszystkie funkcje suspend muszą być wykonywane w korektynie.

Ten kod różni się od poprzedniego przykładu login pod kilkoma względami:

  • launch nie przyjmuje parametru Dispatchers.IO. Jeśli nie przekażesz Dispatcher do launch, wszystkie współprogramy uruchomione z viewModelScope będą uruchamiane w wątku głównym.
  • Wynik żądania sieciowego jest teraz obsługiwany i wyświetla interfejs użytkownika o powodzeniu lub błędzie.

Funkcja logowania działa teraz w następujący sposób:

  • Aplikacja wywołuje funkcję login() z warstwy View w wątku głównym.
  • launch tworzy nową współpracę w wątku głównym i rozpoczyna wykonywanie.
  • W kopercie wywołanie loginRepository.makeLoginRequest() zawiesza dalsze jej wykonanie do momentu zakończenia działania bloku withContext w makeLoginRequest().
  • Gdy blok withContext zakończy się, zgodnie z wynikiem żądania sieciowego współpraca w login() zostanie wznowiona w wątku głównym.

Obsługa wyjątków

Aby obsługiwać wyjątki, które może zgłosić warstwa Repository, użyj wbudowanej obsługi wyjątków Kotlin. 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 w interfejsie jako błąd.

Dodatkowe zasoby współprogramów

Więcej informacji o współprogramach na Androidzie znajdziesz w artykule Zwiększanie wydajności aplikacji dzięki współprogramom Kotlin.

Więcej zasobów dotyczących współprogramów znajdziesz pod tymi linkami: