Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
Vorwort
Teil I Grundlagen
1 Android – eine offene, mobile Plattform
2 Hallo Android!
3 Von der Idee zur Veröffentlichung
Teil II Elementare Anwendungsbausteine
4 Wichtige Grundbausteine von Apps
5 Benutzeroberflächen
6 Multitasking
Teil III Gerätefunktionen nutzen
7 Telefonieren und surfen
8 Sensoren, GPS und Bluetooth
Teil IV Dateien und Datenbanken
9 Dateien lesen, schreiben und drucken
10 Datenbanken
Teil V Multimedia und Produktivität
11 Multimedia
12 Kontakte und Organizer
A Einführung in Kotlin
B Jetpack Compose
C Häufig benötigte Codebausteine
D Literaturverzeichnis
E Die Begleitmaterialien
Stichwortverzeichnis

Ihre Meinung?
Spacer
<< zurück
Android 11 von Thomas Künneth
Das Praxisbuch für App-Entwickler
Buch: Android 11

Android 11
Pfeil 6 Multitasking
Pfeil 6.1 Leichtgewichtige Nebenläufigkeit
Pfeil 6.1.1 Java-Erbe
Pfeil 6.1.2 Der Main- oder UI-Thread
Pfeil 6.1.3 Koroutinen
Pfeil 6.2 Services
Pfeil 6.2.1 Gestartete Services
Pfeil 6.2.2 Gebundene Services
Pfeil 6.3 Regelmäßige Arbeiten
Pfeil 6.3.1 JobScheduler
Pfeil 6.3.2 WorkManager
Pfeil 6.4 Mehrere Apps gleichzeitig nutzen
Pfeil 6.4.1 Zwei-App-Darstellung
Pfeil 6.4.2 Beliebig positionierbare Fenster
Pfeil 6.5 Zusammenfassung
 
Zum Seitenanfang

6    Multitasking Zur vorigen ÜberschriftZur nächsten Überschrift

Smartphones und Tablets sind Multitalente. Während Sie im Internet surfen, können Sie nebenbei Musik hören oder sich Videoclips ansehen. In diesem Kapitel zeige ich Ihnen, wie Sie Ihre Apps fit fürs Multitasking machen.

Die Zeiten, in denen ein Computer mehrere Programme nur nacheinander ausführen konnte, sind zum Glück schon sehr lange vorbei; moderne Desktop-Systeme sind multitaskingfähig und können also mehrere Anwendungen gleichzeitig ausführen. Wenn eine Maschine nur einen Mikroprozessor oder Kern enthält, ist das natürlich eigentlich gar nicht möglich, denn auch der Chip kann ja normalerweise nur ein Maschinenprogramm ausführen. Betriebssysteme greifen deshalb zu einem Trick: Sie führen ein Programm eine gewisse Zeit lang aus und ziehen dann die Kontrolle wieder an sich. Nun kommt ein anderes Programm an die Reihe, und dieses Spiel wiederholt sich ständig. Auch wenn also nur stets ein Programm ausgeführt wird, entsteht für den Nutzer der Eindruck einer parallelen Abarbeitung.

Auch von Betriebssystemen für Smartphones erwartet man, dass sie Multitasking unterstützen. Aber warum eigentlich? Ihr kleiner Bildschirm macht die gleichzeitige Nutzung von mehreren Anwendungen auf geteilter Benutzeroberfläche unpraktisch, und ein eingehender Anruf unterbricht ohnehin die aktuelle Tätigkeit. Sinnvolle Einsatzgebiete für eine parallele Abarbeitung von Aufgaben gibt es dennoch freilich viele, zum Beispiel das Abspielen von Audiotracks oder das Herunterladen von Dateien.

Android ist multitaskingfähig. Das Fundament hierfür bildet der eingesetzte Betriebssystemkern. Linux bietet sogenanntes präemptives Multitasking. Das bedeutet, dass Prozesse, die zu viel Rechenzeit beanspruchen, nicht das ganze System ausbremsen können, da nach einer gewissen Zeit der Kern die Kontrolle wieder an sich zieht. Wie Sie bereits wissen, wird jede Android-App als eigener Linux-Prozess ausgeführt. Ein Fehler in einer App kann die Funktion des Smartphones oder Tablets also (theoretisch) nicht beeinträchtigen.

 
Zum Seitenanfang

6.1    Leichtgewichtige Nebenläufigkeit Zur vorigen ÜberschriftZur nächsten Überschrift

Auch innerhalb eines Programms kann die quasiparallele (ab hier parallele) Ausführung von Aufgaben sehr nützlich sein, zum Beispiel bei Spielen. Das Bewegen einer Figur ist von Eingaben des Benutzers abhängig, Gegner oder bewegliche Hindernisse müssen aber »von allein« ihre Position ändern können. Ein anderes Beispiel: Nehmen Sie an, das Anklicken einer Schaltfläche löst eine komplizierte Berechnung aus, die mehrere Minuten in Anspruch nimmt. Natürlich erwartet der Benutzer, dass er das Programm währenddessen weiter bedienen oder zumindest die aktuelle Tätigkeit unterbrechen kann.

 
Zum Seitenanfang

6.1.1    Java-Erbe Zur vorigen ÜberschriftZur nächsten Überschrift

Auch wenn Apps mittlerweile in Kotlin entwickelt werden, ist es wichtig, sich bewusst zu machen, dass vieles in Android noch fest mit Java verbunden ist. Dazu gehört der Umgang mit Nebenläufigkeit. Java (und damit Android) kennt mit sogenannten Threads (dt. Fäden) ein Instrument zur Realisierung von leichtgewichtigen Prozessen. Mit solchen Fäden kann ein Programm mehrere Tätigkeiten gleichzeitig abarbeiten. Die Klasse java.lang.Thread sowie das Interface java.lang.Runnable bilden die Grundlage für die Nutzung von Threads. Das Beispielprojekt ThreadDemo1 zeigt, wie Threads erzeugt und gestartet werden. Die erste Version der Klasse ThreadDemo1Activity (siehe Listing 6.1) zeige ich Ihnen gleich. Wir werden sie im weiteren Verlauf um ein paar Methoden erweitern. Die Begleitmaterialien enthalten die vollständige Fassung.

Alle Anweisungen, die in einem eigenen Thread abgearbeitet werden sollen, packen Sie in die Methode run() einer Klasse, die das Interface Runnable implementiert. Als Lambda-Ausdruck sieht dies folgendermaßen aus:

val r = Runnable { Log.d(TAG, "run()-Methode wurde aufgerufen") }

Mit diesem Code wird eine Debug-Nachricht ausgegeben. Die Klasse bzw. der Lambda-Ausdruck ist übrigens nicht der Thread. Der Thread entsteht durch den Ausdruck val t = Thread(r). Mit t.start() beginnt seine Ausführung, und sie endet, wenn alle Anweisungen innerhalb des run()-Methodenrumpfes abgearbeitet wurden. Sie können den Status eines Threads mit t.isAlive abfragen. Natürlich wird man für einige wenige Anweisungen, die zudem schnell abgearbeitet werden können (wie beispielsweise eine Ausgabe in Logcat), keinen eigenen Thread starten. Dagegen ist der Einsatz eines Threads bei länger andauernden Berechnungen sinnvoll.

package com.thomaskuenneth.androidbuch.threaddemo1

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

private val TAG = ThreadDemo1Activity::class.simpleName
class ThreadDemo1Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val r = Runnable { Log.d(TAG,
"run()-Methode wurde aufgerufen") }
val t = Thread(r)
t.start()
Log.d(TAG, "Thread wurde gestartet")
Log.d(TAG, "t.isAlive(): ${t.isAlive}")
}
}

Listing 6.1    Erste Version der Klasse »ThreadDemo1Activity«

Das folgende Quelltextfragment (siehe Listing 6.2) berechnet Fibonacci-Zahlen. Bitte fügen Sie es der Klasse ThreadDemo1Activity am besten nach der Methode onCreate() hinzu.

private fun fibRunner(num: Int): Runnable {
return object : Runnable {
override fun run() {
val result = fib(num)
Log.d(TAG, "fib($num) = $result")
}

private fun fib(n: Int): Int {
return when (n) {
0 -> 0
1 -> 1
else -> {
Thread.yield()
fib(n - 1) + fib(n - 2)
}
}
}
}
}

Listing 6.2    Die Methode »fibRunner()«

Um das Codefragment auszuprobieren, fügen Sie nach der Anweisung

Log.d(TAG, "Thread wurde gestartet")

die folgenden zwei Zeilen hinzu:

val fib = Thread(fibRunner(20))
fib.start()

Thread.yield() gibt Rechenzeit an andere, parallel ablaufende Threads ab. Dies zumindest gelegentlich zu tun, ist auch unter präemptivem Multitasking eine gute Idee. Wie lange ein Thread mit dieser Methode anhält, ist aber nicht definiert, und deshalb ist sie ungeeignet, wenn Sie die Abarbeitung für eine bestimmte Dauer unterbrechen möchten, wie zum Beispiel bei Spielen, die Positionsänderungen von Gegnern oder beweglichen Hindernissen alle n Sekunden vorsehen. Für solche Zwecke bietet die Klasse Thread die Methode sleep(). Wenn Sie das Quelltextfragment in Listing 6.3 mit der Anweisung

Thread(bewegeGegner1()).start()

starten, erscheint alle drei Sekunden eine Meldung in Logcat:

private fun bewegeGegner1(): Runnable {
return Runnable {
while (true) {
Log.i(TAG, "bewege Gegner 1")
try {
Thread.sleep(3000)
} catch (e: InterruptedException) {
Log.e(TAG, "sleepTester()", e)
}
}
}
}

Listing 6.3    Die Methode »bewegeGegner1()«

Ist Ihnen die Zeile while (true) { aufgefallen? Threads werden beendet, wenn alle Anweisungen in run() abgearbeitet wurden. Die while-Bedingung ist aber immer erfüllt, und auch der Schleifenrumpf enthält keine weiteren Abbruchgründe. Die Schleife wird also nie verlassen. Das kann in ganz seltenen Ausnahmesituationen so gewollt sein. Üblicherweise ist es aber schlicht falsch. Wie Sie richtig vorgehen, zeige ich Ihnen im folgenden Abschnitt.

[+]  Tipp

Sie können eine App in Android Studio beenden, indem Sie im Werkzeugfenster Logcat das Symbol inline image Terminate Application anklicken. In der Toolbar am oberen Rand des Hauptfensters sowie im Menü Run heißt es Stop 'app'.

Threads beenden

Die Klasse Thread beinhaltet die Methode stop(). Allerdings hat sich im Laufe der Zeit herausgestellt, dass ihre Verwendung aus unterschiedlichen Gründen unsicher ist, weshalb sie nicht verwendet werden soll. Technisch Interessierte finden eine ausführliche Abhandlung des Problems im Artikel Java Thread Primitive Deprecation.[ 5 ](https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html) Zur Lösung des eigentlichen Problems, also des Stoppens von Threads, gibt es mehrere Ansätze, jeder mit spezifischen Vor- und Nachteilen. Beispielsweise ist es möglich, auf das Auslösen einer InterruptedException mit dem Verlassen ihrer Thread-Schleife zu reagieren. Die Methode interrupt() der Klasse Thread löst eine solche Ausnahme aus. Sie können die Schleife aber auch mit einer Abbruchbedingung versehen, die von außen gesteuert wird. In der Regel ist dies eine Instanzvariable des Typs Boolean. Listing 6.4 zeigt eine mögliche Umsetzung. Fügen Sie den Code einfach an geeigneter Stelle in der Klasse ThreadDemo1Activity ein.

@Volatile
private var keepRunning = false

override fun onStart() {
super.onStart()
// Thread erzeugen
val t = Thread(bewegeGegner2())
keepRunning = true
// Thread starten
t.start()
}

override fun onPause() {
super.onPause()
keepRunning = false
}

private fun bewegeGegner2() = Runnable {
while (keepRunning) {
Log.i(TAG, "bewege Gegner 2")
try {
Thread.sleep(3000)
} catch (e: InterruptedException) {
Log.e(TAG, "sleepTester()", e)
}
}
}

Listing 6.4    Beenden eines Threads durch eine Abbruchbedingung

In Java bewirkt das Schlüsselwort volatile einen sogenannten Cache Flush. Dieser ist wichtig, weil das Java-Speichermodell sonst nicht gewährleistet, dass andere Threads den aktuellen Zustand der Variablen sehen. Das wiederum könnte dazu führen, dass der Thread sich niemals beendet, obwohl ein anderer Thread die Variable keepRunning auf false gesetzt hat. Die Kotlin-Annotation @Volatile kennzeichnet die Eigenschaft entsprechend.

Ich habe das Erzeugen und Starten des Threads in der Methode onStart() realisiert, die nach onCreate() aufgerufen wird. Wenn Sie durch Drücken der Home-Schaltfläche die Activity beendeten, würde aber ohne weitere Vorkehrungen dennoch weiterhin alle drei Sekunden die Meldung »bewege Gegner 2« in Logcat erscheinen, denn das bloße Verlassen einer Activity führt nicht zum Stopp zusätzlich gestarteter Threads. Allerdings kann Android diese zum Beispiel bei Speichermangel jederzeit terminieren. Um den Thread beim Verlassen der Activity zu beenden, muss nur die Variable keepRunning auf false gesetzt werden. Dies geschieht in der von mir überschriebenen Methode onPause() in Listing 6.4.

[»]  Hinweis

Das Beenden von Threads beim Verlassen einer Activity ist bewährte Praxis. Hintergrundaktivitäten werden unter Android mit sogenannten Services oder der Jetpack-Komponente WorkManager realisiert. In Abschnitt 6.2 und in Abschnitt 6.3 lernen Sie diese Grundbausteine kennen.

Threads bieten viele Möglichkeiten und geben dem Entwickler ein mächtiges Werkzeug an die Hand. Beispielsweise können Sie jedem Thread eine individuelle Priorität zuweisen und mehrere Threads zu Gruppen zusammenfassen. Allerdings erfordert insbesondere der Zugriff auf gemeinsame Ressourcen einiges an Disziplin und die Kenntnis der Funktionsweise von synchronized. Die entsprechende Funktion der Kotlin-Standardbibliothek wurde aber mittlerweile für veraltet erklärt. Sie sollte nicht mehr verwendet werden. Eine Alternative zeige ich Ihnen in Abschnitt 6.1.3, »Koroutinen«.

 
Zum Seitenanfang

6.1.2    Der Main- oder UI-Thread Zur vorigen ÜberschriftZur nächsten Überschrift

Jedem Thread kann ein Name zugewiesen werden. Die Anweisung Log.d(TAG, Thread.currentThread().name) gibt den Namen des aktuellen Threads in Logcat aus. Sofern die App diese Anweisung nicht in einem anderen Thread ausführt, ist dies der sogenannte Mainthread (Haupt-Thread). In ihm wird nicht nur Ihre Programmlogik ausgeführt, sondern beispielsweise auch das Zeichnen der Benutzeroberfläche. Aus diesem Grund wird er auch UI-Thread genannt.

Welche Konsequenzen dies hat, möchte ich Ihnen anhand des Beispiels ThreadDemo2 zeigen. Nach dem Öffnen der App (sie ist in Abbildung 6.1 zu sehen) können Sie nach Belieben Häkchen vor Berechnung mit sleep() setzen und entfernen. Der aktuelle Status (true oder false) wird unterhalb des Ankreuzfeldes angezeigt. Die Schaltfläche BERECHNUNG STARTEN simuliert eine länger andauernde Tätigkeit. Stellen Sie bitte sicher, dass Berechnung mit sleep() angehakt ist, und starten Sie dann die Berechnung. Nun geschieht etwas Unerwartetes: Die CheckBox reagiert nicht mehr auf Benutzereingaben. Erst nachdem die »Berechnung« abgeschlossen wurde, verhält sich die App wieder wie gewünscht.

Die App »ThreadDemo2«

Abbildung 6.1    Die App »ThreadDemo2«

Der Grund für ihr scheinbar merkwürdiges Verhalten liegt in der Art meiner Simulation begründet, weil bei angehakter CheckBox der aktuelle Thread (also die App) ungefähr drei Sekunden lang schlafen geschickt wird. Dies ist in Listing 6.5 zu sehen.

package com.thomaskuenneth.androidbuch.threaddemo2

import android.os.Bundle
import android.util.Log
import android.widget.*
import androidx.appcompat.app.AppCompatActivity

private val TAG = ThreadDemo2Activity::class.simpleName
class ThreadDemo2Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, Thread.currentThread().name)
val tv = findViewById<TextView>(R.id.textview)
val checkbox = findViewById<CheckBox>(R.id.checkbox)
checkbox.setOnCheckedChangeListener { _,
isChecked: Boolean -> tv.text = isChecked.toString() }
checkbox.isChecked = true
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
// --- Beginn Experimente ---
tv.text = getString(R.string.begin)
if (checkbox.isChecked) {
try {
Thread.sleep(3500)
} catch (e: InterruptedException) {
Log.e(TAG, "sleep()", e)
}
} else {
while (true) { }
}
tv.text = getString(R.string.end)
// --- Ende Experimente ---
}
}
}

Listing 6.5    Erste Version der Klasse »ThreadDemo2Activity«

Starten Sie die Berechnung hingegen ohne Häkchen vor Berechnung mit sleep() und versuchen ein paarmal, die Checkbox anzutippen, erscheint nach einer gewissen Zeit der unter Android-Entwicklern gefürchtete Hinweis Application not responding (ANR). Der Dialog ist in Abbildung 6.2 zu sehen. Auf diese Weise informiert das System den Benutzer, dass eine Anwendung nicht mehr reagiert, und bietet an, diese zu schließen oder weiter auf eine Reaktion zu warten.

Frage, ob eine nicht reagierende App beendet werden soll

Abbildung 6.2    Frage, ob eine nicht reagierende App beendet werden soll

Praktisch alle UI Frameworks (egal, ob unter Windows, Linux, macOS oder iOS) sind, wie der GUI-Teil von Android, single-threaded. Das bedeutet, dass der Mainthread für die Zustellung von allen Ereignissen an Widgets, aber auch für die Kommunikation Ihrer App mit den Bedienelementen zuständig ist. Was passiert, wenn der UI-Thread blockiert wird, können Sie in meinem Beispiel ThreadDemo2 sehr leicht nachvollziehen. Um das Problem zu lösen, lagern wir die lang dauernde Berechnung in einen eigenen Thread aus. Ersetzen Sie die ursprüngliche Fassung (also alles zwischen den Kommentaren Beginn Experimente und Ende Experimente) durch die Implementierung in Listing 6.6. Dieses Mal habe ich die Nennung des Interface und der implementierten Methode weggelassen.

Thread {
try {
Thread.sleep(10000)
} catch (e: InterruptedException) {
Log.e(TAG, "sleep()", e)
}
}.start()

Listing 6.6    Das Anklicken der Schaltfläche startet einen neuen Thread.

Testen Sie die App nun erneut. Sie verhält sich auch während der »Berechnung« normal. Allerdings gibt die ursprüngliche Version der App aus Listing 6.5 zu Beginn und am Ende der Berechnung einen Text aus. Dies tut die gerade eben vorgestellte Variante nicht mehr.

Handler

Fügen Sie deshalb die folgenden beiden Zeilen vor bzw. nach dem try-catch-Block aus Listing 6.6 ein, starten Sie das Programm, und klicken Sie anschließend auf BERECHNUNG STARTEN.

tv.text = getString(R.string.begin)
tv.text = getString(R.string.end)

Die App stürzt ab, und Android zeigt dem Benutzer einen Hinweis, dass ThreadDemo2 unerwartet beendet wurde. Die Ursache des Problems ist in Logcat nachzulesen: Es wurde eine CalledFromWrongThreadException geworfen. Deren Nachricht lautet: »Only the original thread that created a view hierarchy can touch its views.« Bei dem »original thread« handelt es sich um den Main- bzw. UI-Thread. »touch its views« bezieht sich auf den Versuch, mit tv.text = den anzuzeigenden Text zu setzen. Es muss also eine Möglichkeit geben, Anweisungen explizit auf diesem Thread auszuführen. Die folgende Implementierung (siehe Listing 6.7) zeigt, wie Sie richtig vorgehen:

val h = android.os.Handler(Looper.getMainLooper())
thread {
try {
h.post { tv.text = getString(R.string.begin) }
Thread.sleep(10000)
h.post { tv.text = getString(R.string.end) }
} catch (e: InterruptedException) {
Log.e(TAG, "sleep()", e)
}
}

Listing 6.7    Kommunikation mit dem UI-Thread über Handler

Als Erstes instanziieren Sie ein Objekt des Typs android.os.Handler. Damit können Sie Nachrichten (zum Beispiel in Gestalt eines Runnables) an die Warteschlange eines Threads senden. Diese wird durch eine android.os.Looper-Instanz repräsentiert. Ist Ihnen aufgefallen, dass ich durch die Verwendung von thread den Aufruf von start() weglassen konnte? Kotlin ist einfach wunderbar kompakt. Damit das klappt, müssen Sie aber die folgende Zeile hinzufügen:

import kotlin.concurrent.thread

Nicht jeder Thread hat automatisch eine Warteschlange. Um sie zu erzeugen und zu starten, müssen Sie zuerst Looper.prepare() und danach Looper.loop() aufrufen. Ein mit Looper.getMainLooper() erzeugter Handler nutzt die Warteschlange des Mainthreads. Das ist nötig, weil wir Text ausgeben möchten. Änderungen an der Oberfläche müssen auf dem Mainthread stattfinden. Für eigene Threads brauchen Sie prepare() und loop() aber, wenn Sie über Handler mit ihm kommunizieren möchten.

Den Looper eines Threads können Sie mit Looper.myLooper() ermitteln. Der eigentliche Nachrichtenversand erfolgt durch Aufruf der Handler-Methode post(). Sie müssen also lediglich die auszuführende Aktion (zum Beispiel tv.text = getString(R.string.begin)) in eine Runnable-Instanz packen und sie an post() übergeben.

In Android gibt es einige weitere Möglichkeiten, um mit dem UI-Thread zu kommunizieren, zum Beispiel bietet die Klasse Activity die Methode runOnUiThread() an. Auch ihr müssen Sie nur ein Runnable übergeben, was dann so aussehen kann (siehe Listing 6.8):

thread {
try {
Thread.sleep(10000)
} catch (e: InterruptedException) {
Log.e(TAG, "sleep()", e)
}
runOnUiThread { tv.text = getString(R.string.end) }
}
tv.text = getString(R.string.begin)

Listing 6.8    Verwendung der Methode »runOnUiThread()«

runOnUiThread() ist äußerst nützlich, wenn Sie in einem Hintergrund-Thread die Benutzeroberfläche aktualisieren möchten. Übrigens nutzt die Basisklasse android.app. Activity selbst einen Handler.

 
Zum Seitenanfang

6.1.3    Koroutinen Zur vorigen ÜberschriftZur nächsten Überschrift

In diesem Abschnitt möchte ich Ihnen Koroutinen als leichtgewichtige Alternative zu Java-Threads vorstellen. Aus Platzgründen kann das leider nur ein kurzer Abriss sein. Wenn Sie sich ausführlicher mit dem Thema befassen möchten, rate ich zu entsprechender weiterführender Literatur. Hinweise hierzu finden Sie im Literaturverzeichnis in Anhang D. Konzeptionell sind Koroutinen ein fester Bestandteil von Kotlin. Allerdings wird fast die gesamte Funktionalität durch eigene Bibliotheken zur Verfügung gestellt. Wenn Sie Koroutinen in Ihrer App verwenden möchten, müssen Sie deshalb die Zeilen

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4"

im Bereich dependencies { der modulspezifischen build.gradle-Datei eintragen. Eine sehr einfache Koroutine ist in Listing 6.9 zu sehen. Koroutinen sind Instanzen von unterbrechbaren Berechnungen. In meinem Beispiel ist das der Block nach launch. Die Funktion delay() verzögert die Abarbeitung der Schleife um zehn Sekunden. Anders als bei Thread.sleep() wird der aktuelle Thread dabei aber nicht blockiert. Hinter den Kulissen werden Koroutinen in Teile zerlegt. Jedes dieser Teile wird am Stück abgearbeitet.

GlobalScope.launch {
var i = 0
while (i < 1000) {
delay(10000)
Log.d(TAG, "${i++}")
}
}

Listing 6.9    Eine einfache Koroutine

Koroutinen gehören immer zu einem CoroutineScope. Er verwaltet und steuert eine oder mehrere Koroutinen, das heißt, er startet und unterbricht sie und wird bei Fehlern und Abbrüchen informiert. Außerdem legt der Coroutine Scope die Gültigkeit einer Koroutine fest. GlobalScope-Koroutinen sind nur bzgl. der Laufzeit der Anwendung eingeschränkt.

[»]  Hinweis

Sie können für Ihre App eigene Scopes definieren. Auch die zu Jetpack gehörenden Android Architecture Components tun dies. Der in Listing 6.9 verwendete GlobalScope ist in Activities keine gute Wahl, weil er nicht durch deren Lebenszyklus gesteuert wird.

Jede Koroutine wird in einem bestimmten Kontext (CoroutineContext) ausgeführt. Sie können auf ihn mit der CoroutineScope-Eigenschaft coroutineContext zugreifen. Der Kontext setzt sich zusammen aus Standardwerten, dem Elternkontext und Argumenten, die Sie zum Beispiel launch übergeben können. launch ist ein sogenannter Coroutine Builder. Solche Funktionen erzeugen und starten Koroutinen. Ich komme gleich darauf zurück. Vorher möchte ich aber kurz ansprechen, wo Koroutinen eigentlich ausgeführt werden.

Für ihre Verteilung auf einen oder mehrere Threads sind sogenannte Coroutine Dispatcher verantwortlich. Eine mit launch gestartete Koroutine wird standardmäßig mit Dispatchers.Default gemanagt. Das ist prima, solange der Code nicht die UI aktualisieren möchte. Denn sonst passiert, was ich Ihnen in Abschnitt 6.1.2 gezeigt habe: Die App stürzt ab. Aus diesem Grund gibt es Dispatchers.Main. Damit können Sie Ihre Koroutinen auf dem Mainthread ausführen lassen. Wie, zeige ich Ihnen gleich. Dispatchers.IO bietet sich für Datei- oder Netzwerkzugriffe an. Das Beispielprojekt CoroutineDemo (Abbildung 6.3) startet nach dem Anklicken von Start eine Koroutine, die zwischen einer und zehn Sekunden wartet und zu Beginn sowie am Ende einen Text ausgibt. Listing 6.10 ist ein Auszug der Klasse CoroutineDemoActivity.

Die App »CoroutineDemo«

Abbildung 6.3    Die App »CoroutineDemo«

Sie können mit withContext(Dispatchers.Main) Code auf dem Mainthread ausführen. Das ist vergleichbar mit runOnUiThread(), nur eben mit Koroutinen. GlobalScope.launch() kennen Sie bereits: Damit wird eine Koroutine gestartet. Sie erzeugt eine Zufallszahl zwischen 1 und 10 und ruft damit die private Funktion pause() auf. Das Schlüsselwort suspend kennzeichnet eine Funktion als unterbrechbar. Solche Funktionen können nur von Koroutinen oder anderen unterbrechbaren Funktionen aufgerufen werden. Sie bilden zusammen mit den Blöcken, die Sie Coroutine Buildern übergeben, die Bausteine für Koroutinen. withContext() und delay() sind ebenfalls unterbrechbare Funktionen.

package com.thomaskuenneth.androidbuch.coroutinedemo
...
import kotlinx.coroutines.*

class CoroutineDemoActivity : AppCompatActivity() {
private lateinit var textview: TextView

override fun onCreate(savedInstanceState: Bundle?) {
...
textview = findViewById(R.id.textview)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
GlobalScope.launch {
pause((1 + Math.random() * 10).toLong())
}
addToBegin("Button geklickt")
}
}

private suspend fun pause(sec: Long) {
withContext(Dispatchers.Main) {
addToBegin("Warte $sec Sekunden")
}
delay(1000 * sec)
withContext(Dispatchers.Main) {
addToBegin(" ---> $sec Sekunden gewartet")
}
}

private fun addToBegin(s: String) {
var current = textview.text
...
textview.text = getString(R.string.template, s, current)
}
}

Listing 6.10    Auszug der Klasse »CoroutineDemoActivity«

Wenn Sie die fertige App aus den Begleitmaterialien testen, scheint diese gut zu funktionieren. Sie hat aber (Sie ahnen es sicher) ein Problem. Um herauszufinden, welches, setzen Sie bitte die Wartezeit auf einen fixen Wert (zum Beispiel 15 Sekunden) und fügen der Funktion addToBegin() eine Logausgabe hinzu (am einfachsten ist println(s)). Starten Sie dann die App, klicken Sie den Button Start an, und wechseln Sie vor Ablauf der 15 Sekunden auf den Homescreen. Wenn Sie dabei die Ausgaben in Logcat beobachten, stellen Sie fest, dass die Koroutine munter weiterarbeitet, also alle Meldungen so ausgibt, als wäre die Activity noch aktiv. Stattdessen sollte die Koroutine aber beendet werden. Bei Threads hatten wir, Sie erinnern sich sicher, ein sehr ähnliches Problem.

GlobalScope.launch liefert eine kotlinx.coroutines.Job-Instanz. Mit diesem Interface können Sie den Zustand einer Koroutine erfragen (isActive, isCancelled, isCompleted), auf ihre Beendigung warten (join()) oder sie abbrechen (cancel()). Wir werden uns das zunutze machen, indem wir onPause() überschreiben und cancel() aufrufen. Allerdings sollten Sie nicht den Job in einer Instanzvariable speichern. Es ist besser, wenn CoroutineDemoActivity das Interface CoroutineScope implementiert. Auf diese Weise wird die Activity nämlich um die CoroutineScope-Extension-Methode cancel() erweitert. Sie bricht alle Koroutinen innerhalb des Scopes ab. Bitte übernehmen Sie die Änderungen aus dem kurzen Quelltextfragment in Listing 6.11, und prüfen Sie das Verhalten der App erneut.

class CoroutineDemoActivity : AppCompatActivity(), CoroutineScope {
...
override val coroutineContext = SupervisorJob() + Dispatchers.IO

override fun onPause() {
super.onPause()
cancel(null)
}

override fun onCreate(savedInstanceState: Bundle?) {
..
button.setOnClickListener {
launch {
pause((1 + Math.random() * 10).toLong())
}
...

Listing 6.11    Koroutinen beim Verlassen der App abbrechen

Jobs repräsentieren Koroutinen, die mit launch gestartet wurden. Bitte beachten Sie aber, dass nicht jeder Coroutine Builder ein Job-Objekt zurück liefert. Jobs können weitere Jobs starten. Bricht die Ausführung eines Kindes ab, führt dies normalerweise auch zum Abbruch der anderen Kinder sowie des Eltern-Jobs. Um das zu verhindern, können Sie (wie in meinem Beispiel) SupervisorJob verwenden.

Koroutinen können viel mehr, als ich Ihnen in dieser kurzen Einführung zeigen kann. Auf einen Aspekt bei der Implementierung möchte ich Sie aber unbedingt noch hinweisen. Koroutinen müssen sich kooperativ verhalten, indem sie regelmäßig mit isActive oder ensureActive() prüfen, ob sie noch weiter ausgeführt werden sollen. Zunächst ein Beispiel, wie Sie es nicht machen sollten:

val job = GlobalScope.launch {
var i = 0
while (true) {
println("${i++}")
}
}
Thread.sleep(3000)
job.cancel()

Listing 6.12    Eine nicht kooperative Koroutine

Wenn Sie dieses Codefragment ausführen, wird die Schleife trotz cancel() nicht verlassen. Zum Glück ist das Problem schnell behoben. Tauschen Sie einfach true durch isActive aus. Und schon verhält sich die Koroutine vorbildlich. Natürlich sollte ein Schleifendurchlauf trotzdem so schnell wie möglich abgeschlossen sein.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
Zur Rheinwerk-Konferenz für Kotlin
 Buchempfehlungen
Zum Rheinwerk-Shop: Kotlin

Kotlin


Zum Rheinwerk-Shop: Praxisbuch Usability und UX

Praxisbuch Usability und UX


Zum Rheinwerk-Shop: Flutter und Dart

Flutter und Dart


Zum Rheinwerk-Shop: App-Design

App-Design


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2023
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern