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.3    Regelmäßige Arbeiten Zur vorigen ÜberschriftZur nächsten Überschrift

Services bilden einen konzeptionellen Rahmen für die Ausführung vieler unterschiedlicher Hintergrundaktivitäten. Wie entkoppelt diese von der Benutzeroberfläche sind, hängt letztlich von der gewünschten Tätigkeit ab. Das Herunterladen von Dateien oder das Streamen von Audio ist vollständig im Hintergrund möglich. Bei Video kann zwar das Bereitstellen der Daten im Hintergrund erfolgen, aber das Anzeigen ist nur in Verbindung mit einer Activity bzw. einem Fragment sinnvoll.

In den bisher genannten Fällen war der Auslöser der Operation stets eine Aktion des Anwenders. Daneben gibt es aber eine ganze Reihe von Tätigkeiten, die nicht auf ausdrücklichen Befehl des Benutzers hin erfolgen. Denken Sie an das Synchronisieren einer lokalen Datenbank mit einem Server, das Indizieren von Dateien und Verzeichnissen, das Sichern von Statusinformationen, das Aktualisieren der Systemzeit oder das routinemäßige Ausführen von Wartungsarbeiten. All diese Vorgänge wirken sich nicht unmittelbar auf den Anwender oder die Benutzung einer App aus, sind in sich abgeschlossen und können prinzipiell zu jeder beliebigen Zeit ausgeführt werden.

 
Zum Seitenanfang

6.3.1    JobScheduler Zur vorigen ÜberschriftZur nächsten Überschrift

Für solche Jobs (sie werden auch geplante Services genannt) hat Google mit Android 5 die Job-Scheduler-API eingeführt. Sie basiert auf Services und stellt Ihnen als Entwickler einen einfach handhabbaren Mechanismus zur Verfügung, um das Ausführen von Hintergrundaktionen vom Eintreten bestimmter Bedingungen abhängig zu machen. Wie Sie gleich sehen werden, können Sie einen Job so konfigurieren, dass er beispielsweise nur ausgeführt wird, wenn das Gerät an ein Ladekabel angeschlossen ist, wenn es aktuell nicht verwendet wird oder wenn es mit einem nicht getakteten Netzwerk verbunden ist.

Natürlich lassen sich solche Bedingungen auch mit eigenem Code realisieren, allerdings laufen die Tasks der Apps dann nicht koordiniert ab. Das kann dazu führen, dass energieintensive Aktionen unnötig oft ausgeführt werden. Eine zentrale Steuerung hingegen sorgt dafür, dass vor der Ausführung der anstehenden Jobs eine Funkverbindung auf- und nach Abschluss der Jobs wieder abgebaut wird. Außerdem muss ein Job gar nicht erst gestartet werden, wenn das System weiß, dass die Rahmenbedingungen, die der Job für seine Abarbeitung braucht, ohnehin nicht erfüllt sind.

Die Job-Scheduler-API befreit Sie außerdem von der Eigenentwicklung einer ganzen Reihe weiterer, nicht ganz einfach zu realisierender Mechanismen. Sie sorgt für einen automatischen Wiederanlauf, wenn eine Aufgabe nicht erfolgreich abgeschlossen werden konnte. Sie bietet die regelmäßige Wiederholung eines Jobs an und kann einen automatischen Lauf nach einem Reboot sicherstellen. Auch Zustandsänderungen wie das Verlassen eines Netzwerks werden automatisch erkannt und verarbeitet. Das Erzeugen und Planen von Jobs ist in der App JobSchedulerDemo zu sehen. Listing 6.26 zeigt die Hauptaktivität JobSchedulerDemoActivity.

package com.thomaskuenneth.androidbuch.jobschedulerdemo

import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.ComponentName
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class JobSchedulerDemoActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getSystemService(JobScheduler::class.java)?.run {
// ausstehende Jobs anzeigen
val jobs = allPendingJobs
val sb = StringBuilder()
for (info in jobs) {
sb.append(info.toString() + "\n")
}
if (sb.isEmpty()) {
sb.append(getString(R.string.no_jobs))
}
val jobId = 123
// die Klasse des Jobs
val service = ComponentName(this@JobSchedulerDemoActivity,
JobSchedulerDemoService::class.java)
val jobInfo = JobInfo.Builder(jobId, service)
// alle 20 Minuten wiederholen
.setPeriodic(20 * 60 * 10000)
// nur wenn das Ladekabel angeschlossen ist
.setRequiresCharging(true)
.build()
// die Ausführung planen
schedule(jobInfo)
textview.text = sb.toString()
}
}
}

Listing 6.26    Die Klasse »JobSchedulerDemoActivity«

Jobs werden mit Objekten des Typs android.app.job.JobInfo beschrieben. Sie erzeugen eine solche Instanz, indem Sie zunächst mit JobInfo.Builder(…) einen Builder instanziieren und diesen über seine set…()-Methoden konfigurieren. Diese Aufrufe lassen sich verketten. Sie schließen die Konfiguration mit build() ab. Der Aufruf von setRequiresCharging(true) sorgt dafür, dass ein Job nur dann gestartet wird, wenn das Gerät an ein Ladekabel angeschlossen ist und lädt. setPeriodic() steuert, in welchen Abständen ein Job wiederholt ausgeführt wird. In meinem Beispiel geschieht dies alle 20 Minuten. Sie können diesen Wert nicht beliebig klein setzen. Der minimale Wert lässt sich mit der JobInfo-Methode getMinPeriodMillis() abfragen. Android garantiert übrigens nicht, wann innerhalb dieses Intervalls die Ausführung beginnt, sondern nur, dass es höchstens einmal geschieht.

Bedingungen wirken additiv. Es müssen demnach alle erfüllt sein, damit ein Job gestartet wird. Allerdings gibt es eine Ausnahme: Wenn Sie setOverrideDeadline() aufrufen, erfolgt die Abarbeitung in jedem Fall. Die Methode lässt sich aber nicht mit setPeriodic() kombinieren. Die Klasse android.app.job.JobScheduler ist für die Verwaltung von Jobs zuständig. Mit getSystemService(JobScheduler::class.java) ermitteln Sie die Referenz auf ein zentrales JobScheduler-Objekt. Anschließend können Sie mit allPendingJobs alle ausstehenden Jobs ermitteln, mit schedule() einen Job planen oder mit cancel() und cancelAll() ausstehende Jobs abbrechen.

Jobs implementieren

Wir haben dem Konstruktor von JobInfo.Builder(…) ein ComponentName-Objekt übergeben. Die angegebene Klasse JobSchedulerDemoService enthält die Implementierung meines Beispieljobs. Sie ist in Listing 6.27 zu sehen. Jobs leiten von der Klasse android.app.job.JobService ab. Die Klasse ist abstrakt. Kinder müssen die Methoden onStartJob() und onStopJob() implementieren.

Erstere enthält die Logik. Da der Aufruf auf dem Mainthread der Anwendung erfolgt, ist es wichtig, dass Sie Ihre Aktionen in einen eigenen Thread verlagern. In diesem Fall liefern Sie als Rückgabewert true. Falls es für den Job nichts zu tun gibt, setzen Sie ihn auf false. Mit jobFinished() teilen Sie Android mit, wenn die Arbeit Ihres Jobs beendet ist. Es ist unerheblich, in welchem Thread Sie die Methode aufrufen. onStopJob() wird aufgerufen, wenn die Rahmenbedingungen für die Ausführung nicht mehr erfüllt sind. In diesem Fall müssen Sie Ihre Arbeiten beenden, auch wenn der Job noch nicht vollständig abgearbeitet wurde. Der Rückgabewert steuert, ob er verworfen (false) oder für eine neue Ausführung eingeplant wird (true).

package com.thomaskuenneth.androidbuch.jobschedulerdemo

import android.app.job.*
import android.util.Log
import android.widget.Toast
import kotlin.concurrent.thread

private val TAG = JobSchedulerDemoService::class.simpleName
class JobSchedulerDemoService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
Log.d(TAG, "onStartJob()")
Toast.makeText(this, R.string.job_started, Toast.LENGTH_LONG)
.show()
thread {
Log.d(TAG, "Job in Aktion")
jobFinished(params, false)
}
return true
}

override fun onStopJob(params: JobParameters?): Boolean {
Log.d(TAG, "onStopJob()")
return false
}
}

Listing 6.27    Die Klasse »JobSchedulerDemoService«

android.app.job.JobService ist letztlich ein ganz normaler Service. Deshalb müssen Jobs in die Manifestdatei Ihrer App eingetragen werden. Wie das aussehen kann, ist in Listing 6.28 zu sehen. Das <service />-Tag kennen Sie bereits. android:name gibt die Klasse des Jobs (Services) an. android:permission="android.permission.BIND_JOB_SERVICE" muss vorhanden sein, sonst wird eine IllegalArgumentException geworfen (»Scheduled service ... does not require android.permission.BIND_JOB_SERVICE permission«).

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.jobschedulerdemo">
<application
...
<activity android:name=".JobSchedulerDemoActivity">
...
</activity>
<service
android:name=".JobSchedulerDemoService"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
</manifest>

Listing 6.28    Die Manifestdatei des Projekts »JobSchedulerDemo«

Sie können mit dem folgenden Kommando einen Job im Android Emulator testen. Die App muss hierzu installiert sein.

adb shell cmd jobscheduler run -f  
com.thomaskuenneth.androidbuch.jobschedulerdemo 123

Die Option -f besagt, dass der Job in jedem Fall ausgeführt wird, auch wenn die Startbedingungen (zum Beispiel: Gerät lädt) nicht erfüllt sind. Die sich anschließenden Parameter sind:

  1. Paketname der App

  2. Job-ID; sie wurde dem JobInfo.Builder-Konstruktor übergeben

Mit adb shell dumpsys jobscheduler können Sie sich sehr ausführliche Informationen zu allen Jobs anzeigen lassen. Es bietet sich an, die Ausgabe in eine Datei umzuleiten. Dann können Sie gezielt nach Ihren Jobs filtern.

 
Zum Seitenanfang

6.3.2    WorkManager Zur vorigen ÜberschriftZur nächsten Überschrift

Auch die Jetpack-Komponente WorkManager ermöglicht das Abarbeiten von regelmäßigen Aufgaben im Hintergrund. Google sieht die Bibliothek als modernen, zeitgemäßen Ersatz für den JobScheduler. Wie Sie gleich sehen werden, wirkt die API schlanker und eleganter. Außerdem leiten Worker nicht mehr direkt von Services ab, müssen deshalb nicht in der Manifestdatei registriert werden. Allerdings wird JobScheduler von WorkManager verwendet, wenn die Android-Version des Geräts neu genug ist. Sie sollten deshalb beide Varianten kennen. Um WorkManager in einem Projekt verwenden zu können, müssen Sie die folgende Zeile der modulspezifischen build.gradle-Datei hinzufügen:

implementation "androidx.work:work-runtime-ktx:2.4.0"

Mein Beispielprojekt WorkManagerDemo führt nach dem Anklicken eines Buttons eine sehr einfache Hintergrundaktion aus: In einer Schleife wird so lange eine Zufallszahl generiert, bis sie mit einer übergebenen Zahl übereinstimmt. Listing 6.29 zeigt die Activity der App. Um einen Worker regelmäßig auszuführen, verwenden Sie PeriodicWorkRequestBuilder. Dieser PeriodicWorkRequest-Funktion (sie liefert ein Objekt des Typs PeriodicWorkRequest.Builder) wird eine Dauer in Gestalt einer java.time.Duration-Instanz übergeben. Irgendwann innerhalb dieser Periode wird die Hintergrundaktion genau einmal ausgeführt, wobei Stromsparmaßnahmen des Systems sowie Startbedingungen (dazu gleich mehr) berücksichtigt werden. Die Dauer darf nicht kleiner sein als in PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS definiert. Sonst wird der übergebene Wert entsprechend angepasst und eine Logmeldung ausgegeben.

package com.thomaskuenneth.androidbuch.workmanagerdemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.work.*
import kotlinx.android.synthetic.main.activity_main.*
import java.time.Duration

class WorkManagerDemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
start.setOnClickListener {
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
val interval = Duration.ofMillis(
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS)
val work = PeriodicWorkRequestBuilder<DemoWorker>(interval)
.setConstraints(constraints)
.setInputData(Data.Builder().putInt(KeyNumber, 123).build())
.build()
WorkManager.getInstance(this).enqueue(work)
}
}
}

Listing 6.29    Die Klasse »WorkManagerDemoActivity«

Mit Startbedingungen definieren Sie die Voraussetzungen für die Ausführung eines Workers. Hierfür wird die Klasse Constraints.Builder verwendet. setRequiresCharging() legt beispielsweise fest, ob das Gerät geladen werden muss. Es gibt eine Reihe weiterer Startbedingungen, unter anderem setRequiresDeviceIdle() und setRequiresStorageNotLow(). Sie werden nach dem Setzen mit build() erzeugt und dem PeriodicWorkRequest.Builder mit setConstraints() übergeben. Das vom Builder mit build() erzeugte Objekt wird mit enqueue() in eine Warteschlage gestellt.

Um Werte an einen Worker zu übergeben, erzeugen Sie einen Data.Builder und konfigurieren ihn mit put...(). Jeder Aufruf erzeugt ein Schlüssel-Wert-Paar. In meinem Beispiel ist dies die Zahl 123. Sie wird über eine in der Konstante KeyNumber hinterlegten Zeichenkette referenziert. Wie, zeige ich Ihnen gleich. Das mit build() erzeugte Objekt wird an setInputData() übergeben.

[+]  Tipp

Neben PeriodicWorkRequest für wiederkehrende Hintergrundaufgaben gibt es OneTimeWorkRequest für einmalige Aktionen.

Lassen Sie uns nun einen Blick auf die Worker-Implementierung werfen. Sie ist in Listing 6.30 zu sehen. In den meisten Fällen ist es am einfachsten, von androidx.work. Worker abzuleiten. Wenn Sie Koroutinen verwenden möchten, bietet sich androidx.work.CoroutineWorker an. Und falls Sie RxJava (Bibliothek zum Bauen asynchroner und ereignisbasierter Programme) nutzen, steht Ihnen androidx.work.RxWorker zur Verfügung.

package com.thomaskuenneth.androidbuch.workmanagerdemo

import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters

const val KeyNumber = "key"
private val TAG = DemoWorker::class.simpleName
class DemoWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {

override fun doWork(): Result {
Log.d(TAG, applicationContext.getString(R.string.worker_started))
val number = inputData.getInt(KeyNumber, 42)
while (!isStopped) {
val rnd = (Math.random() * (number + 1)).toInt()
if (rnd == number) {
break
}
Log.d(TAG, "Math.random(): $rnd")
}
return Result.success()
}
}

Listing 6.30    Die Klasse »DemoWorker«

Meine Implementierung nutzt androidx.work.Worker. Die zentrale Methode ist doWork(). In ihr findet die komplette Verarbeitung statt. Sie wird auf einem Hintergrundthread aufgerufen. Aktionen, die die Benutzeroberfläche betreffen, sind also tabu. Ihr Worker muss seine Arbeiten synchron verrichten, das bedeutet, er verlässt doWork() erst nach Abschluss aller Tätigkeiten. Dafür stehen ihm zehn Minuten[ 7 ](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/managing-work#stop-worker) zur Verfügung, danach wird er beendet. Sofern Sie einen asynchronen Programmfluss benötigen, sollten Sie von der Elternklasse von Worker, androidx.work.ListenableWorker, ableiten.

Ob Ihr Worker seine Arbeit erfolgreich abschließen konnte, signalisieren Sie mit dem Rückgabewert von doWork(). Result.success() bedeutet: alles in Ordnung. Result.retry() fordert einen erneuten Start an. Und Result.failure() dokumentiert eine fehlerhafte Ausführung. Sicher ist Ihnen aufgefallen, dass der Worker-Konstruktor zwei Argumente an seine Elternklasse durchreicht. Sie stehen Ihnen über Instanzvariablen (bzw. get...()-Methoden) zur Verfügung. Mit applicationContext können Sie auf den Anwendungskontext zugreifen. Das ist praktisch, um Ressourcen zu nutzen (getString()). Mit inputData lesen Sie die Parameter aus, die Ihrem Worker übergeben wurden. In meinem Beispiel ist das eine Zahl, mit der die erzeugten Zufallszahlen verglichen werden.

Bitte denken Sie daran, dass Ihr Worker nach einer gewissen Zeit gestoppt wird. Sie sollten deshalb regelmäßig mit isStopped prüfen, ob Sie die Verarbeitung beenden müssen. Es bietet sich an, dies in Schleifen- oder Austrittsbedingungen zu tun. Damit verlassen wir den spannenden Bereich der Hintergrundverarbeitung. Im folgenden Abschnitt stelle ich Ihnen wichtige Konzepte für das Multitasking auf App-Ebene vor.

 


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