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 10 Datenbanken
Pfeil 10.1 Erste Schritte mit SQLite
Pfeil 10.1.1 Einstieg in SQLite
Pfeil 10.1.2 SQLite in Apps nutzen
Pfeil 10.2 Fortgeschrittene Operationen
Pfeil 10.2.1 Klickverlauf mit SELECT ermitteln
Pfeil 10.2.2 Daten mit UPDATE ändern und mit DELETE löschen
Pfeil 10.3 Implementierung eines eigenen Content Providers
Pfeil 10.3.1 Auf einen Content Provider zugreifen
Pfeil 10.3.2 Die Klasse »android.content.ContentProvider«
Pfeil 10.4 Zusammenfassung
 
Zum Seitenanfang

10.3    Implementierung eines eigenen Content Providers Zur vorigen ÜberschriftZur nächsten Überschrift

In Kapitel 7, »Telefonieren und surfen«, zeige ich Ihnen die Nutzung der systemweiten Anrufhistorie (Call Log). Mit der Methode query() einer ContentResolver-Instanz wurden alle entgangenen Anrufe ermittelt und in einer Liste abgelegt. Hierzu haben wir in einer Schleife einen Cursor zeilenweise weiterbewegt und auf einzelne Ergebnisspalten zugegriffen. Auch das Verändern von Werten mit der Methode update() wurde demonstriert. Cursor, query() und update() klingen zwar nach SQL, gehören in diesem Fall aber zu einer weiteren Datenzugriffsschicht von Android.

Content Provider stellen Informationen als Datensätze zur Verfügung. Alle interessierten Apps können mit einem Content Resolver auf sie zugreifen. Die Vorgehensweise ist stets die gleiche: Zuerst ermitteln Sie die Referenz auf ein Objekt des Typs android.content.ContentResolver. Üblicherweise geschieht dies durch Aufruf von getContentResolver() bzw. einfach contentResolver in Kotlin. Diese Methode ist in allen von android.content.Context abgeleiteten Klassen vorhanden. Anschließend nutzen Sie query(), insert(), update() und delete(), um Daten zu suchen, einzufügen, zu verändern und zu löschen. Operationen können sich auf eine oder mehrere Zeilen oder aber auf eine bestimmte Anzahl von Spalten beziehen.

Wo diese Tabelle gespeichert wird und in welchem Format dies geschieht, ist ein Implementierungsdetail des Providers und für den Nutzer der Daten unerheblich. Es kann sich also um eine lokale Datei, einen Webservice oder um eine beliebige andere Datenquelle handeln. Der Konsument greift ausschließlich auf Methoden der Klasse ContentResolver zu. Einige erinnern an die Klasse android.database.sqlite.SQLiteDatabase. Zwar unterscheiden sich die Methodensignaturen geringfügig, Grundlegende Konzepte wie die Übergabe von Werten in ContentValues-Instanzen sind aber gleich. Tatsächlich lassen sich SQLite-Datenbanken sehr elegant als Content Provider wiederverwenden.

 
Zum Seitenanfang

10.3.1    Auf einen Content Provider zugreifen Zur vorigen ÜberschriftZur nächsten Überschrift

Im bisherigen Verlauf dieses Kapitels haben wir uns zwei Versionen eines Stimmungsbarometers angesehen. Der ausgewählte Smiley sowie der Zeitpunkt der Erfassung werden in einer SQLite-Datenbank gespeichert. Lassen Sie uns nun eine dritte Variante implementieren, die nicht mehr direkt auf die SQLite-Datenbank, sondern auf einen Content Provider zugreift.

Die Klasse »DBDemo3Activity«

Die Activity DBDemo2Activity des Projekts DBDemo2 aus dem letzten Abschnitt greift an drei Stellen auf ein DBDemo2OpenHelper-Objekt zu. Diese Klasse leitet von android.database.sqlite.SQLiteOpenHelper ab und bildet die Datenzugriffsschicht der App. Um stattdessen einen Content Provider zu verwenden, müssen nur die Referenzen auf das Objekt entfernt und stattdessen die Methoden von ContentResolver aufgerufen werden (in diesem Fall nur insert() in imageButtonClicked()). Das Projekt DBDemo3 enthält diese und alle im Folgenden besprochenen Änderungen.

package com.thomaskuenneth.androidbuch.dbdemo3

import android.content.*
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class DBDemo3Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fine.setOnClickListener { imageButtonClicked(MOOD_FINE) }
ok.setOnClickListener { imageButtonClicked(MOOD_OK) }
bad.setOnClickListener { imageButtonClicked(MOOD_BAD) }
history.setOnClickListener {
val intent = Intent(this, HistoryActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or
Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
}

private fun imageButtonClicked(mood: Int) {
val values = ContentValues()
values.put(MOOD_MOOD, mood)
values.put(
MOOD_TIME,
System.currentTimeMillis()
)
contentResolver
.insert(CONTENT_URI, values)
Toast.makeText(this, R.string.saved, Toast.LENGTH_SHORT)
.show()
}
}

Listing 10.10    Die Klasse »DBDemo3Activity«

insert() erwartet als ersten Parameter einen URI, der den zu nutzenden Content Provider referenziert. Welchen Wert die Konstante CONTENT_URI in diesem Fall hat, zeige ich Ihnen etwas später. Der zweite Parameter, ein Objekt des Typs ContentValues, enthält die einzufügenden Daten. Im konkreten Fall sind dies:

  • die Stimmung (MOOD_MOOD)

  • der Zeitpunkt, an dem die Schaltfläche angeklickt wurde (MOOD_TIME)

Auch die Klasse HistoryFragment des Projekts DBDemo2 (sie zeigt einen Verlauf der vom Benutzer erfassten Stimmungen) enthält Verweise DBDemo2OpenHelper und muss geringfügig angepasst werden, um stattdessen einen Content Provider zu nutzen.

Die auf Content Provider umgestellte Klasse »HistoryFragment«

Historieneinträge erscheinen in einer Liste, die ihre Daten von einem CursorAdapter bezieht, den ein android.database.Cursor-Objekt bestückt. Eine entsprechende Referenz wird in der Methode updateList() ermittelt. Der Benutzer kann über ein Kontextmenü den Smiley-Typ bereits erfasster Einträge ändern. Den Aufruf der korrespondierenden ContentResolver-Methode update() habe ich in eine eigene private Methode gleichen Namens ausgelagert. Das Löschen findet in delete() statt.

package com.thomaskuenneth.androidbuch.dbdemo3

import android.content.ContentValues
import android.net.Uri
import android.os.Bundle
import android.view.*
import android.widget.*
import androidx.fragment.app.ListFragment

class HistoryFragment : ListFragment() {
private val menuInflater: MenuInflater?
get() = activity?.menuInflater

private lateinit var cursorAdapter: CursorAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
cursorAdapter = DBDemo3Adapter(context)
listAdapter = cursorAdapter
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
registerForContextMenu(listView)
updateList()
}

override fun onCreateContextMenu(
menu: ContextMenu, v: View,
menuInfo: ContextMenu.ContextMenuInfo?
) {
super.onCreateContextMenu(menu, v, menuInfo)
menuInflater?.inflate(R.menu.context_menu, menu)
}

override fun onContextItemSelected(item: MenuItem): Boolean {
val info = item
.menuInfo as AdapterView.AdapterContextMenuInfo
return when (item.itemId) {
R.id.menu_good -> {
update(
info.id,
MOOD_FINE
)
updateList()
true
}
R.id.menu_ok -> {
update(
info.id,
MOOD_OK
)
updateList()
true
}
R.id.menu_bad -> {
update(
info.id,
MOOD_BAD
)
updateList()
true
}
R.id.menu_delete -> {
delete(info.id)
updateList()
true
}
else -> super.onContextItemSelected(item)
}
}

private fun updateList() {
val cursor = context?.contentResolver?.query(
CONTENT_URI, null, null,
null, "$MOOD_TIME DESC"
)
cursorAdapter.changeCursor(cursor)
}

private fun update(id: Long, mood: Int) {
val uri = Uri.withAppendedPath(
CONTENT_URI,
id.toString()
)
val values = ContentValues()
values.put(MOOD_MOOD, mood)
context?.contentResolver?.update(uri, values, null, null)
}

private fun delete(id: Long) {
val uri = Uri.withAppendedPath(
CONTENT_URI,
id.toString()
)
context?.contentResolver?.delete(uri, null, null)
}
}

Listing 10.11    Die auf Content Provider umgestellte Klasse »HistoryFragment«

In beiden Fällen, dem Ändern und dem Löschen, wird die Methode withAppendedPath() aufgerufen. Content Provider gestatten den Zugriff auf individuelle Datensätze, indem an den CONTENT_URI eine eindeutige Kennung (id: Long) angehängt wird. Aus diesem Grund müssen Sie kein Auswahlkriterium festlegen, das Sie an update() bzw. delete() übergeben müssten. Damit haben wir alle notwendigen Anpassungen abgeschlossen, um über einen Content Provider auf die Stimmungsdatenbank zuzugreifen. Wie dieser realisiert wird, ist unser nächstes Thema.

 
Zum Seitenanfang

10.3.2    Die Klasse »android.content.ContentProvider« Zur vorigen ÜberschriftZur nächsten Überschrift

Content Provider leiten üblicherweise von der abstrakten Klasse android.content.ContentProvider ab. Sie müssen deshalb mindestens die Methoden onCreate(), query(), insert(), update(), delete() und getType() implementieren. Außerdem sollten Sie bestimmte Konstanten definieren. Besonders wichtig ist CONTENT_URI, denn dieser Verweis auf einen Content Provider wird, wie Sie bereits wissen, an sehr viele Methoden der Klasse ContentResolver übergeben.

Tabellenmodell und URIs

Content Provider verwalten einen oder mehrere Datentypen. Die App DBDemo3 kennt nur die Tabelle mood, in der Smiley-Typen und Erfassungszeitpunkte abgelegt werden. Die beiden Spalten dieser Tabelle bilden den Datentyp mood. Der in diesem Abschnitt entwickelte Content Provider kennt ausschließlich diesen Datentyp.

Denkbar ist aber auch, dass ein Content Provider mehrere Datentypen verwaltet. Zum Beispiel könnte er zwischen Personen, Adressen und Telefonnummern unterscheiden, und auch eine Gliederung in Untertypen ist möglich. Ein Content Provider, der Fahrzeuge verwaltet, könnte beispielsweise zwischen Land- und Wasserfahrzeugen unterscheiden. Interessant ist nun, wie beim Aufruf einer Methode aus ContentResolver der gewünschte »Datentopf« ausgewählt wird, weil ja üblicherweise nur ein URI übergeben wird.

Der URI eines Content Providers besteht aus mehreren Teilen. Er beginnt mit dem Standardpräfix content://, auf das die Authority folgt. Sie identifiziert einen Content Provider und sollte aus einem vollqualifizierten Klassennamen bestehen, der in Kleinbuchstaben umgewandelt wurde. Wie Sie später noch sehen werden, muss die Authority in der Manifestdatei eingetragen werden.

Der nun folgende Pfad kennzeichnet den von mir angesprochenen Datentyp. Sofern ein Content Provider nur einen Datentyp kennt, könnte er leer bleiben. Aus Gründen der Übersichtlichkeit rate ich Ihnen aber dazu, ihn trotzdem anzugeben. Hier bietet sich der Name der verwendeten Datenbanktabelle an (im Fall von DBDemo3 ist dies mood).

Um zwischen Untertypen zu unterscheiden, können Sie einen Pfad durch Slashes (/) in mehrere Segmente teilen. URIs, die diesem Schema folgen, repräsentieren den gesamten Datenbestand eines Content Providers; auch die Konstante CONTENT_URI hat dieses Format. Um einen ganz bestimmten Datensatz auszuwählen (zum Beispiel in der Klasse HistoryFragment beim Verändern und Löschen von Einträgen), wird dem URI noch eine eindeutige Kennung als Suffix hinzugefügt.

Die Klasse »DBDemo3Provider«

DBDemo3Provider leitet von android.content.ContentProvider ab. Sie definiert die beiden öffentlichen Konstanten AUTHORITY und CONTENT_URI. MOOD und MOOD_ID hingegen sind privat; sie werden verwendet, um bei Zugriffen auf den Content Provider zwischen dem gesamten Bestand und einzelnen Datensätzen zu unterscheiden. Auch die Variable uriMatcher wird in diesem Zusammenhang verwendet. Sie verweist auf ein Objekt des Typs android.content.UriMatcher. Ein solcher Baum verknüpft Authoritys und URIs mit einem Code, der zurückgeliefert wird, wenn die Methode match() mit einem entsprechenden URI aufgerufen wird.

onCreate() instanziiert ein Objekt des Typs DBDemo3OpenHelper und weist es der Variablen dbHelper zu. Der Rückgabewert true signalisiert, dass die Initialisierung des Content Providers erfolgreich war. getType() liefert zu einem übergebenen URI einen MIME-Typ (er beginnt mit vnd.android.cursor.). Hierzu wird die Methode match() des durch uriMatcher referenzierten UriMatcher aufgerufen. Der Aufbau Ihrer eigenen Typen muss streng dem Schema folgen, das in Listing 10.12 gezeigt ist.

package com.thomaskuenneth.androidbuch.dbdemo3

import android.content.*
import android.database.*
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.text.TextUtils
import java.util.*

val AUTHORITY =
DBDemo3Provider::class.qualifiedName!!.toLowerCase(Locale.US)
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$TABLE_MOOD_NAME")
private const val MOOD = 1
private const val MOOD_ID = 2
class DBDemo3Provider : ContentProvider() {
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

init {
uriMatcher.addURI(AUTHORITY, TABLE_MOOD_NAME, MOOD)
uriMatcher.addURI(
AUTHORITY, "$TABLE_MOOD_NAME/#",
MOOD_ID
)
}

private lateinit var dbHelper: DBDemo3OpenHelper

override fun onCreate(): Boolean {
dbHelper = DBDemo3OpenHelper(context)
return true
}

override fun query(
uri: Uri, projection: Array<String?>?, selection: String?,
selectionArgs: Array<String?>?, sortOrder: String?
): Cursor? {
val builder = SQLiteQueryBuilder()
builder.tables = TABLE_MOOD_NAME // Ein bestimmer Eintrag?
if (uriMatcher.match(uri) == MOOD_ID) {
builder.appendWhere("$COLUMN_ID = ${uri.pathSegments[1]}")
}
val cursor = builder.query(
dbHelper.writableDatabase, projection,
selection, selectionArgs,
null, null, if (sortOrder.isNullOrBlank()) MOOD_TIME else sortOrder
)
// bei Änderungen benachrichtigen
context?.contentResolver.let {
cursor?.setNotificationUri(it, uri)
}
return cursor
}

override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
val count = when (uriMatcher.match(uri)) {
MOOD -> dbHelper.writableDatabase.update(
TABLE_MOOD_NAME,
values, selection, selectionArgs
)
MOOD_ID -> dbHelper.writableDatabase.update(
TABLE_MOOD_NAME,
values, "$COLUMN_ID = ${uri.pathSegments[1]}"
+ if (!TextUtils.isEmpty(selection)) " AND ($selection)"
else "",
selectionArgs
)
else -> throw IllegalArgumentException("Unknown URI $uri")
}
notifyChange(uri)
return count
}

override fun delete(uri: Uri, selection: String?,
selectionArgs: Array<out String>?): Int {
val count = when (uriMatcher.match(uri)) {
MOOD -> dbHelper.writableDatabase.delete(
TABLE_MOOD_NAME,
selection, selectionArgs
)
MOOD_ID -> {
dbHelper.writableDatabase.delete(
TABLE_MOOD_NAME,
"$COLUMN_ID = ${uri.pathSegments[1]}"
+ if (!TextUtils.isEmpty(selection))
" AND ($selection)"
else "",
selectionArgs
)
}
else -> throw IllegalArgumentException("Unknown URI $uri")
}
notifyChange(uri)
return count
}

override fun insert(uri: Uri, values: ContentValues?): Uri? {
val rowID = dbHelper.writableDatabase
.insert(
TABLE_MOOD_NAME,
"", values
)
if (rowID > 0) {
val result = ContentUris.withAppendedId(
CONTENT_URI,
rowID
)
notifyChange(result)
return result
}
throw SQLException("Failed to insert row into $uri")
}

override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
MOOD ->
"vnd.android.cursor.dir/vnd.$AUTHORITY/$TABLE_MOOD_NAME"
MOOD_ID ->
"vnd.android.cursor.item/vnd.$AUTHORITY/$TABLE_MOOD_NAME"
else -> throw IllegalArgumentException(
"Unsupported URI: "
+ uri
)
}
}

private fun notifyChange(uri: Uri) {
context?.contentResolver?.notifyChange(uri, null)
}
}

Listing 10.12    Die Klasse »DBDemo3Provider«

insert() ermittelt zunächst mit writableDatabase eine Referenz auf ein SQLite-Datenbankobjekt und fügt diesem einen neuen Datensatz hinzu. Sofern diese Aktion erfolgreich war, muss meine private Funktion notifyChange() aufgerufen werden. Dies stellt sicher, dass Interessierte bei Änderungen des Content Providers informiert werden.

query() nutzt eine Instanz des Typs SQLiteQueryBuilder, anstatt direkt auf eine mit writableDatabase ermittelte Datenbank zuzugreifen. Dies ist nötig, weil zusätzlich zu der in den beiden Parametern selection und selectionArgs übergebenen Auswahlbedingung eine Prüfung stattfinden muss, wenn nicht im gesamten Datenbestand, sondern nach einem Datensatz gesucht werden soll.

update() prüft zunächst anhand des übergebenen URI, ob sich Änderungen auf den gesamten Datenbestand oder nur auf einen bestimmten Datensatz beziehen sollen. In diesem Fall wird die Auswahlbedingung um ein entsprechendes Kriterium erweitert. Auch bei Aktualisierungen müssen Interessierte durch notifyChange() informiert werden.

delete() schließlich prüft zunächst anhand des übergebenen URIs, ob sich Löschoperationen auf den gesamten Datenbestand oder nur auf einen bestimmten Datensatz beziehen sollen. Im letzteren Fall wird die Auswahlbedingung um ein entsprechendes Kriterium erweitert. Auch diese Funktion ruft notifyChange() auf.

notifyChange() erhält eine Uri. Diese wird nur an die gleichnamige ContentResolver-Methode weitergeleitet. Damit Interessierte tatsächlich informiert werden, wird in meiner query()-Implementierung mit cursor?.setNotificationUri() festgelegt, welche Uri zu dem Cursor gehört.

Damit ist die Implementierung des Content Providers abgeschlossen. Um ihn nutzen zu können, müssen Sie ihn mit <provider ... /> in die Manifestdatei eintragen. android:name enthält den Klassennamen. android:authorities listet die Authority(s), über die der Zugriff auf den Provider erfolgt. Und mit android:exported legen Sie fest, ob Ihr Content Provider auch von anderen Apps genutzt werden darf. In diesem Fall sollten Sie ihn mit Berechtigungen absichern. Mein Beispiel-Provider ist nur für DBDemo3 sichtbar.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.dbdemo3">
<application
...
<activity android:name=".HistoryActivity" />
<provider
android:name=".DBDemo3Provider"
android:authorities=
"com.thomaskuenneth.androidbuch.dbdemo3.dbdemo3provider"
android:exported="false" />
...
</manifest>

Listing 10.13    Auszug der Manifestdatei des Projekts »DBDemo3«

[»]  Hinweis

DBDemo3Helper ist im Grunde ein Klon der Klasse DBDemo2Helper des Projekts DBDemo2. Die Methoden insert(), query(), update() und delete() sind mit der Nutzung des Content Providers aber überflüssig geworden. Sie wurden deshalb entfernt.

Content Provider stellen tabellenartige Daten anwendungsübergreifend zur Verfügung. Die Schnittstelle wurde so konzipiert, dass sich vorhandene SQLite-Datenbanken mit minimalem Aufwand anbinden lassen. Aus welcher Quelle ein Content Provider seine Nutzdaten letztendlich bezieht, ist für den nutzenden Client vollkommen transparent. Deshalb können auch Webdienste oder RSS-Feeds als »Datentöpfe« dienen.

 


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