Kotlin na Androidzie

Współprogram to wzorzec projektowy współbieżności, którego możesz używać w Androidzie, aby uprościć kod wykonywany asynchronicznie. Współprogramy zostały dodane do Kotlina 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ć wątek główny i powodować, że aplikacja przestanie odpowiadać. Ponad 50% profesjonalnych deweloperów korzystających ze współprogramów zgłosiło wzrost produktywności. Z tego artykułu dowiesz się, jak używać współprogramów Kotlina do rozwiązywania tych problemów, co pozwoli Ci pisać czystszy i bardziej zwięzły kod aplikacji.

Funkcje

Współprogramy to zalecane przez nas rozwiązanie do programowania asynchronicznego w Androidzie. Do najważniejszych funkcji należą:

  • Lekkość: dzięki obsłudze zawieszania możesz uruchamiać wiele współprogramów w jednym wątku, co 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 współbieżnych operacji.
  • Mniej wycieków pamięci: używaj strukturalnej współbieżności aby uruchamiać operacje w zakresie.
  • Wbudowana obsługa anulowania: Anulowanie jest automatycznie propagowane w hierarchii uruchomionych współprogramów.
  • Integracja z Jetpack: 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ółprogramów, którego możesz używać do strukturalnej współbieżności.

Omówienie przykładów

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

W szczególności komponent architektury ViewModel wywołuje warstwę repozytorium w wątku głównym, aby uruchomić żądanie sieciowe. W tym przewodniku omawiamy różne rozwiązania, które używają współprogramów, aby wątek główny nie był zablokowany.

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

Informacje o zależnościach

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

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

Wysłanie żądania sieciowego w wątku głównym powoduje, że wątek ten czeka lub blokuje się, aż otrzyma odpowiedź. Ponieważ wątek jest zablokowany, 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 zapewnić lepsze wrażenia użytkownikom, uruchomimy 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 uruchamia żą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 interfejsu podczas wysyłania żądania sieciowego. Najprostszym rozwiązaniem, aby przenieść wykonanie 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 współprogramów w funkcji login:

  • viewModelScope to predefiniowany 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ązanym współprogramem.
  • launch to funkcja, która tworzy współprogram i wysyła wykonanie jego treści funkcji do odpowiedniego dyspozytora.
  • Dispatchers.IO wskazuje, ż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 wątku głównym.
  • launch tworzy nowy współprogram, 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ź sieciowa jest na razie ignorowana.

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

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

Używanie współprogramów do zapewnienia bezpieczeństwa w wątku głównym

Funkcję uważamy za bezpieczną w wątku głównym , gdy nie blokuje ona aktualizacji interfejsu w wątku głównym. Funkcja makeLoginRequest nie jest bezpieczna w wątku głównym, ponieważ wywołanie makeLoginRequest z wątku głównego blokuje interfejs. Aby przenieść wykonanie współprogramu do innego wątku, użyj funkcji withContext() z biblioteki współprogramów:

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 funkcja wywołująca jest bezpieczna w wątku głównym, a interfejs może się aktualizować w razie potrzeby.

makeLoginRequest jest też oznaczona słowem kluczowym suspend. To słowo kluczowe jest sposobem Kotlina na wymuszenie wywołania funkcji z poziomu współprogramu.

W tym przykładzie współprogram jest tworzony w LoginViewModel. Ponieważ makeLoginRequest przenosi wykonanie z wątku głównego, 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 współprogram jest tu nadal potrzebny, ponieważ makeLoginRequest jest funkcją suspend, a wszystkie funkcje suspend muszą być wykonywane w współprogramie.

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

  • 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 sieciowego jest teraz obsługiwany w celu wyświetlenia interfejsu informującego o powodzeniu lub niepowodzeniu.

Funkcja logowania jest teraz wykonywana w ten sposób:

  • Aplikacja wywołuje funkcję login() z warstwy View w wątku głównym.
  • launch tworzy nowy współprogram w wątku głównym, a współprogram rozpoczyna wykonywanie.
  • W ramach współprogramu wywołanie loginRepository.makeLoginRequest() teraz zawiesza dalsze wykonywanie współprogramu do czasu zakończenia działania bloku withContext w makeLoginRequest().
  • Po zakończeniu bloku withContext współprogram w login() wznawia wykonywanie w wątku głównym 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 Kotlinie . 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 jako błąd w interfejsie.

Dodatkowe materiały o współprogramach

Aby uzyskać bardziej szczegółowe informacje o współprogramach w Androidzie, przeczytaj artykuł Zwiększanie wydajności aplikacji za pomocą współprogramów Kotlina.

Więcej materiałów o współprogramach znajdziesz pod tymi linkami: