Android & Kotlin Coroutines

Concurrency und Multithreading bereiten Android Entwicklern die meisten Probleme. Das ergab eine Befragung durch Google von Android Entwicklern weltweit. Bisherige Ansätze scheinen nicht zufriedenstellend. Googles LiveData sind nicht mächtig genug. Rx ist hingegen zu mächtig und schnell missbraucht. Coroutines erscheinen vielen noch unreif und zu wenig unterstützt, aber das passende Werkzeug für das Problem. In jüngsten Entwicklungen setzt Google in eigenen Librarys darauf auf. Was sind also Coroutines und wie benutzt man sie auf Android?

Scopes

Coroutines sind im Kern leichtgewichtige Threads, die unabhängig vom Main- oder UI-Thread laufen und dort langlaufende Operationen durchführen können.

Um eine Coroutine starten zu können, benötigt man einen CoroutineScope. Alle in einem Scope gestarteten Coroutines gehören zu diesem Scope – das gilt auch für weitere Coroutines, die innerhalb der Methoden ausgeführt werden. Der Scope kann die Coroutines abbrechen, sollten ihre Ergebnisse nicht mehr benötigt werden. Diese Funktionalität ist wichtig, da sonst langlaufende Coroutines versuchen könnten, Views zu aktualisieren, die nicht mehr vorhanden sind.

Beispiele für Scopes sind:

  • runBlocking – Alle hier gestarteten Coroutines blockieren den startenden Thread bis zum Ende der Ausführung.

  • GlobalScobe – Dieser Scope ist an den Lebenszyklus der Anwendung gekoppelt und endet, sobald die Anwendung beendet wird.

Google stellt in seinen Kotlin Extensions weitere praktische Scopes bereit:

  • viewModelScope – Dieser Scope ist an den Lebenszyklus des ViewModels gekoppelt und beendet die Coroutines, sobald das ViewModel onCleared aufruft.

  • lifeCycleScope – Dieser Scope ist an den LifeCycle der Activity gekoppelt und ermöglicht es, Coroutines beim Eintreten bestimmter LifeCycle Events auslösen.

  • liveDataScope – Coroutinen in diesem Scope werden ausgeführt, sobald das LiveData Objekt aktiv beobachtet wird und bricht ab, wenn es keine aktiven Beobachter mehr gibt.  

Suspending Functions

Suspending Functions halten den Scope der aktuellen Coroutine an, bis sie ein Ergebnis zurückliefern oder abgeschlossen sind. Sie ermöglichen einen leicht lesbaren Code, da sie asynchrone Callbacks verstecken und durch klassisch sequenzielle Funktionsaufrufe ersetzen.  Gekennzeichnet werden sie durch das Wort suspend, und sie können nur aus einem CoroutineScope aufgerufen werden.

Wichtige Schlüsselwörter sind hierbei:

  • launch – Hiermit wird eine Coroutine gestartet, die kein Ergebnis zurückliefern wird. Stattdessen erhält man ein Job Objekt, das abschließen oder abgebrochen werden kann.

  • async – Hiermit startet man eine Coroutine, die ein Ergebnis zurückliefert. Dieses ist ein Deferred Objekt gewrappt, im Endeffekt ein Job mit Ergebnis oder ein Future.

  • withContext – Hiermit startet man eine Coroutine auf einem bestimmten Dispatcher wie IO, Main oder Default, die ein Ergebnis zurückliefern wird. Dadurch kann man einfach zwischen verschiedenen Threads hin- und herwechseln.

Einige Android Frameworks wie Retrofit oder Room bieten bereits Möglichkeiten, über Suspending Functions asynchron Daten abzurufen oder zu speichern. WorkManager und andere AndroidX Bibliotheken werden folgen.

Beispiel

In einem folgenden Beispiel wird ein ViewModel erstellt, das Blogposts als LiveData bereitstellt, die die Activity beobachten kann. Die Posts werden zuerst aus dem Cache geladen, um die Ladezeiten zu verringern und anschließend vom Server geholt, um die lokalen Daten zu aktualisieren. Dabei wird mehrfach der Kontext und Dispatcher gewechselt, und die LiveData mehrfach aktualisiert.

class PostViewModel(
    private val database: PostDatabase,
    private val client: PostClient,
    private val businessService: PostsBusinessService
) : ViewModel() {

    val posts = liveData {
        val cachedPosts = database.getPosts()
        emit(cachedPosts)
        val remotePosts = client.getPosts()
        val filteredPosts = businessService.filterPosts(remotePosts)
        database.updatePosts(filteredPosts)
        emit(filteredPosts)
    }

}


class PostDatabase {

    suspend fun getPosts() = withContext(Dispatchers.IO){
        // Read from the database to get cached Posts
    }

    suspend fun updatePosts(posts: List<Post>) = withContext(Dispatchers.IO){
        // Update posts inside the database
    }
}

interface PostClient {

    // Retrieve posts from the server via Retrofit
    @GET("posts")
    suspend fun getPosts(): List<Post>

}

class PostsBusinessService {

    suspend fun filterPosts(posts: List<Post>) = withContext(Dispatchers.Default) {
        // Computing intensive business logic to filter posts to the user's preferences
    }

}

Fazit

Coroutines sind ein mächtiges Werkzeug und dieser Blogpost umfasst nur die rudimentären Funktionen, doch einfache Probleme lassen sich auch einfach lösen. Das Prinzip ist eingängig und der Code leicht lesbar. Die feingranulare Kontrolle über den Eventfluss, die RxJava bietet, bringen Coroutines nicht. Nutzt man Rx allerdings hauptsächlich, um Arbeit vom Main-Thread zu verlagern, sind Coroutines aktuell die beste Wahl.

Linksammlung