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 5 Benutzeroberflächen
Pfeil 5.1 Views und ViewGroups
Pfeil 5.1.1 Views
Pfeil 5.1.2 Positionierung von Bedienelementen mit ViewGroups
Pfeil 5.1.3 Alternative Layouts
Pfeil 5.2 Vorgefertigte Bausteine für Oberflächen
Pfeil 5.2.1 Listen darstellen mit ListFragment
Pfeil 5.2.2 Programmeinstellungen mit dem PreferencesFragment
Pfeil 5.2.3 Dialoge
Pfeil 5.2.4 Menüs und Action Bar
Pfeil 5.3 Nachrichten und Hinweise
Pfeil 5.3.1 Toast und Snackbar
Pfeil 5.3.2 Benachrichtigungen
Pfeil 5.3.3 App Shortcuts
Pfeil 5.4 Trennung von Oberfläche und Logik
Pfeil 5.4.1 Bedienelemente ohne »findViewById()«
Pfeil 5.4.2 Android Architecture Components
Pfeil 5.5 Dark Mode
Pfeil 5.5.1 Das DayNight-Theme
Pfeil 5.5.2 Dark Mode in eigenen Themes
Pfeil 5.6 Zusammenfassung
 
Zum Seitenanfang

5.2    Vorgefertigte Bausteine für Oberflächen Zur vorigen ÜberschriftZur nächsten Überschrift

In meinem Beispiel Tierkreiszeichen (Kapitel 3, »Von der Idee zur Veröffentlichung«) haben Sie die Klasse android.widget.ListView kennengelernt. Mit ihr ist es sehr einfach, Listen (bildschirmfüllend) anzuzeigen. Aber wie gehen Sie vor, wenn Sie außer der Liste noch andere Informationen darstellen möchten? Natürlich ist es problemlos möglich, alle benötigten UI-Komponenten in einem eigenen Layout zusammenzufassen. Allerdings lassen sich diese anderen Informationen dann nicht ohne Weiteres als Block an anderer Stelle in Ihrer App wiederverwenden.

Mehrere Activities gleichzeitig darzustellen, ist zwar technisch nach wie vor möglich, von Google aber nicht gewünscht. Stattdessen sollten Sie die Teile Ihrer Oberflächen, die Sie wiederverwenden möchten, mit Fragmenten realisieren. Diese können Sie ganz nach Bedarf alleine oder kombiniert mit anderen Fragmenten darstellen. Die grundsätzliche Funktionsweise von Fragmenten habe ich Ihnen in Kapitel 4, »Wichtige Grundbausteine von Apps«, gezeigt. Jetzt sehen wir uns einige spezialisierte Ableitungen der Basisklasse an.

 
Zum Seitenanfang

5.2.1    Listen darstellen mit ListFragment Zur vorigen ÜberschriftZur nächsten Überschrift

Beginnen möchte ich mit androidx.fragment.app.ListFragment. Das Beispielprojekt MiniContacts (Abbildung 5.8) verwendet diese Klasse, um eine Liste mit den Namen Ihrer Kontakte anzuzeigen. Wenn Sie ein Listenelement antippen, stellt die Standard-Kontakte-App Details zu der Person dar. Die Klasse MainActivity ist nur für das Laden und Anzeigen der Benutzeroberfläche zuständig. In der Layoutdatei (activity_main.xml) befindet sich genau ein Element, nämlich mein MiniContactsFragment (Listing 5.13). Wir werden uns den Code gleich ausführlich ansehen. Damit das Auslesen der Kontaktdaten klappt, muss in der Manifestdatei die Berechtigung android. permission.READ_CONTACTS definiert und zur Laufzeit angefordert und genehmigt werden. Hierfür sind die Methoden handlePermissions() und onRequestPermissionsResult() zuständig. Ausführliche Informationen zu Berechtigungen finden Sie in Abschnitt 4.4, »Berechtigungen«.

MiniContactsFragment leitet von androidx.fragment.app.ListFragment ab. Diese Klasse beinhaltet eine ListView, die ihre Elemente als scrollbare, vertikale Liste anzeigt. Sie bezieht die darzustellenden Informationen von Adaptern, die das Interface android.widget.ListAdapter implementieren. Der SimpleCursorAdapter greift auf Daten eines sogenannten Cursors zu und stellt sie der ListView zur Verfügung. Der Cursor wiederum holt sich seine Informationen von einem Content Provider. Fürs Erste können Sie sich darunter eine Datenbank vorstellen, die aus Tabellen mit Zeilen und Spalten besteht. Ein Cursor zeigt immer auf eine Zeile einer bestimmten Tabelle. Weiterführende Informationen zu Content Providern finden Sie in Kapitel 10, »Datenbanken«.

Die App »MiniContacts«

Abbildung 5.8    Die App »MiniContacts«

Daten, die sich ändern können oder von einer App-fremden Komponente stammen (Datenbank, Webservice ...), werden üblicherweise mithilfe von Loadern bereitgestellt. Diese laden die anzuzeigenden Informationen (in diesem Beispiel Ihre Kontakte) und stellen sie nach Verfügbarkeit zum Anzeigen bereit. Hierzu implementiert MiniContactsFragment die drei Methoden onCreateLoader(), onLoadFinished() und onLoaderReset() des AndroidX-Interfaces LoaderManager.LoaderCallbacks. Bei den wenigen statischen Daten in meiner Tierkreiszeichen-App ist das hingegen nicht nötig.

[»]  Hinweis

Bitte beachten Sie, dass es das LoaderCallbacks-Interface auch als android.app.LoaderManager.LoaderCallbacks gibt, das zur Android-Standardklassenbibliothek gehört. Mit API-Level 28 hat Google es für veraltet erklärt und rät zur Nutzung von AndroidX – wie in meinem Beispiel.

Der Aufruf LoaderManager.getInstance(this).initLoader() in meiner privaten Methode load() bereitet einen Loader vor. Bei Bedarf wird ein neuer Loader erzeugt oder ein bereits vorhandener wiederverwendet. Im ersten Fall wird onCreateLoader() aufgerufen. Meine Implementierung instanziiert dann für den Zugriff auf die Kontaktdatenbank ein Objekt des Typs androidx.loader.content.CursorLoader und gibt es zurück. Die Methode onLoadFinished() wird aufgerufen, wenn das Laden der Daten abgeschlossen ist. adapter.swapCursor(data) stellt dem CursorAdapter die Daten zur Verfügung. Er reicht sie je nach Bedarf an die ListView weiter. onLoaderReset() kommt zum Zuge, wenn die Daten des Loaders ungültig werden. Dann sorgt adapter.swapCursor(null) dafür, dass unser SimpleCursorAdapter und die ListView geleert werden.

package com.thomaskuenneth.androidbuch.minicontacts

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.os.Bundle
import android.provider.ContactsContract
import android.view.View
import android.widget.ListView
import android.widget.SimpleCursorAdapter
import androidx.fragment.app.ListFragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader

private const val REQUEST_READ_CONTACTS = 123
class MiniContactsFragment : ListFragment(),
LoaderManager.LoaderCallbacks<Cursor> {

private val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.LOOKUP_KEY,
ContactsContract.Contacts.DISPLAY_NAME
)
private val selection = "((" +
ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND (" +
ContactsContract.Contacts.DISPLAY_NAME + " != '' ))"

private lateinit var adapter: SimpleCursorAdapter

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
handlePermissions()
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_READ_CONTACTS -> {
if (grantResults.isNotEmpty()
&& grantResults[0] ==
PackageManager.PERMISSION_GRANTED
) {
load()
} else {
setEmptyText(context?.getString(R.string.no_permission))
}
}
}
}

override fun onCreateLoader(
id: Int,
args: Bundle?
): Loader<Cursor> {
return CursorLoader(
requireContext(),
ContactsContract.Contacts.CONTENT_URI,
projection, selection, null, null
)
}

// Wird aufgerufen, wenn ein Loader mit dem Laden fertig ist
override fun onLoadFinished(
loader: Loader<Cursor>,
data: Cursor?
) {
adapter.swapCursor(data)
}

// Wird aufgerufen, wenn die Daten eines Loaders ungültig
// geworden sind
override fun onLoaderReset(loader: Loader<Cursor>) {
adapter.swapCursor(null)
}

override fun onListItemClick(l: ListView, v: View,
position: Int, id: Long) {
val intent = Intent(Intent.ACTION_VIEW)
val c = listView.getItemAtPosition(position) as Cursor
val uri = ContactsContract.Contacts.getLookupUri(
c.getLong(0), c.getString(1)
)
intent.data = uri
startActivity(intent)
}

private fun handlePermissions() {
if (context?.checkSelfPermission(Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.READ_CONTACTS),
REQUEST_READ_CONTACTS
)
} else {
load()
}
}

private fun load() {
// Welche Spalte wird in welcher View angezeigt?
val fromColumns =
arrayOf(ContactsContract.Contacts.DISPLAY_NAME)
val toViews = intArrayOf(android.R.id.text1)
adapter = SimpleCursorAdapter(
context,
android.R.layout.simple_list_item_1, null,
fromColumns, toViews, 0
)
listAdapter = adapter
setEmptyText(context?.getString(R.string.no_contacts))
// Loader initialisieren
LoaderManager.getInstance(this).initLoader(0, null, this)
}
}

Listing 5.13    Die Klasse »MiniContactsFragment«

Um auf das Antippen eines Listenelements zu reagieren, müssen Sie nur die Methode onListItemClick() überschreiben. Das Anzeigen von Kontaktdaten habe ich Ihnen schon einmal kurz gezeigt: Hierzu erzeugen Sie ein Intent mit der Aktion ACTION_VIEW und lösen es mit startActivity() aus. Der gewünschte Kontakt wird als Uri übermittelt. Der Ausdruck

ContactsContract.Contacts.getLookupUri(c.getLong(0), c.getString(1))

greift auf Daten zu, die durch den CursorAdapter bereitgestellt werden. Ein Cursor beschreibt eine Ergebniszeile und ist spaltenweise organisiert. Welche Elemente er enthält, habe ich mit dem String-Array projection definiert. selection legt fest, wie nach Kontakten gesucht werden soll.

Ist Ihnen aufgefallen, dass in projection mehr Werte angefordert werden, als letztlich in der Liste zu sehen sind? Adapter bereiten die eingehenden Daten für die Anzeige auf. Der zweite Parameter des SimpleCursorAdapter-Konstruktors erhält hierfür die ID eines Layouts, das jeweils für ein Listenelement verwendet wird. android.R.layout.simple_list_item_1 ist ein sehr einfaches, einzeiliges Layout. Da es zur Plattform gehört, finden Sie in den Dateien meines Beispiels keine dazu passende XML-Datei. Mit den Arrays fromColumns und toViews lege ich fest, dass der Wert DISPLAY_NAME in einer TextView mit der ID android.R.id.text1 angezeigt werden soll. Je nachdem, welches Layout Sie als Listenelement verwenden, stehen in toViews natürlich andere Werte: die IDs der Views, die Inhalte des Cursors anzeigen sollen.

[»]  Hinweis

Mein Beispiel Tierkreiszeichen aus Kapitel 3, »Von der Idee zur Veröffentlichung«, nutzt einen eigenen Adapter, weil ich Ihnen dessen Funktionsweise (zum Beispiel ViewHolder und Wiederverwendung mit convertView) nahebringen möchte. Wenn die fertigen Implementierungen zu Ihrer App passen, müssen Sie sich die Mühe, einen eigenen zu implementieren, natürlich nicht machen.

Übrigens können Sie mit setEmptyText() einen Text festlegen, der erscheint, wenn es keine anzuzeigenden Listenelemente gibt. Noch ein Tipp: Bis Sie einen Adapter setzen (in meinem Beispiel ist dies die Zeile listAdapter = adapter), blendet ListFragment eine Fortschrittsanzeige ein. Sie verschwindet nach einem Aufruf von setListShown(true). Das Setzen eines Adapters erledigt das aber für Sie, sodass Sie diese Methode eigentlich nicht aufrufen müssen.

 
Zum Seitenanfang

5.2.2    Programmeinstellungen mit dem PreferencesFragment Zur vorigen ÜberschriftZur nächsten Überschrift

Nahezu jede App lässt sich durch den Benutzer anpassen. Wie Android Sie beim Bau solcher Einstellungsseiten unterstützt, zeige ich Ihnen anhand des Programms PreferencesDemo. Die App besteht aus den drei Klassen PreferencesDemoActivity (das ist die Hauptaktivität), SettingsFragment (die eigentlichen Einstellungen) und SettingsActivity (Abbildung 5.9). Sie zeigt, wie Sie ein Fragment in eine Activity ohne Layoutdatei einbetten. Die Hauptaktivität enthält nur die Schaltfläche Einstellungen sowie ein Textfeld, das den gegenwärtigen Status der Einstellungen enthält. Ein Klick auf diese Schaltfläche öffnet die Einstellungsseite, die aktiv bleibt, bis Sie sie mit Zurück (oder der entsprechenden Wischgeste) beenden. Nun aktualisiert PreferencesDemoActivity das Textfeld mit den gegebenenfalls geänderten Einstellungen.

Die Einstellungsseite der App »PreferencesDemo«

Abbildung 5.9    Die Einstellungsseite der App »PreferencesDemo«

Einstellungsseiten gibt es in Android seit der ersten Version. Mit API-Level 29 hat Google aber das Plattform-Paket android.preference für veraltet erklärt und empfiehlt stattdessen die Verwendung der Jetpack-Komponente Preference. Um sie in Ihrem Projekt nutzen zu können, fügen Sie der modulspezifischen Datei build.gradle folgende Zeile hinzu:

implementation "androidx.preference:preference:1.1.1"

Darüber hinaus gibt es mit androidx.preference:preference-ktx noch ein paar Kotlin-spezifische Erweiterungen, die unter anderem den Umgang mit PreferenceGroup vereinfachen. Für normale Einstellungsseiten werden Sie diese zusätzliche Bibliothek wahrscheinlich nicht benötigen. Die wichtigste Klasse ist androidx.preference.PreferenceFragmentCompat. Ihre Nutzung ist sehr einfach: Leiten Sie Ihr Einstellungsfragment davon ab, und laden Sie deren Benutzeroberfläche aus einer XML-Datei (Listing 5.14).

package com.thomaskuenneth.androidbuch.preferencesdemo

import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat

class SettingsFragment : PreferenceFragmentCompat() {

override fun onCreatePreferences(savedInstanceState: Bundle?,
rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
}
}

Listing 5.14    Die Klasse »SettingsFragment«

Hierfür müssen Sie die Methode onCreatePreferences() implementieren. Sie wird während der Abarbeitung von onCreate() aufgerufen und stellt die Einstellungen für ein Fragment zur Verfügung: Rufen Sie entweder direkt die Methode setPreferenceScreen() auf oder wie in meinem Beispiel addPreferencesFromResource(). Sie lädt die Einstellungsseite aus einer XML-Datei (preferences.xml), die sich in res/xml befinden muss.

Jede Einstellungsseite hat das Wurzelelement <PreferencesScreen />. Mit dem Element <PreferenceCategory /> können Sie Ihre Einstellungen in Rubriken oder Kategorien unterteilen. Neben den hier gezeigten <EditTextPreference /> und <CheckBoxPreference /> kennt Android zahlreiche weitere Elemente, die entweder direkt auf klassische Bedienelemente abgebildet werden oder beim Anklicken Dialoge öffnen. Dies ist auch bei <EditTextPreference /> der Fall. Der Titel dieses Dialogs wird mit dem Attribut android:dialogTitle festgelegt. Die Attribute android:title und android: summary geben den angezeigten Text sowie eine Beschreibung an. Möchten Sie, dass anstelle der Zusammenfassung der gespeicherte Wert zu sehen ist, dann setzen Sie app:useSimpleSummaryProvider auf true. Damit Android Studio das Präfix app: auflösen kann, müssen Sie unter Umständen im Tag <PreferenceScreen ... > händisch das Attribut xmlns:app="http://schemas.android.com/apk/res-auto" hinzufügen.

[+]  Tipp

Mit android:icon können Sie für Kategorien und einzelne Einstellungen Symbole anzeigen. Wenn Sie der freie Platz bei nicht vorhandenen Icons stört, lässt er sich mit app:iconSpaceReserved="false" entfernen.

Um im Programm auf die Einstellungen zugreifen zu können, müssen alle <...Preference />-Tags das Attribut android:key enthalten. Die vollständige Fassung von preferences.xml sieht so aus:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/cat1">
<CheckBoxPreference
android:key="checkbox_1"
android:summary="@string/summary_cb1"
android:title="@string/title_cb1" />
<CheckBoxPreference
android:key="checkbox_2"
android:summary="@string/summary_cb2"
android:title="@string/title_cb2" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/cat2">
<EditTextPreference
android:dialogTitle="@string/dialog_title_et1"
android:key="edittext_1"
android:summary="@string/summary_et1"
android:title="@string/title_et1" />
</PreferenceCategory>
</PreferenceScreen>

Listing 5.15    Die Datei »preferences.xml«

Die Klasse SettingsActivity nutzt den FragmentManager, um als einzigen Inhalt das Fragment SettingsFragment anzuzeigen. Dies geschieht innerhalb einer Fragment-Transaktion:

package com.thomaskuenneth.androidbuch.preferencesdemo

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

class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, SettingsFragment())
.commit()
}
}

Listing 5.16    Die Klasse »SettingsActivity«

Zuerst wird durch Zugriff auf supportFragmentManager (die Activity leitet von AppCompatActivity ab) der zuständige Fragment Manager ermittelt. beginTransaction() beginnt eine neue Transaktion. replace() zeigt das Fragment anstelle des gegebenenfalls aktuell dargestellten an. android.R.id.content bezieht sich hierbei auf das Wurzelelement der View-Hierarchie. Es wird also der komplette Bereich unterhalb der Action Bar ausgetauscht. Die Methode commit() beendet die Transaktion.

Die Hauptaktivität PreferencesDemoActivity registriert einen OnClickListener, um beim Anklicken der Schaltfläche Einstellungen mit startActivityForResult() die Activity SettingsActivity zu starten. Nach deren Ende (onActivityResult()) werden die Einstellungen ausgelesen und in einem Textfeld angezeigt. Der vollständige Quelltext sieht so aus:

package com.thomaskuenneth.androidbuch.preferencesdemo

import android.content.Intent
import android.os.Bundle
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager

private const val REQUEST_SETTINGS = 1234
class PreferencesDemoActivity : AppCompatActivity() {

private lateinit var textview: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.button).setOnClickListener {
val intent = Intent(
this,
SettingsActivity::class.java
)
startActivityForResult(intent, REQUEST_SETTINGS)
}
textview = findViewById(R.id.textview)
updateTextView()
}

override fun onActivityResult(
requestCode: Int,
resultCode: Int, data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
if (REQUEST_SETTINGS == requestCode) {
updateTextView()
}
}

private fun updateTextView() {
val prefs = PreferenceManager
.getDefaultSharedPreferences(this)
val cb1 = if (prefs.contains("checkbox_1"))
prefs.getBoolean("checkbox_1", false).toString()
else
getString(R.string.not_set)
val cb2 = if (prefs.contains("checkbox_2"))
prefs.getBoolean("checkbox_2", false).toString()
else
getString(R.string.not_set)
val et1 = prefs.getString("edittext_1", null)
?: getString(R.string.not_set)
textview.text = getString(R.string.template, cb1, cb2, et1)
}
}

Listing 5.17    Die Klasse »PreferencesDemoActivity«

Um auf Einstellungen zuzugreifen, nutzen Sie die statische Methode getDefaultSharedPreferences() der Klasse androidx.preference.PreferenceManager. Einzelne Werte lesen Sie beispielsweise mit getBoolean() oder getString() aus. Der jeweils als erster Parameter übergebene String entspricht dabei dem Wert, den Sie im Attribut android:key des korrespondierenden Elements in der XML-Datei mit den Einstellungen (zum Beispiel preferences.xml) angegeben haben. Der zweite Parameter wird von der get…()-Methode zurückgeliefert, wenn der angefragte Schlüssel nicht vorhanden ist. In diesem Fall wird er aber nicht angelegt. Ist Ihnen aufgefallen, dass ich mit contains() prüfe, ob ein Schlüssel vorhanden ist? Das ist praktisch, wenn Sie den Benutzer zum Beispiel mit einem Hinweis wie »Nicht gesetzt« darüber informieren möchten.

Übrigens hat das nicht vorhanden Sein eines Schlüssels bestimmte Konsequenzen: Die Benutzeroberfläche der Einstellungsseite wird durch Auslesen der Shared Preferences initialisiert. Ein Ankreuzfeld erhält also ein Häkchen, wenn der korrespondierende Boolean-Wert true war, was bei nicht vorhandenen Schlüsseln natürlich nicht der Fall ist. Um ein Auseinanderlaufen von Code und Oberfläche zu vermeiden, können Sie in der XML-Datei Default-Werte eintragen. Hierfür gibt es das Attribut android:defaultValue. Allerdings pflegen Sie Standardwerte dann an zwei Stellen, nämlich in XML und in den get…()-Aufrufen. Um das zu umgehen, bietet es sich an, mit contains() prüfen, ob ein Schlüssel vorhanden ist. Falls nicht, speichern Sie den von Ihnen gewünschten Startwert. Das Speichern eines Wertes funktioniert so:

val e = prefs.edit()
e.putBoolean("checkbox_1", false)
e.putBoolean("checkbox_2", false)
e.putString("edittext_1", "")
e.apply()

Listing 5.18    Einstellungen programmatisch speichern

e ist die Referenz auf ein Objekt des Typs SharedPreferences.Editor. Dessen Methoden putBoolean() und putString() weisen einem Schlüssel Werte des jeweiligen Typs zu. Die Änderungen werden aber erst in den Einstellungsspeicher zurückgeschrieben, wenn Sie die Methode apply() aufrufen. Der Editor wirkt also wie ein Puffer.

 
Zum Seitenanfang

5.2.3    Dialoge Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn der Benutzer ein <EditTextPreference />-Element (um genau zu sein, die daraus erzeugte GUI-Komponente) anklickt, öffnet sich ein kleines Fenster mit einer Überschrift, einem Eingabefeld sowie zwei Schaltflächen. Solche modalen Dialoge werden über der aktuellen Activity angezeigt. Sie erhalten den Fokus und nehmen alle Eingaben entgegen. Der Anwender wird also in seiner Tätigkeit unterbrochen. Deshalb sollten Dialoge stets in enger Beziehung zu der gegenwärtigen Aktivität stehen. Beispiele sind Aufforderungen zur Eingabe von Benutzername und Passwort, Fortschrittsanzeigen oder Hinweise und Fehlermeldungen. Anhand des in Abbildung 5.10 dargestellten Projekts DialogDemo zeige ich Ihnen, wie Sie Dialoge in eigenen Programmen einsetzen.

Die beiden Schaltflächen ALERTDIALOG und DATEPICKERDIALOG öffnen jeweils einen Dialog. Die Klasse android.app.DatePickerDialog ermöglicht dem Benutzer die Auswahl eines Datums. Android kombiniert hierzu die View android.widget.DatePicker mit zwei Schaltflächen zum Übernehmen der Eingabe bzw. zum Abbrechen des Vorgangs. Damit das System das ausgewählte Datum an Ihre Activity oder Ihr Fragment übermitteln kann, registrieren Sie einen OnDateSetListener. Ein solches Objekt wird dem Konstruktor von DatePickerDialog übergeben. android.app.AlertDialog ist die Elternklasse von DatePickerDialog. Sie ist bestens geeignet, um Warn- oder Hinweisdialoge zu realisieren, denn sie ermöglicht Dialoge mit bis zu drei Schaltflächen, einer Überschrift und einer Nachricht.

Die App »DialogDemo«

Abbildung 5.10    Die App »DialogDemo«

In frühen Android-Versionen wurden Dialoge durch Aufruf der Activity-Methoden onCreateDialog(), onPrepareDialog() und showDialog() gebaut, vorbereitet und angezeigt. Mit der Einführung von Fragmenten hat Google diese Vorgehensweise allerdings für veraltet erklärt. Seitdem werden Dialoge in Fragmente eingebettet. Wie dies funktioniert, zeige ich Ihnen zunächst am Beispiel der Datumsauswahl. Sehen Sie sich hierzu die Klasse DatePickerFragment an.

package com.thomaskuenneth.androidbuch.dialogdemo

import android.app.DatePickerDialog
import android.app.DatePickerDialog.OnDateSetListener
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import java.util.*

class DatePickerFragment : DialogFragment() {

private lateinit var listener: OnDateSetListener

override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnDateSetListener) {
listener = context
}
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// das aktuelle Datum als Voreinstellung nehmen
val c = Calendar.getInstance()
val year = c.get(Calendar.YEAR)
val month = c.get(Calendar.MONTH)
val day = c.get(Calendar.DAY_OF_MONTH)
// einen DatePickerDialog erzeugen und zurückliefern
return DatePickerDialog(
requireContext(),
listener, year, month, day
)
}

companion object {
val TAG = DatePickerFragment::class.simpleName
}
}

Listing 5.19    Die Klasse »DatePickerFragment«

Die Elternklasse ist androidx.fragment.app.DialogFragment. In der Methode onCreateDialog() wird ein Objekt des Typs android.app.Dialog erzeugt. In meinem Beispiel ist das eine DatePickerDialog-Instanz. Dem Konstruktor werden der Eltern-Kontext (requireContext()), das aktuelle Datum sowie ein Objekt des Typs DatePickerDialog.OnDateSetListener übergeben. Die einzige Methode dieses Interface, onDateSet(), wird aufgerufen, wenn im Dialog ein Datum ausgewählt wird. Die zugehörige Variable wird in onAttach() gesetzt. Warum das so gemacht wird (die Prüfung mit is mag zunächst ungewöhnlich erscheinen) und wofür die in einem companion object definierte Konstante TAG benötigt wird, erkläre ich Ihnen gleich. Sie wird ja in DatePickerFragment selbst nicht genutzt. Zuvor sehen wir uns aber noch die Klasse AlertFragment an:

package com.thomaskuenneth.androidbuch.dialogdemo

import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment

class AlertFragment : DialogFragment() {

private lateinit var listener: DialogInterface.OnClickListener

override fun onAttach(context: Context) {
super.onAttach(context)
if (context is DialogInterface.OnClickListener) {
listener = context
}
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Builder instanziieren
val builder = AlertDialog.Builder(requireContext())
// Builder konfigurieren
builder.setTitle(R.string.app_name)
builder.setMessage(R.string.message)
builder.setCancelable(false)
builder.setPositiveButton(R.string.close, listener)
// AlertDialog erzeugen und zurückliefern
return builder.create()
}

companion object {
val TAG = AlertFragment::class.simpleName
}
}

Listing 5.20    Die Klasse »AlertFragment«

Auch sie leitet von androidx.fragment.app.DialogFragment ab, definiert in einem companion object eine Konstante TAG und implementiert die Methoden onAttach() und onCreateDialog(). Der AlertDialog wird mithilfe eines Builders erzeugt. Dieser sieht unter anderem das Setzen des Titels und einer Nachricht bzw. eines Hinweistextes vor. Mit builder.setPositiveButton() legen Sie fest, was beim Anklicken der »positiven«, bestätigenden Schaltfläche passiert. Eine »negative« Schaltfläche entspräche dann Verwerfen oder Abbruch. Die Referenz auf ein Objekt des Typs DialogInterface.OnClickListener wird analog zur Implementierung in DatePickerFragment in der Methode onAttach() gesetzt. Auch dieses Interface definiert genau eine Methode, nämlich onClick(). Wie die beiden Fragmente in der App genutzt werden, zeige ich Ihnen nun anhand der Klasse DialogDemoActivity:

package com.thomaskuenneth.androidbuch.dialogdemo

import android.app.DatePickerDialog
import android.content.DialogInterface
import android.os.Bundle
import android.widget.*
import androidx.appcompat.app.AppCompatActivity

class DialogDemoActivity : AppCompatActivity(),
DatePickerDialog.OnDateSetListener,
DialogInterface.OnClickListener {

private lateinit var datePickerFragment: DatePickerFragment
private lateinit var alertFragment: AlertFragment
private lateinit var textview: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textview = findViewById(R.id.textview)
// DatePicker
datePickerFragment = DatePickerFragment()
val buttonDatePicker = findViewById<Button>(R.id.button_datepicker)
buttonDatePicker.setOnClickListener {
datePickerFragment.show(supportFragmentManager,
DatePickerFragment.TAG)
}
// Alert
alertFragment = AlertFragment()
val buttonAlert = findViewById<Button>(R.id.button_alert)
buttonAlert.setOnClickListener {
alertFragment.show(supportFragmentManager,
AlertFragment.TAG)
}
}

override fun onDateSet(view: DatePicker?,
year: Int, monthOfYear: Int, dayOfMonth: Int) {
textview.text = getString(R.string.button_datepicker)
}

override fun onClick(dialog: DialogInterface?, which: Int) {
textview.text = getString(R.string.button_alert)
}
}

Listing 5.21    Die Klasse »DialogDemoActivity«

Die Klasse DialogDemoActivity leitet von AppCompatActivity ab. Sie implementiert die Interfaces DatePickerDialog.OnDateSetListener (hierzu gehört die Methode onDateSet()) und DialogInterface.OnClickListener (onClick()). In onCreate() wird, wie üblich, die Benutzeroberfläche geladen und angezeigt. Auch die beiden Fragmente DatePickerFragment und AlertFragment werden hier instanziiert. Ein Aufruf ihrer Methode show() zeigt sie an. Dies geschieht beim Anklicken der korrespondierenden Schaltfläche, wobei die Konstante TAG übergeben wird.

Vielleicht fragen Sie sich nun, warum die Activity zwei Interfaces implementiert, von denen jeweils eines in der Fragmentmethode onAttach() mit is geprüft wird. Dies geschieht ja, Sie erinnern sich, in AlertFragment und DatePickerFragment. In beiden Fällen möchte das Fragment eine Information an die Activity weitergeben, in die es eingebettet ist. Google empfiehlt für diese Art der Kommunikation die Definition eines Interface. Bei AlertDialog und DatePickerDialog bietet es sich an, die bereits vorhandenen Interfaces zu nutzen. Wenn Sie eigene Fragmente entwerfen, sollten Sie hierfür ein passendes Interface definieren.

Um Daten an ein bereits erzeugtes und in eine Activity eingebettetes Fragment zu übermitteln, definieren Sie ein Interface, welches das Fragment implementiert. Die Activity kann dann einfach die Methode dieses Interface aufrufen und die Parameter übergeben. Sie ermitteln ein eingebettetes Fragment mit supportFragmentManager.findFragmentById() oder findFragmentByTag(). Liefern diese Aufrufe null, erzeugen Sie das Fragment und übergeben die gewünschten Daten mit setArguments(). Das Fragment greift mit getArguments() auf sie zu. Kotlin-typisch geht beides auch einfach mit arguments.

 
Zum Seitenanfang

5.2.4    Menüs und Action Bar Zur vorigen ÜberschriftZur nächsten Überschrift

Menüs präsentieren dem Benutzer Funktionen und Aktionen, die er zu einem bestimmten Zeitpunkt ausführen kann. Klassische Desktop-Systeme kennen neben einer Menüleiste sogenannte Kontextmenüs, die geöffnet werden, wenn der Benutzer ein Objekt mit der rechten Maustaste anklickt.

Das Optionsmenü

Bis einschließlich Android 2.x wurde das sogenannte Optionsmenü durch Drücken einer speziellen Hardwaretaste geöffnet. Seitdem gewährt ein Symbol in der Action Bar Zugriff darauf. Das Optionsmenü ist prinzipiell mit einer klassischen Menüleiste vergleichbar, sollte aber weitaus weniger Elemente enthalten und nur solche Funktionen anbieten, die für die aktuelle Activity sinnvoll sind. Üblicherweise kann der Benutzer eine Einstellungsseite aufrufen oder sich Informationen über die App anzeigen lassen. Wenn eine Activity ein Optionsmenü anbieten möchte, muss sie die Methode onCreateOptionsMenu() überschreiben.

Die App »MenuDemo« mit geöffnetem Menü

Abbildung 5.11    Die App »MenuDemo« mit geöffnetem Menü

Eine typische Implementierung finden Sie in der Klasse MenuDemoActivity meiner Beispiel-App MenuDemo, die in Abbildung 5.11 dargestellt ist.

package com.thomaskuenneth.androidbuch.menudemo

import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MenuDemoActivity : AppCompatActivity() {

private lateinit var tv: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv = findViewById(R.id.textview)
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.optionsmenu, menu)
return super.onCreateOptionsMenu(menu)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.item1, R.id.item2, R.id.item3 -> {
tv.text = item.title
true
}
else -> super.onOptionsItemSelected(item)
}
}

override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
tv.text = getString(R.string.app_name)
return super.onPrepareOptionsMenu(menu)
}
}

Listing 5.22    Die Klasse »MenuDemoActivity«

Der Code in der Methode onCreateOptionsMenu() entfaltet mit einem MenuInflater (auf ihn verweist menuInflater der Elternklasse AppCompatActivity) ein Menü, dessen Elemente in der Datei optionsmenu.xml definiert wurden. Sie wird unter res/menu abgelegt und hat den folgenden Aufbau:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item1"
android:title="@string/item1" />
<item
android:id="@+id/item2"
android:title="@string/item2" />
<item
android:id="@+id/item3"
android:icon="@mipmap/ic_launcher"
android:title="@string/item3"
app:showAsAction="always" />
</menu>

Listing 5.23    Die Datei »optionsmenu.xml«

Elemente haben einen Titel, der üblicherweise in strings.xml eingetragen und über android:title="@string/..." referenziert wird. Damit Sie auf das Anklicken eines Menüeintrags reagieren können, sollten Sie jedem Element mit android:id eine ID zuweisen. Bitte achten Sie darauf, mit super.onCreateOptionsMenu() auch die Elternklasse aufzurufen, falls diese ebenfalls zum Menü beitragen möchte.

Die auszuführenden Aktionen implementieren Sie in der Methode onOptionsItemSelected(). Ihr wird das ausgewählte Menüelements als MenuItem-Instanz übergeben. Die Methode liefert true, wenn Ihre Activity auf das Anklicken dieses Elements reagiert hat. Andernfalls sollten Sie die Methode der Elternklasse aufrufen und deren Ergebnis zurückliefern. onCreateOptionsMenu() wird nur einmal aufgerufen. Das erzeugte Menü bleibt bis zur Zerstörung der Activity verfügbar. Wenn Sie Änderungen an Einträgen vornehmen möchten, können Sie die Methode onPrepareOptionsMenu() überschreiben. Android ruft sie jedes Mal vor dem Anzeigen des Optionsmenüs auf. Um das Erzeugen des Menüs zu erzwingen, rufen Sie invalidateOptionsMenu() auf.

Möchten Sie, dass wichtige Einträge ständig sichtbar sind, können Sie dies mit dem Ausdruck android:showAsAction="always" steuern. In diesem Fall erscheint ein Eintrag nicht innerhalb des Menüs, sondern als Icon in der Action Bar (zu ihr kommen wir ein bisschen später). Das müssen Sie natürlich mit android:icon auch setzen. Soll Android abhängig vom in der Action Bar verfügbaren Platz entscheiden, so verwenden Sie anstelle von always den Wert ifRoom.

Kontextmenüs

Android kennt das von Desktop-Systemen vertraute Konzept der Kontextmenüs. Statt eines Klicks auf die rechte Maustaste löst unter Android das lange Antippen (Tippen und Halten) eines Elements dieses Menü aus. Besonders gern werden ListViews mit Kontextmenüs versehen, aber auch andere Bedienelemente können mit solchen situationsbezogenen Menüs verknüpft werden. Nutzen Sie die Activity-Methode registerForContextMenu(), um eine View mit einem Menü zu koppeln. Der Bau der Menüs verläuft analog zu Optionsmenüs; nur die Methodennamen sind andere: Sie brauchen nur onCreateContextMenu() und onContextItemSelected() zu überschreiben und in der bereits bekannten Weise zu implementieren. Wie dies funktioniert, ist in der Klasse ContextMenuActivity (Listing 5.24) meiner Beispiel-App ContextMenuDemo zu sehen.

package com.thomaskuenneth.androidbuch.contextmenudemo

import android.os.Bundle
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.MenuItem
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity

class ContextMenuDemoActivity : AppCompatActivity() {

private lateinit var tv: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button = findViewById<Button>(R.id.button)
registerForContextMenu(button)
tv = findViewById(R.id.textview)
}

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

override fun onContextItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.item1, R.id.item2 -> {
tv.text = item.title
true
}
else -> super.onContextItemSelected(item)
}
}
}

Listing 5.24    Die Klasse »ContextMenuDemoActivity«

onCreate() lädt auf gewohnte Weise die Benutzeroberfläche und zeigt sie an. Die Methode registerForContextMenu() verbindet die Schaltfläche Tippen und halten mit dem Kontextmenü. In onCreateContextMenu() wird mit der MenuInflater-Instanz der Elternklasse AppCompatActivity (menuInflater) durch Aufruf der Methode inflate() das Menü aus der Datei contextmenu.xml entfaltet. Auch wenn sich das Kontextmenü auf ein Element Ihrer Benutzeroberfläche bezieht, sollten Sie mit super.onCreateContextMenu() die Elternimplementierung aufrufen. Der Methode onContextItemSelected() wird eine MenuItem-Instanz übergeben, die das ausgewählte Menüelement repräsentiert. Wenn Sie darauf reagieren, muss Ihre Implementierung true liefern, sonst den Rückgabewert der Elternmethode.

Nach dem Start der App öffnen Sie das Menü, indem Sie die Schaltfläche antippen und halten. Wenn Sie einen Menübefehl auswählen, erscheint dessen Name unterhalb der Schaltfläche. Die App mit geöffnetem Kontextmenü ist in Abbildung 5.12 zu sehen.

Die App »ContextMenuDemo«

Abbildung 5.12    Die App »ContextMenuDemo«

Schon seit Honeycomb gibt es die Action Bar am oberen Rand von Activities. Sie ersetzt die in ganz frühen Android-Versionen verwendete Titelzeile. Im Unterschied zu ihrem Vorgänger kann die Action Bar Funktionen übernehmen, die den Werkzeugleisten unter Windows, macOS und Linux ähneln. Außerdem gewährt sie ja den Zugang zum Optionsmenü.

Action Bar

Mein Beispiel ActionBarDemo zeigt, wie Sie die Funktionen der Action Bar nutzen. Die App ist in Abbildung 5.13 zu sehen. Bitte werfen Sie nun einen Blick auf die Klasse ActionBarDemoActivity in Listing 5.25.

Die App »ActionBarDemo«

Abbildung 5.13    Die App »ActionBarDemo«

Die Vorgehensweise zum Anzeigen eines Menüs kennen Sie bereits: Sie überschreiben die Methode onCreateOptionsMenu() und entfalten mithilfe eines MenuInflator die gewünschte XML-Datei. In meinem Beispiel ist dies menu.xml. Bitte denken Sie daran, mit super.onCreateOptionsMenu() auch die Elternimplementierung aufzurufen. In onOptionsItemSelected() reagieren Sie auf das Antippen eines Menübefehls. Meine Trivial-Implementierung übernimmt nur den Menütitel in ein Textfeld. Mit true signalisieren Sie, dass Ihr Code die Menübehandlung abgeschlossen hat. Wurde ein Ihnen unbekanntes Element ausgelöst, sollten Sie mit super.onOptionsItemSelected() die Elternimplementierung aufrufen. Mein Code spart sich das. Die Bedeutung von android.R.id.home erkläre ich gleich. Toast.makeText() zeigt ein kleines Infotäfelchen an. Weitere Informationen hierzu erhalten Sie in Abschnitt 5.3, »Nachrichten und Hinweise«.

package com.thomaskuenneth.androidbuch.actionbardemo

import android.os.Bundle
import android.view.*
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class ActionBarDemoActivity : AppCompatActivity() {

private lateinit var textview: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textview = findViewById(R.id.textview)
}

override fun onStart() {
super.onStart()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return super.onCreateOptionsMenu(menu)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (android.R.id.home == item.itemId) {
Toast.makeText(this, R.string.app_name,
Toast.LENGTH_LONG).show()
} else {
textview.text = item.title
}
return true
}
}

Listing 5.25    Die Klasse »ActionBarDemoActivity«

Ist Ihnen aufgefallen, dass das Heute-Symbol ständig angezeigt wird? Ich habe es, wie im vorherigen Abschnitt, mit android:showAsAction als sogenanntes Action Item definiert.

<item
android:id="@+id/menu_today"
android:icon="@android:drawable/ic_menu_today"
android:title="@string/today"
app:showAsAction="ifRoom|withText" />

Listing 5.26    Auszug aus »menu.xml«

Wie Sie wissen, legt der Wert ifRoom fest, dass der Befehl in das Menü ausgelagert werden kann, wenn nicht genügend Platz im permanent sichtbaren Bereich der Action Bar zur Verfügung steht. Beim Wert always unterbleibt dies. Durch das Entfernen von withText erscheint nur das Symbol ohne begleitende Beschriftung. Die Beschriftung eines Symbols wird auch bei vorhandenem withText weggelassen, wenn nicht genügend Platz zur Verfügung steht. Dies ist bei meinem Beispiel im Porträtmodus der Fall. Wenn Sie den Emulator oder ein reales Gerät in den Quermodus bringen, erscheint die Beschriftung.

[»]  Hinweis

Android stellt über @android:drawable etliche Icons zur Verfügung. Es ist verlockend, diese (wie in meinem Beispiel) in eigenen Apps zu verwenden. Google rät davon allerdings ab, weil diese möglicherweise nicht gut zum restlichen Look passen. Android Studio stellt im Asset Studio eine ganze Reihe von Material Design Icons zur Verfügung. Sie finden sie auch online unter https://material.io/resources/icons/?style=baseline.

Statt eines Action Items kann die Action Bar auch Widgets enthalten. Das kann praktisch sein, um beispielsweise ein Suchfeld im ständigen Zugriff zu haben. Solche Action Views werden mit dem Attribut android:actionViewLayout versehen, dessen Wert eine Layoutressource referenziert. Alternativ kann mit android:actionViewClass der Klassenname des zu verwendenden Widgets festgelegt werden. Damit das Element in der Action Bar erscheint, müssen Sie das Ihnen bereits bekannte Attribut android:showAsAction auf "ifRoom|collapseActionView" setzen. Steht nicht genügend Platz zur Verfügung, erscheint das Element im normalen Menü. In diesem Fall verhält es sich allerdings wie ein normales Menüelement, zeigt also kein Widget an.

Um die Action Bar zu konfigurieren, greifen Sie mit supportActionBar? auf ein Objekt des Typs androidx.appcompat.app.ActionBar zu. Da die Konfiguration frühzeitig erfolgen muss, bietet es sich an, wie in meinem Beispiel die Methode onStart() zu überschreiben. Bitte denken Sie aber daran, mit super.onStart() die Implementierung der Elternklasse aufzurufen. Für Apps mit verschachtelten Seiten oder festgelegten Activity-Folgen kann es sinnvoll sein, zur nächsthöheren Ebene zurückzugehen. Die ActionBar-Methode setDisplayHomeAsUpEnabled() zeigt einen kleinen nach links weisenden Pfeil an (true) oder blendet ihn aus (false). Um auf das Antippen zu reagieren, prüfen Sie in onOptionsItemSelected() einfach auf android.R.id.home == item.itemId. Die Navigation sieht dann so aus (ersetzen Sie StartActivity einfach durch den Namen der Activity, die aufgerufen werden soll):

val intent = Intent(this, StartActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)

Falls Ihre Activity keine Menüs verwendet, wäre es natürlich schade, nur für die Navigation die Methode onOptionsItemSelected() überschreiben zu müssen. In diesem Fall können Sie stattdessen in der Manifestdatei eine Activity als Elternaktivität kennzeichnen, die beim Anklicken des Pfeils aufgerufen wird. Fügen Sie dem <activity />-Tag einfach das mit API-Level 16 eingeführte Attribut android:parentActivityName hinzu, dessen Wert ein voll qualifizierter Klassenname ist.

 


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