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.2    Fortgeschrittene Operationen Zur vorigen ÜberschriftZur nächsten Überschrift

Sie finden die verbesserte Fassung im Projekt DBDemo2. Beide Versionen lassen sich auf diese Weise gut miteinander vergleichen und parallel testen. SQLite-Datenbanken werden im privaten Anwendungsverzeichnis abgelegt. Da der Paketname Bestandteil dessen Pfades ist, hat jede Variante der App ihre eigene Datenbank.

 
Zum Seitenanfang

10.2.1    Klickverlauf mit SELECT ermitteln Zur vorigen ÜberschriftZur nächsten Überschrift

DBDemo2 zeigt den Klickverlauf in einem ListFragment an, das in die Activity HistoryActivity eingebettet ist. Um diese starten zu können, setzen wir für den Button Verlauf den OnClickListener wie folgt:

history.setOnClickListener {
val intent = Intent(this, HistoryActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or
Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}

Listing 10.3    Den Verlauf öffnen

Damit das funktioniert, muss die Activity in der Manifestdatei eingetragen sein (durch Einfügen von <activity android:name=".HistoryActivity" /> als Kind von <application />). Die beiden Flags sorgen dafür, dass bei geteilten Bildschirmen die gestartete Activity nach Möglichkeit neben oder unter der Hauptaktivität angezeigt wird.

Mit CursorAdaptern arbeiten

Die Klassen ListActivity, ListView und ListFragment beziehen die anzuzeigenden Daten von Objekten, die android.widget.ListAdapter implementieren. Android enthält eine ganze Reihe von Adaptern, die Ihnen einen Großteil der sonst nötigen Implementierungsarbeit abnehmen. android.widget.CursorAdapter beispielsweise stellt die Daten eines Cursors zur Verfügung. Diese äußerst praktische Klasse ist abstrakt. Kinder müssen die Methoden newView() und bindView() implementieren. DBDemo2Adapter (Listing 10.5) tut dies. newView() wird vom System aufgerufen, wenn eine neue View benötigt wird, was zum Beispiel während der erstmaligen Befüllung einer Liste der Fall ist. Die Beispielimplementierung entfaltet mit inflate() die im Folgenden abgedruckte Layoutdatei icon_text_text.xml. Sie stellt zur Laufzeit ein Symbol sowie zwei Zeilen Text mit unterschiedlicher Größe dar.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="16dip">

<ImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="16dip"
android:contentDescription="@null"
android:scaleType="centerInside" />

<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:layout_toEndOf="@id/icon"
android:textAppearance="?android:attr/textAppearanceMedium" />

<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/text1"
android:layout_alignStart="@id/text1"
android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>

Listing 10.4    Die Datei »icon_text_text.xml«

Die zweite von DBDemo2Adapter implementierte Methode bindView() überträgt konkrete Daten aus einem Cursor in eine bereits vorhandene View. Sie wird aufgerufen, wenn eine Liste befüllt wird oder wenn durch Scrollen verdeckte oder neue Bereiche sichtbar werden. Das Vorgehen ist stets gleich:

  • Auslesen eines Datenbankfeldes mit cursor.getXYZ()

  • Ermitteln der View mit findViewById()

  • Setzen des Wertes mit view.text = ... oder Ähnlichem

Die Umsetzung dieser Schritte ist in Listing 10.5 zu sehen:

package com.thomaskuenneth.androidbuch.dbdemo2

import android.content.Context
import android.database.Cursor
import android.view.*
import android.widget.*
import java.text.*
import java.util.*

class DBDemo2Adapter(context: Context?) : CursorAdapter(context, null, 0) {
private val dateFormat = SimpleDateFormat
.getDateInstance(DateFormat.MEDIUM)
private val timeFormat = SimpleDateFormat
.getTimeInstance(DateFormat.MEDIUM)

private val inflater = LayoutInflater.from(context)
private val date = Date()

override fun newView(context: Context?, cursor: Cursor?,
parent: ViewGroup?): View {
return inflater.inflate(R.layout.icon_text_text, null)
}

override fun bindView(view: View?, context: Context?,
cursor: Cursor?) {
cursor ?: return
val ciMood = cursor.getColumnIndex(MOOD_MOOD)
val mood = cursor.getInt(ciMood)
val image = view?.findViewById<ImageView>(R.id.icon)
image?.setImageResource(
when (mood) {
MOOD_FINE -> R.drawable.ic_smiley_fine
MOOD_OK -> R.drawable.ic_smiley_ok
else -> R.drawable.ic_smiley_bad
}
)
val ciTimeMillis = cursor.getColumnIndex(MOOD_TIME)
val timeMillis = cursor.getLong(ciTimeMillis)
date.time = timeMillis
val textview1 = view?.findViewById<TextView>(R.id.text1)
textview1?.text = dateFormat.format(date)
val textview2 = view?.findViewById<TextView>(R.id.text2)
textview2?.text = timeFormat.format(date)
}
}

Listing 10.5    Die Klasse »DBDemo2Adapter«

DBDemo2 speichert neben dem Smiley-Typ und dem Zeitpunkt der Erfassung eine eindeutige Kennung. Dieses _id-Feld wird vom System verwendet, um Tabellenzeilen auf Listenelemente abzubilden. Es muss vorhanden sein, wenn Sie CursorAdapter nutzen möchten. In welchem Zusammenhang dies geschieht, zeige ich Ihnen im folgenden Abschnitt.

Die Klasse »HistoryFragment«

HistoryActivity ist sehr kurz. Die Klasse erzeugt nur ein HistoryFragment und zeigt es an. Das Fragment (Listing 10.6) leitet von ListFragment ab. Es ist für die Anzeige der Smileys zuständig. Wenn der Benutzer einen Eintrag antippt und hält, erscheint ein Kontextmenü (Abbildung 10.3). Wie Sie bereits aus Kapitel 5, »Benutzeroberflächen«, wissen, fügen Sie es mit registerForContextMenu() hinzu. Das darf aber erst in onCreateContextMenu() erfolgen. Sonst wird zur Laufzeit eine Ausnahme geworfen. Außerdem müssen Sie die Methoden onCreateContextMenu() und onContextItemSelected() überschreiben.

Das Kontextmenü der Activity »History«

Abbildung 10.3    Das Kontextmenü der Activity »History«

Allerdings haben Fragmente keinen MenuInflater. Meine Implementierung nutzt deshalb den derjenigen Activity, in die das Fragment eingebettet ist. Ich habe das mit einem Getter realisiert, um Problemen bei einer zu frühen Zuweisung entgegenzuwirken.

package com.thomaskuenneth.androidbuch.dbdemo2

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
private lateinit var dbHelper: DBDemo2OpenHelper

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

override fun onDestroy() {
super.onDestroy()
dbHelper.close()
}

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 -> {
dbHelper.update(
info.id,
MOOD_FINE
)
updateList()
true
}
R.id.menu_ok -> {
dbHelper.update(
info.id,
MOOD_OK
)
updateList()
true
}
R.id.menu_bad -> {
dbHelper.update(
info.id,
MOOD_BAD
)
updateList()
true
}
R.id.menu_delete -> {
dbHelper.delete(info.id)
updateList()
true
}
else -> super.onContextItemSelected(item)
}
}

private fun updateList() {
// Cursor tauschen - der alte wird geschlossen
cursorAdapter.changeCursor(dbHelper.query())
}
}

Listing 10.6    Die Klasse »HistoryFragment«

HistoryFragment nutzt eine Kopie der Klasse DBDemo1OpenHelper als Schnittstelle zur Datenbank. Auch die neue Implementierung leitet von SQLiteOpenHelper ab. Wie Sie wissen, liefert writableDatabase ein Objekt des Typs SQLiteDatabase. Mit der Methode query() formulieren Sie Suchanfragen. Ergebnisse werden in Gestalt eines Cursors übertragen. Das folgende Quelltextfragment aus DBDemo2OpenHelper liefert alle Zeilen der Tabelle mood in absteigender zeitlicher Reihenfolge. In der Listenansicht erscheinen neue Einträge also weiter oben.

fun query(): Cursor? {
val db = writableDatabase
return db.query(
tableMoodName,
null, null, null,
null, null,
"$MOOD_TIME DESC"
)
}

Listing 10.7    Einträge in absteigender zeitlicher Reihenfolge ermitteln

Das Quelltextfragment wird in der Methode updateList() der Klasse HistoryFragment aufgerufen. Der neue Cursor wird mit changeCursor() einem CursorAdapter-Objekt (meiner Klasse DBDemo2Adapter) übergeben.

 
Zum Seitenanfang

10.2.2    Daten mit UPDATE ändern und mit DELETE löschen Zur vorigen ÜberschriftZur nächsten Überschrift

Die Methode onContextItemSelected() der Klasse HistoryFragment wird aufgerufen, wenn der Benutzer einen Befehl des Kontextmenüs auswählt. Dieses bietet an, einen neuen Smiley-Typ zu setzen oder den korrespondierenden Eintrag zu löschen. Die Aktualisierung eines Eintrags findet in der Methode update() der Klasse DBDemo2OpenHelper statt. Sie erhält als Parameter dessen eindeutige Kennung sowie den neuen Smiley-Typ. Die ID wird aus einer Instanz des Typs AdapterContextMenuInfo gelesen, die durch Aufruf der Methode getMenuInfo() (im Code ist, Kotlin-Standards folgend, nur menuInfo zu sehen) ermittelt wurde. Die private Methode updateList() sorgt nach Änderungen an der Datenbank für eine Aktualisierung des Cursors sowie des ListFragment.

Aktualisierung von Einträgen

Um einen oder mehrere Werte in einer Datenbank ändern zu können, müssen Sie ermitteln, welche dies sind. Die Änderung des Smiley-Typs bezieht sich immer auf genau eine Tabellenzeile, nämlich auf die Zeile, die zum Zeitpunkt der Kontextmenüauswahl aktiv ist. Deren ID wird, wie Sie gerade gesehen haben, von Android zur Verfügung gestellt. Welche Werte geändert werden sollen, übermitteln Sie dem System mit einem Objekt des Typs ContentValues. Dessen Methode put() wird der Name der zu ändernden Spalte sowie der neue Wert übergeben.

Nun können Sie die SQLiteOpenHelper-Methode update() aufrufen. Sie erwartet den Namen einer Tabelle, eine Bedingung, die die zu ändernden Zeilen festlegt, sowie das Objekt mit den neuen Werten. Die Änderungsbedingung wird in zwei Teilen übergeben. _ID + " = ?" legt das Suchkriterium fest. Das Fragezeichen wird aus der Wertemenge arrayOf(id.toString()) substituiert.

fun update(id: Long, smiley: Int) {
val db = writableDatabase
val values = ContentValues()
values.put(MOOD_MOOD, smiley)
val numUpdated = db.update(
tableMoodName,
values, "$columnId = ?", arrayOf(id.toString())
)
Log.d(TAG, "update(): id=$id -> $numUpdated")
}

Listing 10.8    Einen Eintrag aktualisieren

Löschen eines Eintrags

Auch der zu löschende Eintrag wird in Gestalt seiner ID von Android übermittelt. Deshalb ist die Vorgehensweise beim Löschen analog zur Aktualisierung eines Datensatzes. Die SQLiteOpenHelper-Methode delete() erwartet den Namen einer Tabelle sowie eine Bedingung, die die zu löschenden Zeilen festlegt. Die Löschbedingung wird wiederum in zwei Teilen übergeben. _ID + " = ?" legt das Suchkriterium fest. Das Fragezeichen wird analog zum Aktualisieren aus der Wertemenge arrayOf(id.toString()) substituiert.

fun delete(id: Long) {
val db = writableDatabase
val numDeleted = db.delete(
tableMoodName,
"$columnId = ?",
arrayOf(id.toString())
)
Log.d(TAG, "delete(): id=$id -> $numDeleted")
}

Listing 10.9    Löschen eines Eintrags

Android stellt eine Vielzahl von Funktionen zur Verfügung, um Dateien und Verzeichnisse zu lesen und zu schreiben. Üblicherweise werden diese im privaten Anwendungsverzeichnis abgelegt, sind also für andere Programme nicht erreichbar. Ist ein Austausch mit fremden Apps gewünscht, können Sie Dateien auf einer SD-Karte bzw. einem externen Speichermedium ablegen. Potenzielle Nutzer Ihrer Dateien müssen aber deren Aufbau kennen. Deshalb bietet sich ein Datenaustausch auf Dateiebene in der Regel nur für Standardformate an.

Strukturierte Daten sind besonders gut für eine Ablage in SQLite-Datenbanken geeignet. Auf diese greifen Sie mit der standardisierten Datenbanksprache SQL zu. Eigentlich prädestiniert die Verwendung von SQL die strukturierten Daten für einen Zugriff durch Drittanwendungen. Allerdings sind auch Datenbanken für fremde Apps unsichtbar, weil sie in einer Datei im privaten Anwendungsverzeichnis gespeichert werden. Auf Content Provider hingegen können beliebige Apps zugreifen. Deshalb sind sie ideal, um Daten zu veröffentlichen. Wir sehen Sie uns im folgenden Abschnitt genauer an.

 


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