Googles Android: Direktausdruck
Dieser Text ist sowohl unter der „Creative Commons Attribution/Share-Alike“-Lizenz 3.0 als auch GFDL lizenziert.
Eine deutschsprachige Beschreibung für Autoren und Weiternutzer findet man in den Nutzungsbedingungen der Wikimedia Foundation.
Vorwort
Einleitung
Eine Übersicht zu Android
Zurück zu Googles Android
Faszination Smartphone
Smartphones begeistern auch wenig technikaffine Menschen seit einigen Jahren. Die Kombination aus Telefonie, GPS, Internet und Multimedia-Inhalten bietet Entwicklern mit Phantasie ein breites Spektrum an Anwendungsmöglichkeiten. Kaum ein Tag vergeht, an dem nicht eine neue originelle App auf einem der virtuellen Marktplätze angeboten wird.
Derzeit ist Android, gefolgt von Apples iOS, das meistverwendete Betriebssystem auf Smartphones und Tablet-Computern. Da Android anders als vergleichbare Betriebsysteme offen ist, kann es auch auf andere Geräte portiert werden. In diesem Buch lernen Sie vieles zum Betriebssystem Android und Möglichkeiten zur Anwendungsentwicklung.
Linux Inside
Obwohl Android auf Linux 2.6 basiert, gibt es doch einige ganz erhebliche Unterschiede zu den klassischen Linux-Systemen. Für Systemaufrufe wird nicht wie sonst die Bibliothek glibc, sondern die androideigene Entwicklung bionic verwendet. Für diesen – auf den ersten Blick ungewöhnlichen Schritt – gibt es gute Gründe:
- Lizenz: glibc nutzt die GPL (GNU General Public License), bionic dagegen die BSD-Lizenz.
GPL erfordert das so genannte Copyleft und somit, dass jede Software, die glibc nutzt, selbst unter die GPL zu stellen ist. Diese Anforderung schränkt die kommerziellen Anpassungen von Android stark ein. Selbst wenn kommerzielle Interessen keine primäre Rolle spielen, kann man aus dem Quellcode unter Umständen auf interne Details des Gerätes schließen, die nicht öffentlich gemacht werden sollen.
- Größe: Die glibc benötigt etwa 400 kByte je Prozess, bionic dagegen gerade mal die Hälfte.
Ein Vorteil, der natürlich besonders auf mobilen Endgeräten mit ihren begrenzten Ressourcen zum Tragen kommt.
Darüber hinaus gilt bionic und insbesondere ihre Thread-Bibliothek als sehr schnell.
Java
Apps für Android werden in Java entwickelt. Das hat den großen Vorteil, dass so eine große Gruppe von Entwicklern nach einer Einarbeitung in das javabasierte Android-SDK schnell in der Lage ist, für die Plattform zu entwickeln. Mit Hilfe des Android NDK (Native Development Kit) kann aber auch nativer Code, also Code der mit etwa mit Hilfe eines C- oder C++-Compilers übersetzt wurde, integriert werden. Die Entwicklungsumgebung für Android Apps findet man auch in der folgenden Abbildung:
-
Ein Überblick über die Toolchain bei der App-Entwicklung
Wir sehen, dass die Java-Quellen in der Entwicklungsumgebung geschrieben und wie gewohnt in das class
-Format als Java-Bytecode übersetzt werden.
Anders als bei der Arbeit mit dem Java-SDK werden die class
-Dateien dann in das so genannte dex
-Format (Dalvik Executable) übersetzt.
Eine dex
-Datei kann dabei mehrere class
-Dateien umfassen.
Es handelt sich aber nicht nur um die Zusammenfassung mehrerer Dateien zu einer einzigen, sondern wirklich um die Übersetzung in neuen Programm-Code.
Doch dazu gleich mehr.
Die dex
-Datei wird mit weiteren Dateien, wie etwa dem Manifest, zur eigentlichen App zusammengefasst, deren Datei die Endung apk
erhält.
Diese apk
-Datei wird dann auf dem Gerät installiert.
Der dex
-Code aus der App wird dort von der Dalvik Virtual Machine (DVM) ausgeführt.
Die DVM wurde von Dan Bornstein bei Google entwickelt und eignet sich besonders als Laufzeitumgebung für Low-End-Geräte.
Sie basiert auf einer frei verfügbaren Java-Umgebung namens Apache Harmony und zeichnet sich durch einen geringen Bedarf an Arbeitsspeicher und Strom aus.
Da die DVM keinen Java-Byte-Code ausführt, ist sie keine JVM und fällt somit nicht unter Lizenzbestimmungen von Oracle.
Neben dem lizenzrechtlichen Vorteil ergibt sich aber auch ein erheblicher Performance-Unterschied:
Die JVM basiert auf dem Maschinenmodell der Kellermaschine.
Konkrete Computer sind aber Registermaschinen.
Die Bytecode-Anweisungen, die für eine Kellermaschine entwickelt wurden, müssen also zur Laufzeit in Anweisungen für Registermaschinen umgesetzt werden.
In der Mitte der 1990-er Jahre interpretierten die frühen JVM-Implementierungen den Bytecode und benötigten dazu viel Zeit, da die Anweisungen ja auf eine komplett andere Architektur umgesetzt werden mussten.
Heute wird Bytecode mit Hilfe eines Just-In-Time-Compilers (JIT-Compiler) zur Laufzeit direkt in den Maschinencode der Zielplattform übersetzt.
Das ist zwar schneller als die Interpretation und benötigt – anders als bei leistungsstarken Servern – doch einige der auf mobilen Endgeräten knappen Ressourcen.
Bei der Entwicklung unter Android wird der class
-Code bereits auf der Entwicklungsmaschine in dex
-Code übersetzt.
Dieser dex
-Code basiert auf Registermaschinen und kann von der DVM auf dem Endgerät zügig interpretiert werden.
Unter anderem versprach man sich von diesem Ansatz auch, keinen JIT-Compiler mehr zu benötigen, doch enthält die DVM seit Android 2.2 einen JIT-Compiler, was insbesondere bei dürftig ausgestatteten Geräten zu spürbaren Leistungssteigerungen geführt hat.
Die Sandbox
Zunächst erscheint es wieder merkwürdig, dass Android verschwenderisch mit System-Ressourcen umgeht. Jede App hat nämlich
- einen eigenen Prozess
- eine eigene DVM
- einen eigenen Heap
- ein eigenes Verzeichnis im Dateisystem
- einen eigenen Betriebssystem-User
Dadurch, dass wir jeder App diese eigene Umgebung zugestehen, erhalten wir mehr Sicherheit. Die strikte Trennung der Prozesse bewirkt, dass verschiedene Apps nicht auf den Arbeitsspeicher anderer Apps zugreifen können. Das Verzeichnis der Anwendung ist ebenfalls gegen fremde Zugriffe geschützt. Apps dürfen nur Ressourcen anderer Apps nutzen, wenn sie dazu berechtigt sind. Berechtigungen werden explizit in einer eigenen Datei, dem Manifest der App, angefordert und bereitgestellt.
Tatsächlich können eigene DVMs sogar ohne großen Aufwand bereitgestellt werden.
Es wird auf den expliziten Neustart einer DVM verzichtet.
Stattdessen gibt es einen Systemprozess namens zygote
, der mit Hilfe der Unix-Anweisung fork
dafür sorgt, dass jede App einen eigenen DVM-Prozess hat.
Außerdem sorgt zygote
dafür, dass möglichst viele Ressourcen geteilt werden.
Dies sorgt für einen raschen Start und eine ressourcenschonenden Betrieb der DVM.
Wer mehr über die Interna von Android wissen will sei auf einen Übersichtsartikel verwiesen.
TODO Referenz auf Fokusreport
Da Android mehrere Apps gleichzeitig betreiben kann, sind Engpässe im Hauptspeicher möglich, wenn zu viele Apps gestartet werden. Die Apps werden in einem Stapel verwaltet.
-
Der App-Stack
Das oberste Programm ist dabei sichtbar und aktiv. Mit dem Back-Button des Android-Gerätes wird eine Anwendung vom Stack aktiviert und die vormals aktive Anwendung auf den Stack gelegt. Sollte es zu Speicherengpässen kommen, kann Android Apps, die weiter unter auf dem Stapel liegen, beenden.
Das Manifest
Wir haben bereits gesehen, dass Android-Apps eine Manifest-Datei enthalten. Das Manifest ist ein XML-Dokument, in dem verschiedene Eigenschaften der App publiziert sind. Unter anderem sind dort auch die Rechte verzeichnet und somit die Ressourcen, die die App nutzen will. Bei der Installation wird der Anwender gefragt, ob er bereit ist, der App diese Rechte einzuräumen. Im folgenden Beispiel wird deklariert, dass die App GPS-Zugriff braucht:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"> </uses-permission>
Komponenten
Wegen des Sandbox-Prinzips kann keine App die öffentlichen Klassen einer anderen App nutzen. Auf den ersten Blick schränkt dies die Wiederverwendbarkeit unseres Codes stark ein. Tatsächlich bedient sich Android einer wesentlich robusteren Software-Architektur als der einfachen Verwendung von Java-Klassen. Die Klassenstruktur einer App ist vollständig gekapselt und somit für andere Apps eine Blackbox. Jede App kann aber Komponenten publizieren, die dann von anderen Apps nutzbar sind. Wir lösen uns so von den Implementierungsdetails unserer Java-Klassen und haben insgesamt weniger Abhängigkeiten. Der Entwurf und die Implementierung einer App können sich jetzt komplett ändern, solange nur die Schnittstelle der Komponenten erhalten bleibt.
Ein erheblicher Teil dieses Buches widmet sich der Entwicklung von Komponenten. Damit sind insbesondere gemeint
- Activities
- Services
- Broadcast-Receiver
- Content-Provider
Jeder dieser Komponentenarten ist jeweils mindestens ein Kapitel gewidmet. Wir geben daher an dieser Stelle nur einen kurzen Überblick
Activities
Activity
ist die Java-Klasse die zu jeder Form von GUI gehört.
Die Activity enthält die Logik, die zu einer GUI gehört.
Ein bekanntes Beispiel ist die Dialer-Activity aus der folgenden Abbildung.
-
Die Dialer-Activity
Services
Komplexe Aufgaben werden nicht im Hauptthread einer View ausgeführt. Bei Zeitüberschreitung einer Anwenderaktion kann Android die ganze App mit einer „Application Not Responding“-Meldung wegen Zeitüberschreitung abbrechen. Potenziell zeitaufwändige Operationen wie etwa Netzwerkzugriffe werden daher von Services in Threads oder eigenen DVMs bearbeitet. Nebenläufigkeit spielt bei Android eine bedeutende Rolle.
Broadcast-Receiver
Auf dem Gerät treten Ereignisse ein, die über einen so genannten Broadcast gemeldet werden. Beispiele für Broadcasts sind
- das Telefon klingelt
- eine SMS trifft ein
- die Akkuleistung lässt nach
Ausserdem können Apps auch eigene Broadcasts versenden. Mit Hilfe eines Broadcast-Receivers kann eine App Broadcasts mit Ereignissen „abonnieren“, die für die App wichtig sind.
Content-Provider
Content-Provider bieten ihren Nutzern eine SQL-ähnliche Syntax als Schnittstelle für Daten. Wie der Provider diese Schnittstelle implementiert und ob überhaupt eine Datenbank zum Einsatz kommt, ist dem Nutzer des Providers dabei nicht bekannt. Im folgenden Beispiel verschaffen wir uns Zugriff auf alle Lieder, die auf unserem Telefon gespeichert sind und die das Wort ‚Yesterday‘ in ihrem Titel tragen.
Uri from = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String artist = MediaStore.Audio.AudioColumns.ARTIST; String stream = MediaStore.Audio.Media.DATA; String title = MediaStore.Audio.Media.TITLE; String where = title + " like '%Yesterday%'"; Cursor songs = getContentResolver().query(from, new String[] { artist, stream, title }, where, null, null);
Einen ersten Eindruck über den Aufbau eines Content-Providers liefert die folgende Abbildung
-
Die Architektur eines Content-Providers
Wir sehen, dass die Komponenten einer App voneinander unabhängig sind.
Wie sie ihre Schnittstellen publizieren, lernen wir noch in einem späteren Kapitel.
Wir bezeichnen Komponenten auch als lose gekoppelt (loosely coupled).
Obwohl sich alles lokal auf einem kleinen mobilen Gerät abspielt, hat die Architektur einer App in Android Ähnlichkeit mit einem verteilten System.
Einzelnachweis
Einrichten der Programmierumgebung
Zurück zu Googles Android
Um Programme für Android zu entwickeln, benötigt man neben dem Android SDK auch eine entsprechende IDE. Die Entwickler von Android selbst empfehlen für diesen Zweck die Verwendung von Eclipse. Es gibt aber auch die von Google entwickelte IDE mit dem Namen Android Studio. Sie können sich entscheiden welche IDE sie benutzen, denn wir werden ihnen Tutorials zu beiden IDEs machen.
- Unterstützte Plattformen:
- Android Studio: Windows, Mac OSX
- Eclipse: Windows, Mac OSX, Linux
Damit der Einstieg in die Android-Entwicklung so angenehm wie möglich ist, gibt es an dieser Stelle Anleitung dazu, wie man Eclipse, das Android Studio, Java und das Android SDK auf seinem System einrichtet.
Java installieren
1. Stellen Sie sicher, dass Sie eine aktuelle Java-Version auf Ihrem Rechner installiert haben!
- Windows: Installieren Sie das Java JDK (Java Development Kit) in Version 5 oder 6 (6 wird empfohlen!).
- Mac OS X: Da Java auf dieser Plattform standardmäßig installiert ist, sollten Sie die Softwareaktualisierung starten, um sicher die neueste Version zu besitzen.
Eclipse installieren
2. Installieren Sie die Eclipse IDE in Version 3.5 (Galileo), 3.6 (Helios) oder 4.2 (Juno). Eclipse „Classic“ ist für die Android-Programmierung völlig ausreichend und wird auch für alle hier erstellten Beispielprogramme genutzt. Je nach Ihren Präferenzen und Erfahrungen können Sie aber auch eine der folgenden Versionen benutzen:
- Eclipse IDE for Java EE Developers
- Eclipse IDE for Java Developers oder
- Eclipse for RCP/Plug-in Developers.
3. Laden Sie das Android SDK Starter Package entsprechend Ihrem Betriebssystem herunter. Hinterlegen Sie das ZIP-File an einem sinnigen Verzeichnis und entpacken es dort.
4. Installieren Sie das ADT-Plugin für Eclipse. Öffnen Sie dazu in der Menüleiste 'Help' –> 'Install New Software' (Abbildung 1) und fügen Sie über 'Add…' ein neues Package hinzu (Abbildung 2). Als Namen verwenden Sie etwas sinniges wie „Android“ oder „ADT Plugin“ (aber das bleibt ihn überlassen). Für die Quelle geben Sie, wie in Abbildung 3, https://dl-ssl.google.com/android/eclipse/ oder http://dl-ssl.google.com/android/eclipse/ an (früher wurde die Quelle über 'https' oftmals nicht gefunden, über 'http' sollte das aber kein Problem sein).
-
1. Neue Software installieren
-
2. Neue Software hinzufügen
-
3. Angabe der Quelladresse für das ADT-Plugin
5. Jetzt müssen Sie Eclipse zeigen, wo es das in Schritt 3. heruntergeladen SDK finden kann. Geben Sie dazu den Pfad in den Android-Einstellungen an (Abbildung 4).
- Windows: Window –> Preferences –> Android –> Pfad hinzufügen –> Apply
- Mac OS X: Eclipse –> Preferences –> Android –> Pfad hinzufügen –> Apply
-
4. Angeben des Android-SDK-Pfades
6. Zu guter Letzt müssen Sie jetzt noch über den 'Android SDK and AVD Manager' (Abbildung 5) die Android-Versionen auswählen, für die Sie entwickeln wollen. Wir arbeiten mit Android 2.2 (alias Version 8 oder Froyo). Klicken Sie dazu in der Symbolleiste auf das Androidsymbol und gehen auf 'Available Packages'. Für den Anfang genügt es, wenn Sie folgende Packages installieren (siehe Abbildung 6):
- SDK Platform Android 2.2, API 8
- Documentation for Android SDK, API 8 und
- Android SDK Tool
-
5. Öffnen des Android-SDK-Managers
-
6. Auswahl der Android-Packages
Grundlegende Komponenten einer Android-Anwendung
Zurück zu Googles Android
Bevor wir jedoch anfangen, eigene Android-Anwendungen zu schreiben, möchte ich zunächst einen sehr kurzen Abriss über die Bestandteile geben, welche die Grundstruktur einer jeden Android-Anwendung ausmachen. Diese hier vorgestellten und auch viele weitere Elemente werden in späteren Kapitel näher betrachtet und mit eigenen Beispielen erklärt.
Activities
Meistens besteht eine Anwendung aus einer Verknüpfung mehrerer Activities, zwischen denen navigiert werden kann. Dabei kann man eine Activity mit der Nutzeroberfläche gleichsetzen, da sie auf die Eingaben des Nutzer reagiert und gegebenenfalls Ausgaben erzeugt, sprich sie übernimmt einen Großteil der Logik einer Anwendung. Für jede neue Nutzeroberfläche muss daher auch eine neue Activity erstellt werden. Damit auf Eingaben reagiert werden kann, wird jeder Activity eine View zugeordnet, welche die Informationen über die Bildinhalte besitzt.
Views
Views sind Elemente, die den Anwendern an der Oberfläche präsentiert werden. Eine grafische Darstellung ist in Android ein Baum von Views, das heißt ein oberstes View-Element enthält wahlweise andere Views. Diese können zum Beispiel Textfelder, Buttons, Bilder und viele mehr sein. Hauptsächlich werden alle grafischen Oberflächen in XML-Layoutfiles beschrieben, in denen die oben genannten Baumstruktur zum tragen kommt. Diese Layoutfiles werden zur Laufzeit einer Activity an diese gebunden und ihr Inhalt auf dem Bildschirm dargestellt. Finden kann man die Layoutfiles in den Application Ressources.
Android-Manifest XML
Das Manifest-File ist der Dreh- und Angelpunkt einer jeden Android-Anwendung. Sämtliche Metadaten einer Anwendung werden in XML-Syntax in diesem File hinterlegt. Solche Metadaten sind beispielsweise:
- Zugriffsrechte der Anwendung wie Zugang zum Internet, Schreiben/Lesen auf die SD-Karte, Auslesen von SMS und viele mehr
- Registrieren aller Activities der Anwendung und wie sich diese Verhalten
- Informationen und Restriktionen über unterstützte Android-Versionen
- und viele mehr …
Application Ressources
Neben den Java-Klassendateien, dem Manifest oder den Viewdateien etc., kommt es nicht selten vor, dass eine Android-Anwendung Dateien wie Bild-, Audio- und Stringdateien verwendet. Diese Ressourcen werden in den Application Ressources hinterlegt, von wo aus sie an jeder Stelle einer Anwendung erreichbar sind. Dateien die hier immer zu finden sein werden, sind die oben genannten Layoutfiles.
Ein Déjà-vu – Hello World
Zurück zu Googles Android
Nun, da wir die Entwicklungsumgebung installiert und konfiguriert haben und wissen, aus welchen Basiskomponenten eine Android-Anwendung besteht, wollen wir unsere erste eigene Anwendung erstellen. Und nichts würde sich besser dafür eignen, als eine „Hello World“-Anwendung zu schreiben.
1. Zunächst müssen wir dafür in Eclipse ein neues Android-Projekt erstellen.
Wie in Abbildung 1 gezeigt, geht das mit Hilfe von 'File –> New –> Project' oder über die Schaltfläche in der Symbolleiste. Indem sich öffnenden Dialog suchen wir uns unter Android das Android-Projekt aus und klicken auf 'Next'.
-
1. Öffnen eines neuen Projekts unter Eclipse
-
2. Wählen eines Android-Projekts
-
3. Initialisierungsdialog eines Android-Projekts
2. Jetzt müssen wir einige Voreinstellungen für unsere Anwendung vornehmen:
- Der Projektname: Naja, eigentlich selbsterklärend, aber unter diesem Namen erstellen wir unser Projekt und können es unter diesem auch in unserem Verzeichnis finden.
- Das Build-Target: Hier geben wir an, unter welcher Android-Version unsere Anwendung entwickelt wird. Dies kann später in den Projekteinstellungen geändert werden.
- Der Anwendungsname Der Name unserer Anwendung unter dem wir die App auch auf dem Android-Gerät finden.
- Der Package-Name: Alle Java-Klassen werden in einem gemeinsamen Package zusammengefasst.
- Optional besteht die Möglichkeit eine 'Start'-Activity zu erstellen, die beim Programmstart als erstes aufgerufen wird. Da wir mit interaktiven Apps arbeiten wollen, empfiehlt sich die Angabe eines Namens für die Start-Activity.
- Optional: ist auch die Angabe einer minimalen SDK Version.
Damit kann angegeben werden, welche Version das Betriebssystems ein Endgerät besitzen muss, damit es die Anwendung ausführen kann. Das ist an dieser Stelle aber noch nicht von Bedeutung.
Wie in dieser Anwendung die einzelnen Felder belegt sind, sehen wir in Abbildung 3.
Mit einem Click auf 'Finish' wird das Projekt erstellt.
3. Nachdem das Projekt erstellt wurde, kann, darf und sollte ein kurzer Blick in die Verzeichnisstruktur der Anwendung riskiert werden. Wie in der Abb. 4 zu sehen ist, kann man alle aus dem Kapitel Grundlegende Komponenten einer Android-Anwendung genannten Komponenten hier finden:
- Activity:
StartActivity.java
- Layout-Datei:
main.xml
- Manifest:
AndroidManifest.xml
-
4.Packagestruktur eines Android-Projekts
Im Grunde genommen haben wir bereits jetzt eine lauffähige Anwendung vor uns liegen. Um zu zeigen, dass dem auch so ist, werden wir das Programm einmal ausführen … doch Stopp!
Um eine Android-Anwendung auszuführen, benötigen wir mindestens eins von zwei Dingen:
- Wir haben entweder die Möglichkeit unser Programm direkt auf einem androidfähigen Gerät zu starten, das mindestens die Betriebssystemversion unterstützt, in der wir das Programm geschrieben haben,
- Wir nutzen einen im SDK mitgelieferten als „Android Virtual Device“ (AVD) bezeichneten Emulator. Auch wenn Sie bereits ein Android-Smartphone besitzen, werden wir nun so ein AVD einrichten.
4. Dazu öffnen wir wieder den „Android SDK und AVD Manager“ (siehe Abbildung 5).
Wir sollten uns bereits jetzt im Dialog für 'Virtual Devices' befinden (wenn dem nicht so ist, bitte dorthin wechseln). Mit einem Klick auf 'New' legen wir ein neues AVD an und können es konfigurieren. Momentan ist es für uns nur wichtig, dem AVD im Konfigurationsdialog einen Namen und seine Betriebssystemversion mitzuteilen. Die anderen Eigenschaften werden wir in einem späteren Kapitel behandeln. Mit einem Click auf 'Create AVD' besitzen nun unser eigenes virtuelles Entwicklungsgerät.
-
5. AVD Manager
-
6. Initialisierungsdialog für neue AVDs
5. Wir haben unsere Anwendung und unser AVD. Also bringen wir den Stein ins Rollen: Mit Rechtsklick auf das Projekt –> Run As –> Android Application (siehe Abb. 7) wird das Programm gestartet. Da wir zurzeit nur ein virtuelles Device nutzen, wird die Anwendung automatisch auf diesem ausgeführt. An dieser Stelle sei gesagt, dass, egal wie schnell ihr Computer ist, das Hochfahren eines AVD immer eine gefühlte Ewigkeit dauert! (In dieser Zeit können Sie sich ruhig einen Kaffee holen :) ). Irgendwann sollte ein Fenster erscheinen, das wie in Abbildung 8 aussieht.
Sobald das Device hochgefahren ist, erscheint in den meisten Fällen zugleich auch die von uns gestartete Anwendung, wie es auch in Abb. 9 der Fall ist. Wie zu sehen ist, steht auf dem Bildschirm schon ein kleiner Satz, der sich aus dem Projektnamen und dem Namen unserer Start-Activity zusammensetzt. Jede „Start“-Activity, die wir beim Erstellen eines neuen Android-Projektes mit erstellen lassen ist so aufgebaut. Sicherlich (und auch hoffentlich), stellt sich Ihnen jetzt die Frage wie dieser kleine Satz auf den Bildschirm kommt. Da bietet es sich an, einmal den Zusammenhang und die Zusammenarbeit zwischen Activity, Layouts und Ressourcen zu betrachten.
-
7. Start des Android-Projekts
-
8. Homescreen eines Android-Emulators
-
9. „Hello World“ im Android-Emulator
- StartActivity.java:
package com.jami.hw; import android.app.Activity; import android.os.Bundle; public class StartActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
Die Activity in diesem Beispiel macht zwar noch nicht viel her, die wichtigsten Funktionen sind dennoch zu sehen. In jeder Activity kommt die Methode onCreate
vor, die jedes Mal dann aufgerufen wird, wenn eine Activity neu „erschaffen“ wird. Es gibt häufig auch die Situation, dass eine bereits bestehende Activity neu aufgerufen wird. In diesem Fall wird nicht die onCreate
- sondern die onRestart
- bzw. onStart
-Methode genutzt. Aber zum Lifecycle einer Activity und aller damit einhergehenden Methoden kommen wir noch im Kapitel [[../_Activities|Activities]] zu sprechen. Wie gesagt wird onCreate
nur beim Initialisieren der Activity genutzt. Dabei sollte hier die Zuweisung eines Layouts, sprich der UI der Activity, erfolgen. Dies wird durch die Methode setContentView
erreicht, der als Parameter eine Layout-Ressource mitgegeben wird. In unserem Beispiel ist dies die Ressourcen-Datei main.xml
, die sich im Ordner res/layout befindet. Da unsere Anwendung nur einen einfachen Text ausgeben soll, wird passiert in dieser Activity nichts weiter.
- main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello"/> </LinearLayout>
Diese XML-Datei beschreibt, wie die Oberfläche einer Activity aufgebaut ist. Als „Rahmen“ benötigt man dazu als erstes ein Grundlayout.
Wir fangen mit dem einfachen linearen Layout an.
Welche weiteren Typen es gibt und worin sich diese Layouts unterscheiden, wird in einem späteren Kapitel behandelt. In diese Layouts werden dann, je nachdem was man darstellen möchte, weitere View-Elemente eingefügt.
In unserem Fall möchten wir einen Text darstellen, also verwenden wir hier eine View vom Typ TextView
. Es gibt auch noch weitere Elemente, die zum Beispiel zur Darstellung von Buttons, Eingabefeldern oder Bildern genutzt werden. Aber auch diese werden explizit in einem eigenen Kapitel behandelt.
Um den Text zu setzen, der später auf dem Bildschirm angezeigt werden soll, muss die TextView
auch wissen, welchen Text er anzuzeigen hat.
Dafür gibt es zwei Möglichkeiten:
- Man kann dem Attribut
android:text
derTextView
den anzuzeigenden Text direkt übergeben oder - Man kann ihm eine Referenz auf einen Eintrag in einer Ressourcen-Datei mitgeben.
In diesem Beispiel erhält die TextView
eine Referenz auf den String namens 'hello', wie er in der Datei strings.xml
definiert wurde. Wenn man den String direkt übergeben will, setzt man das text
-Attribut in der Layout-Defintion wie folgt:
android:text="Hello World, StartActivity!"
- strings.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, StartActivity!</string> <string name="app_name">HelloWorld</string> </resources>
Im Grunde genommen ist die Datei strings.xml
nichts weiter als eine Ansammlung von Strings, die durch diese Ressourcen-Datei überall in der Anwendung verfügbar sind.
Ich gratuliere Ihnen! Sie haben Ihr erstes Android-Programm geschrieben, auch wenn Sie an dieser Stelle nicht wirklich etwas programmiert haben … aber das werden wir schon bald ändern.
Ich (nicht der Autor) habe gerade mal (23. 8. 2023) versucht, das Beispiel mit einem aktuellen Android zum Laufen zu bekommen - es hat geklappt. Wenn man mit Eclipse arbeitet, muss man wahrscheinlich gar nichts ändern. Ich bevorzuge aber die Nutzung von Befehlen in der Konsole. Da geht es wie folgt:
Die benötigten Dateien sind:
./AndroidManifest.xml ./res/layout/main.xml ./res/values/strings.xml ./src/com/jami/hw/StartActivity.java
Außer der ersten sind diese oben abgebildet. Die erste lautet wie folgt:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jami.hw"> <uses-sdk android:targetSdkVersion="33" /> <application android:label="HelloWorld"> <activity android:name=".StartActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Hier muss man evtl. die sdk-Version anpassen.
Mit folgenden Befehlen wird das ganze dann kompiliert (der Pfad zu android.jar muss ggfs. angepasst werden):
Datei R.java erzeugen:
aapt package -M AndroidManifest.xml -I ~/.android_sdk/platforms/android-33/android.jar -S res/ -v -m -J src/
Java-Dateien kompilieren:
javac -source 9 -target 9 -d obj/ -cp ~/.android_sdk/platforms/android-33/android.jar -sourcepath src/ src/com/jami/hw/*.java
Class-Dateien in dex-Format umwandeln (früher ging das mit dx, jetzt mit d8):
d8 --lib ~/.android_sdk/platforms/android-33/android.jar --output bin/ obj/com/jami/hw/*.class
In apk-Datei packen. Diese ist noch nicht signiert.
aapt package -M AndroidManifest.xml -I ~/.android_sdk/platforms/android-33/android.jar -S res/ -v -F bin/HelloWorld.unsigned.apk bin
Die Daten in der Datei müssen auf 4 Bytes aligniert werden:
zipalign -f -p -v 4 bin/HelloWorld.unsigned.apk bin/HelloWorld.aligned.apk
Dann muss das ganze signiert werden, dafür ist ein keystore notwendig, siehe unten.
apksigner sign -verbose -ks HelloWorld.keystore --out bin/HelloWorld.signed.apk bin/HelloWorld.aligned.apk
Und zuletzt wird die App auf dem Smartphone installiert. (Dafür muss sich das Smartphone im Debug-Modus befinden. Wie das geht ist für jedes Smartphone anders -> Im Netz suchen...)
adb install bin/HelloWorld.signed.apk
Das war's (wenn alle Schritte geklappt haben...)
Zum Signieren wird ein keystore benötigt. Denn kann man mit folgendem Befehl erstellen:
keytool -genkey -validity 10000 -dname "CN=AndroidDebug, O=Android, C=US" -keystore HelloWorld.keystore -storepass android -keypass android -alias androiddebugkey -keyalg RSA -keysize 2048
Beim Signieren wird man nach dem Passwort gefragt. Das lautet "android". Kann man aber natürlich auch was anderes wählen...
Androids Innenleben
Activities
Zurück zu Googles Android
Der Vielfalt der Programmiersprachen und Frameworks entsprechend gibt es viele Möglichkeiten, um graphische Benutzerschnittstellen (GUIs) zu entwickeln. Wenn wir in Java mit den Swing-Klassen arbeiten, definieren wir beispielsweise Container-Klassen, in die wir dann Steuerelemente (Widgets) wie Buttons oder Eingabefelder einfügen. Die Steuerelemente können dabei selbst Container sein, die andere Steuerelemente enthalten. Auf diese Weise ergibt sich eine hierarchische Struktur. Im .NET-Framework gibt es seit der Version 3.0 die Windows Presentation Foundation (WPF). Hier können die Steuerelemente in XML-Dokumenten hierarchisch strukturiert werden. Die eigentliche Logik, die die Interaktion mit dem Anwender steuert, wird dann in zusätzlichem Programmcode definiert. Auf diese Weise erhält man eine Trennung zwischen Präsentation und Anwendungslogik.
Activities brauchen Views
Die Android-API enthält nicht die Swing-Klassen aus dem Java-SDK. In Android-Anwendungen werden GUIs nach einem ähnlichen Konzept wie in der WPF definiert.
Die Darstellung der GUI wird in XML-Dateien durchgeführt; das hierarchische XML-Format trägt dabei der hierarchischen Struktur einer GUI besonders gut Rechnung.
In einer Android Anwendung können wir auf diese GUI in Form von Objekten vom Typ View zugreifen.
Diese View-Objekte reflektieren die Struktur und die Namensgebung, wie sie in der XML-Datei hinterlegt sind.
Ein View haben wir in der Datei main.xml
bereits im vorherigen Kapitel definiert.
Die eigentliche Anwendungslogik wird dann in Objekten vom Typ Activity hinterlegt. Eine Activity ist ein Objekt, das direkt an der Benutzerfront arbeitet:
- Sie ist mit einer View verbunden und kann so die Buttons, Eingabefelder, Checkboxen und was es sonst noch so in der GUI gibt steuern.
- Wenn ein Anwender sich entschließt mit der GUI zu interagieren, werden Events ausgelöst, auf die die Activity dann geeignet reagieren kann.
Grundsätzlich muss einer Activity nicht unbedingt eine View zugeordnet sein, doch verliert sie ohne View eigentlich ihre Existenzberechtigung.
Die Zuordnung geschieht mit der Methode setContentView
.
Diese Vorgehensweise haben wir auch bereits im vorhergehenden [[../_Ein Déjà-vu – Hello World|Kapitel]] gesehen.
Die Standardanwendung, die automatisch mit jedem Android-Projekt erzeugt wird, enthält eine Klasse, die von Activity
erbt:
public class StartActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
Wir werden im Laufe des Kapitels noch die Bedeutung der Methode onCreate
kennenlernen. Uns interessiert zunächst nur ihr Inhalt:
Im wesentlichen enthält sie den Aufruf von setContentView
.
Damit wird der Activity die View zugewiesen, die in main.xml
definiert wurde.
Die View ist dabei eine Ressource ähnlich wie ein Text aus der Datei strings.xml
(siehe auch [[../_Ein Déjà-vu – Hello World|Ein Déjà-vu – Hello World]]).
Wir wollen hier Objekte vom Typ Activity
[1] genauer unter die Lupe nehmen, die Beschreibung von Views in XML-Dateien ist Gegenstand eines eigenen Kapitels.
Da GUIs so ganz ohne Views ziemlich langweilig sind, werfen wir noch einmal einen Blick auf die Standard-View aus dem vorherigen Kapitel.
Dort ist eine TextView enthalten, die für die Darstellung eines einfachen Textes verantwortlich ist:
<TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" />
Zwar gibt es auch eine korrespondierende Java-Klasse namens TextView
,[2] doch haben wir keine Möglichkeit an die Objektdarstellung der View zu kommen.
Daher können wir den Text der View nicht programmgesteuert in unserer onCreate
-Methode[3] anpassen.
Um mit dem Objekt zu arbeiten, das zu dieser View gehört, müssen wir der View einen Namen geben:
<TextView android:id="@+id/simpletv" android:layout_width="fill_parent" android:layout_height="wrap_content" />
Ähnlich wie bei den Texten im letzten Kapitel sorgt das Android-Plugin der Eclipse-IDE dafür, dass wir jetzt in der Klasse R.id
ein ganzzahliges Attribut simpletv
haben, das unsere TextView eindeutig identifiziert.
Zwar ist das Textfeld noch leer, doch können wir die View mit ihrer id und der Methode findViewById
[4] im Universum der Objekte unserer Android-Anwendung wiederfinden:
TextView message = (TextView) findViewById(R.id.simpletv);
Die Methoden der Klasse TextView
– eine Unterklasse des allgemeinen Typs View
[5] – verwenden wir jetzt für die Steuerung.
Den Text setzen wir wie folgt:
message.setText("Hello Android");
Die Interaktion mit dem Anwender
Zur Übung wollen wir zu unserer View noch ein Eingabefeld (Typ EditText
)[6] und einen Button (Typ Button
)[7] hinzufügen.
Die Steuerelemente bekommen die id word
bzw. upper
; der Button soll programmgesteuert mit dem Text „To Upper“ beschriftet werden.
Wer etwa Geduld hat und experimentierfreudig ist, kann sich etwa mit Hilfe der Dokumentation zur API an dieser kleine Aufgabe versuchen.
Wer ungeduldiger ist, fügt in main.xml
vor der Definition von firsttv
die folgenden Zeilen ein:
<EditText android:id="@+id/word" android:layout_width="fill_parent" android:layout_height="wrap_content" android:inputType="text" /> <Button android:id="@+id/upper" android:layout_height="wrap_content" android:layout_width="wrap_content" />
Der zugehörige Java-Code sieht wie folgt aus:
TextView view=(TextView)findViewById(R.id.firsttv); EditText edit=(EditText)findViewById(R.id.word); Button upper = (Button)findViewById(R.id.upper); upper.setText("To Upper");
Die App sollte im Emulator ungefähr so aussehen:
-
Die Activity aus dem Beispiel
Wenn wir diese App starten, tut sich natürlich nichts, wenn wir den Button anklicken.
Sein Click-Event wird zwar ausgelöst, läuft aber ins Leere.
Wie bei der ereignisgesteuerten Programmierung üblich, können wir unserem Button einen Listener zuordnen.
Dazu enthält der Typ View
ein Interface namens OnClickListener
[8] mit einer Methode onClick
.[9]
Dieses Interface implementieren wir mit einer anonymen inneren Klasse.
Anonyme Klassen werden in Android-Anwendungen oft gebraucht und wer sie noch nicht kennt, sollte das schnell nachholen.
private TextView view; private EditText edit; private Button upper; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); view = (TextView) findViewById(R.id.simpletv); edit = (EditText) findViewById(R.id.word); upper = (Button) findViewById(R.id.upper); upper.setText("To Upper"); upper.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { String result=edit.getText().toString().toUpperCase(); view.setText(result); } }); }
Die Organisation des Codes hat sich etwas geändert: Die Views werden nicht mehr in lokalen Variablen, sondern in Attributen gehalten.
So können wir sie in allen Methoden unserer Klasse StartActivity
nutzen.
Wirklich neu sind nur die letzten Zeilen der Methode. Wir sehen, dass ein Objekt vom Typ OnClickListener
anonym erzeugt und dem Button mit Hilfe seiner Methode setOnClickListener
[10] zugeordnet wird.
Die Funktionalität ist einfach:
Der Inhalt des Eingabefeldes wird ausgelesen und in ein Objekt vom Typ String
transformiert.
Dieser Text wird dann in Großbuchstaben umgewandelt und der Variablen result
zugewiesen.
Anschließend wird der Inhalt von result
in unserer TextView dargestellt.
Das ist sicher keine spannende Anwendung, doch haben wir so bereits einige wesentliche Aspekte bei der Arbeit mit Activities kennen gelernt.
- Die Zuordnung einer View.
- Der Zugriff auf die Objekte, die in der View enthalten sind.
- Die Arbeit mit Widgets, die alle durch jeweils eine View repräsentiert werden.
- Die Verarbeitung von Anwendereingaben mit Hilfe von Listenern.
> Activities sind meldepflichtig <
Zwar werden wir die Manifest-Datei, die ja zu jeder Android-Anwendung gehört noch in einem eigenen [[../_Das Manifest|Kapitel]] kennen lernen, doch sollten wir bereits jetzt wissen, dass dort auch alle Activities unserer Anwendung verzeichnet sein müssen.
Wenn wir einen Blick auf die Datei AndroidManifest.xml
werfen, sehen wir, dass Eclipse das in unserem Fall bereits erledigt hat:
<application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".StartActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
Ein Teil der Angaben ist selbsterklärend, einen anderen Teil wie intent-filter
lernen wir noch kennen, wenn es um [[../_Das Manifest|Manifeste]] oder [[../_Intents oder "Ich hätte gern den Zucker"|Intents]] geht.
Menüs
Der meiste Code, der in einer Activitiy anfällt, hat zwar mit Views zu tun, doch gibt es auch Eigenschaften einer Activity, die unabhängig von Views sind. So können wir in einer Activity beispielsweise Code hinterlegen, der ausgeführt wird, wenn die Akkuleistung unseres Gerätes nachlässt. Ebenso können Ereignisse abgefangen werden, die ausgelöst werden, wenn bestimmte Tasten des Gerätes gedrückt werden.
Exemplarisch behandeln wir den Fall, dass ein Anwender die Menü-Taste seines Gerätes drückt.
In diesem Fall wird das so genannte Kontext-Menü angezeigt, sofern wir eines definiert haben.
Dafür ist die Methode onCreateOptionsMenu
[11] zuständig, die wir wie folgt überschreiben wollen:
private static final int BOLD_MENU = Menu.FIRST; @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(Menu.NONE, BOLD_MENU, Menu.NONE, "Large Font"); return super.onCreateOptionsMenu(menu); }
Der Name der Methode add
[12] deutet ja schon sehr darauf hin, dass wir hier einen Menüeintrag hinzufügen.
Er wird mit dem Text „Large Font“ beschriftet und soll später die Möglichkeit bieten, den Inhalt der TextView vergrößert darzustellen.
Der erste Parameter von add
kann genutzt werden, um Einträge zu gruppieren, der dritte um den Eintrag in eine Reihenfolge einzuordnen.
Wer sich dafür interessiert, sollte mal einige Einträge hinzufügen und diese Parameter etwa variieren.
Wir haben es uns hier leicht gemacht: Gruppierung und Reihenfolge sind uns in diesem Beispiel egal.
Die id des ersten (und einzigen) Menüeintrages merken wir uns in der Konstanten BOLD_MENU
.
Den zum Menü gehörenden Code definieren wir, indem wir die Methode onOptionsItemSelected
[13] überschreiben.
Typischerweise werden hier in einer switch/case-Anweisung die einzelnen Menüeinträge abgeklappert:
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case BOLD_MENU: view.setTextSize(21); default: } return super.onOptionsItemSelected(item); }
In unserem Fall ist das natürlich sehr einfach gehalten:
Wenn ein Anwender den Menüpunkt „Large Font“ auswählt wird die Größe des Textes einfach auf 21 erhöht.
Nachdem wir das einmal ausprobiert haben, schadet es sicher nicht dieses rudimentäre Menü unter Verwendung der API-Dokumentation des Typen Menu
selbstständig zu erweitern.
Toast
Views, wie wir sie hier kennengelernt haben, sind mit Abstand die gebräuchlichsten Instrumente, um mit Anwendern zu interagieren. Wir lernen jetzt, dass wir unabhängig von XML-Dateien etwa kleine Textfelder anzeigen können.
-
Beispiel für eine Toast-Nachricht
Tatsächlich bedürfen diese Objekte vom Typ Toast
[14] keiner Layoutdateien, sie brauchen noch nicht einmal eine Activitiy.
Um ein Ergebnis zu sehen, reicht es, wenn wir den folgenden Code etwa an das Ende unserer onCreate
-Methode schreiben.
Toast toast=Toast.makeText(this, "in onCreate", Toast.LENGTH_LONG); toast.show();
Der erste Parameter, den die Factory-Methode makeText
[15] braucht, ist vom Typ Context
[16]; einem der Basistypen von Activity
.
Da aber auch Typen wie Service
oder BroadcastReceiver
von Context
abgeleitet sind, ist der Einsatz einer Activity nicht zwingend erforderlich.
Für den dritten Parameter gibt es die beiden Möglichkeiten
für die Anzeigedauer der Textnachricht; angezeigt wird der Text aber erst nach Aufruf der Methode show
.
Das Leben einer Activity
Wir benutzen den Typ Toast jetzt, um einige grundlegende Erfahrungen mit Activities in Android zu sammeln. Jede Activity hat einen der folgenden Zustände:
- aktiv und im Vordergrund: Dies ist der Zustand, in dem wir unsere Beispiel-Activity immer erlebt haben.
- passiv: Die Activity ist nicht mehr sichtbar und eine andere Anwendung steht im Vordergrund. Dies passiert etwa, wenn wir in unserer Anwendung mit mehreren Activities arbeiten und zwischen diesen hin und her navigieren. Um mit mehreren Activities zu arbeiten benötigen wir noch mehr Wissen über Intents, das wir aber erst in einem eigenen [[../_Intents oder "Ich hätte gern den Zucker"|Kapitel]] erwerben werden.
- aktiv, aber teilweise überdeckt: Zu den beiden extremen Zuständen 'aktiv' und 'passiv' gibt es noch ein Zwischenstadium. Die Activity steht nicht im Vordergrund und ist noch sichtbar, sie wird aber teilweise durch eine andere Activity überdeckt.
Activities wechseln in typischen Android-Use-Cases häufig ihren Zustand. Immer, wenn eine Zustandsänderung eintritt, wird mindestens eine der so genannten Lifecycle-Methoden aufgerufen.
Die erste dieser Methoden kennen wir bereits: Sie wird bei der Erzeugung der Activity aufgerufen; die Methoden onStart
und onRestart
immer dann, wenn eine nicht sichtbare Activity in den Vordergrund wechselt. Wechselt die Activity in den Hintergrund wird ihre onPause
-Methode ausgeführt und onStop
zusätzlich dann, wenn sie nicht mehr sichtbar ist. Bei jedem Wechsel in den Vordergrund wird onResume
aufgerufen.
Wenn die Activitiy zerstört wird, wird onDestroy
ausgelöst.
Es ist sehr interessant diesen Zustandswechsel zu beobachten.
Dazu überschreiben wir in unserer Klasse StartActivity
jede der sieben Lifecycle-Methoden so, dass sie einen Toast anzeigt.
Für die Methode onCreate
haben wir das schon gemacht. Dabei dürfen wir nicht vergessen, dass die überschriebenen Lifecycle-Methoden immer auch ihre Implementierung in der Basisklasse aufrufen muss. Diesem Umstand haben wir beim Überschreiben von onCreate
bereits mit der Zeile
super.onCreate(savedInstanceState);
Rechnung getragen.
Wenn wir diese App laufen lassen, sehen wir beispielsweise auch, dass auch nach dem Betätigen der Return-Taste am Gerät, die Activity angehalten wird, und somit onPause
und onStop
aufgerufen werden.
Die Activity wird aber nicht beendet, sondern läuft im Hintergrund weiter.
Auf den Aufruf von onDestroy
warten wir im Normalfall vergebens.
Wenn das System allerdings eine Engpass mit seinem Speicher hat, kann es passieren, dass eine Activity über die Klinge springen muss.
Zunächst werden natürlich passive Activities zerstört und dann erst teilweise sichtbare; in keinem Fall wird aber die Activity im Vordergrund kassiert. Dieses Verfahren hält möglichst viele Activities am Leben und erspart uns so deren häufige Erzeugung und somit einige Laufzeit. Nur Activities, die zerstört werden, müssen neu erzeugt werden und durchlaufen dann wieder onCreate
.
Zustand retten
Da bei der Zerstörung eines Objektes auch sein ganzer Zustand verloren geht, kann es durchaus sinnvoll sein, diesen Zustand so abzuspeichern, dass er im Fall der Vernichtung beim nächsten Aufruf von onCreate
wiederhergestellt werden kann.
Dafür bietet die Android-API den Typ SharedPreferences
:[26]
In onPause
können mit diesem Typ Werte einiger wichtiger Attribute abgelegt und in onCreate
wieder ausgelesen werden. Am Namen SharedPreferences
erkennt man ja bereits, dass dieser Typ eigentlich zum Speichern von Einstellungen gemacht ist.
Wir wollen uns seine Funktionsweise aber in einem anderen Zusammenhang klarmachen: Wenn ein Anwender in unserer Beispielanwendung einen Text eingibt, soll der Text erhalten bleiben und nach dem Wiederbeleben der Activity wieder angezeigt werden, selbst wenn die Activity zerstört oder das Gerät abgeschaltet wird. Wir vereinbaren dazu ein private Attribut
private SharedPreferences prefs;
Das Attribut initialisieren wir in der onCreate-Methode . Dann schauen wir nach, ob in prefs
ein Text aus dem früheren Leben der Activity erhalten ist. Diesen Text weisen wir dann dem Eingabefeld edit
zu:
edit = (EditText) findViewById(R.id.word); prefs = getSharedPreferences("StartActivityPrefs", MODE_PRIVATE ); String userText = prefs.getString("userText",""); edit.setText(userText);
Die Activity ist immer dann zerstörungsgefährdet, wenn die Methode onPause
aufgerufen wird.
Daher ist diese Methode auch der richtige Ort um alles zu retten, was wichtig ist:
@Override public void onPause(){ super.onPause(); SharedPreferences.Editor editor = prefs.edit(); editor.putString("userText", edit.getText().toString()); editor.commit(); }
Dieser kurze Streifzug durch die Klasse Activity
gibt uns einige Eindrücke über die Möglichkeiten dieses Typs. Selbstverständlich hat Activity
noch weitere Fähigkeiten, die man ruhig einmal mit Hilfe der zugehörigen API-Dokumentation ausprobieren sollte.
Einzelnachweise
- ↑ http://developer.android.com/reference/android/app/Activity.html
- ↑ http://developer.android.com/reference/android/widget/TextView.html
- ↑ http://developer.android.com/reference/android/app/Activity.html#onCreate(android.os.Bundle)
- ↑ http://developer.android.com/reference/android/app/Activity.html#findViewById(int)
- ↑ http://developer.android.com/reference/android/view/View.html
- ↑ http://developer.android.com/reference/android/widget/EditText.html
- ↑ http://developer.android.com/reference/android/widget/Button.html
- ↑ http://developer.android.com/reference/android/view/View.OnClickListener.html
- ↑ http://developer.android.com/reference/android/view/View.OnClickListener.html#onClick(android.view.View)
- ↑ http://developer.android.com/reference/android/view/View.html#setOnClickListener(android.view.View.OnClickListener)
- ↑ http://developer.android.com/reference/android/app/Activity.html#onCreateOptionsMenu(android.view.Menu)
- ↑ http://developer.android.com/reference/android/view/Menu.html#add(int,%20int,%20int,%20java.lang.CharSequence)
- ↑ http://developer.android.com/reference/android/app/Activity.html#onOptionsItemSelected(android.view.MenuItem)
- ↑ http://developer.android.com/reference/android/widget/Toast.html
- ↑ http://developer.android.com/reference/android/widget/Toast.html#makeText(android.content.Context,%20java.lang.CharSequence,%20int)
- ↑ http://developer.android.com/reference/android/content/Context.html
- ↑ http://developer.android.com/reference/android/widget/Toast.html#LENGTH_LONG
- ↑ http://developer.android.com/reference/android/widget/Toast.html#LENGTH_SHORT
- ↑ http://developer.android.com/reference/android/app/Activity.html#onCreate(android.os.Bundle)
- ↑ http://developer.android.com/reference/android/app/Activity.html#onStart()
- ↑ http://developer.android.com/reference/android/app/Activity.html#onRestart()
- ↑ http://developer.android.com/reference/android/app/Activity.html#onResume()
- ↑ http://developer.android.com/reference/android/app/Activity.html#onPause()
- ↑ http://developer.android.com/reference/android/app/Activity.html#onStop()
- ↑ http://developer.android.com/reference/android/app/Activity.html#onDestroy()
- ↑ http://developer.android.com/reference/android/content/SharedPreferences.html
Intents oder "Ich hätte gern den Zucker"
Zurück zu Googles Android
Allgemein
Mittlerweile kennen wir Activities, ihren Aufbau und ihren Lebenszyklus. Aber mit einer Activity alleine kommt man in den meisten Anwendung eher nicht aus. Also, wie kommen wir nun von einer Activity zur nächsten? Die Antwort ist das Konzept von „Intents“. Kurz gesagt stellen Intents einen indirekten Nachrichtendienst dar, mit dem es möglich ist, Activities und Services aufzurufen und Broadcast-Receiver über Ereignisse zu informieren. Aber warum nun „indirekt“? Stellen Sie sich folgendes Szenario vor:
„Sie haben bei sich zu Hause eine Vorratskammer, in der Sie Gewürze, Mehl, Haushaltswaren usw. lagern. Nun brauchen Sie z. B. ein Paket Zucker. Was machen Sie? Sie gehen einfach in die Kammer und holen sich den Zucker. Genauso funktioniert das Android-Betriebssystem bzw. Intents nicht. Im Falle von Android wartet vor der Vorratskammer ein Butler, dem Sie sagen was Sie möchten. Dieser holt Ihnen den Zucker und übergibt Ihnen diesen.“
Was heißt das für uns? Wenn wir auf Kernelemente von Android (den Zucker) zugreifen wollen, müssen wir ein Intent-Objekt erstellen (den Butler), dieses mit Informationen befüllen (bitten den Zucker zu holen) und dieses an den Kontext weitergeben (in die Vorratskammer schicken).
Die gerade genannten Kernelemente sind
- die bereits bekannten [[../_Activities|Activities]],
- [[../_Services|Services]],
- [[../_BroadcastReceiver|Broadcast-Receiver]] und
- [[../_ContentProvider|Content-Provider]]
Der Kontext versucht, mit den im Intent enthaltenen Informationen, die gewünschte (explizite) oder eine passende (implizite) Komponente zu finden und dem Absender des Intents zukommen zu lassen. Von den drei möglichen Typen, die an einem Intent teilnehmen können, kennen wir bisher nur Activities. In den beiden folgenden Abschnitten werden wir uns anhand einiger beispielhafter Activities den Unterschied zwischen expliziten und impliziten Intents klarmachen. Damit haben wir Intents grundsätzlich verstanden. Aspekte von Intents, die für Services und Broadcast-Receiver spezifisch sind, besprechen wir in den jeweiligen Spezialkapiteln.
Die Android-Projekte, die wir im Kapitel [[../_Activities|Activities]] entwickelt haben, bestehen aus nur einer Activity. So etwas kommt auch in der Praxis vor, doch umfassen viele Apps mehrere Activities. In diesem Kapitel umfasst unser Projekt zwei Activities. Um diese Komponenten miteinander zu verbinden brauchen wir Intents.
Doch zunächst legen wir ein neues Android-Projekt an.
Wir nennen es IntentDemo
und lassen uns von Eclipse auch gleich eine Activity erzeugen, die wir PrimaryActivity
nennen wollen.
Explizite Intents
Wir duplizieren den Java-Code, der zu PrimaryActivity gehört, und nennen die Klasse SecondaryActivity
. Die erste Activity verzeichnet Eclipse für uns im Manifest, bei der zweiten müssen wir selbst Hand anlegen. Der application
-Teil des Manifestes sollte also etwa so aussehen:
<application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".PrimaryActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".SecondaryActivity" android:label="@string/app_name"> </activity> </application>
Da es in diesem Kapitel wirklich nur um die Wirkungsweise von Intents geht, brauchen wir kein kunstvolles Layout.
Durch umbenennen und kopieren des Standard-Layouts verschaffen wir uns zwei Dateien primary.xml
und secondary.xml
. Die zweite lassen wir wie sie ist, die erste ergänzen wir noch um einen Button:
<Button android:id="@+id/next" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="Next"/>
In die onCreate
-Methoden der beiden Activity-Klassen fügen wir noch – so wie wir es [[../_Activities|gelernt]] haben – Code ein, um die zugehörige Views zu laden. Den Listener für den Button definieren wir zwar, lassen ihn aber zunächst wirkungslos:
PrimaryActivity.onCreate
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.primary); Button next = (Button) findViewById(R.id.next); next.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { } }); }
SecondaryActivity.onCreate
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.secondary); }
Die beiden sehr einfachen Activities sehen wie folgt aus:
-
1.
PrimaryActivity
-
2.
SecondaryActivity
Beim Klick auf den mit „Next“ beschrifteten Button soll PrimaryActivity
in den Hintergrund und SecondaryActivity
in den Vordergrund wechseln. Und genau dafür brauchen wir Intents.
Wir definieren ein Objekt vom Typ Intent
[1] und teilen dem Kontext mit, dass SecondaryActivity
den Vordergrund übernehmen soll.
Den folgenden Code setzen wir also an das Ende von PrimaryActivity.onCreate
:
final Intent intent=new Intent(this, de.wikibooks.android.SecondaryActivity.class); next.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { startActivity(intent); } });
Wir beobachten, dass wir beim Erzeugen des Intents den Packagenamen und den kompletten Klassennamen der Ziel-Activity mitgegeben. Die Methode startActivity
[2] erbt die Klasse Activity
von Context
.
Sie macht das, was ihr Name verspricht und bringt uns zur SecondaryActivity
.
Wenn wir die App starten und „Next“ auslösen, wird anstandslos zur SecondaryActivity
gewechselt. Diese Art von Intent nennt sich explizit, weil wir unserem Butler sehr präzise gesagt haben, was wir brauchen. Also nicht nur einfach „Zucker“, sondern „Alnatura Rohrohrzucker“.
Diese Vorgehensweise findet man auch in anderen Java-Frameworks: es gibt Methoden, denen man den Namen einer Klasse voll qualifiziert übergibt und die sich dann um die Objekterzeugung kümmern. Und genau diese enge Koppelung von Klassen hat sich bei Änderungen als unflexibel erwiesen. Nachteile, die auf der Hand liegen, sind:
- Wenn wir in eine andere als die
SecondaryActivity
verzweigen wollen, müssen wir den Java-Code wieder anfassen. - Wir können nur Activities aus unserer eigenen Anwendung ansteuern.
- Die Activities (und natürlich die Services und Broadcast-Receiver) unserer Anwendung können nicht von anderen Anwendungen genutzt werden.
Dass es auch anders geht, sehen wir im folgenden Abschnitt.
Implizite Intents nutzen
Bei impliziten Intents werden Ross und Reiter nicht genannt, sondern nur beschrieben.
Wenn wir in der Klasse PrimaryActivity
die folgende Intent-Definition
final Intent intent=new Intent(this, de.wikibooks.android.SecondaryActivity.class);
austauschen durch
final Intent intent=new Intent(Intent.ACTION_DIAL);
dann landen wir, wenn wir den Next-Button betätigen, in der Dial-Activity des Android-Systems.
-
Die Dial-Activity
Wir kennen nicht den Namen der Klasse, der zu der im Intent hinterlegten Activity gehört.
Das brauchen wir auch nicht, da hinterlegt ist, dass sie auch über die sogenannte Action abgerufen werden kann.
Der Vorteil sollte klar sein: Wenn die Dialer-Activity durch einen anderen Dialer ausgetauscht werden soll, muss einfach nur die Zuordnung der Aktion zur Activity geändert werden. Wir werden noch sehen, dass dazu einfache Eingriffe in die Manifest Datei reichen. Es ist also nur eine einzige Änderung nötig, um alle ACTION_DIAL
-Intents[3] auf den neuen Dialer umzulenken.
Hinsichtlich der Zuordnung gelten die folgenden Regeln:
- Jede Action kann keiner oder mehreren Activities zugeordnet sein
- Jede Activity kann mehreren Aktionen zugeordnet sein.
Die Aktionen sind dabei einfache Texte, von denen sehr viele – wie auch ACTION_DIAL
– bereits als Konstante im Typ Intent
definiert wurden.
Wenn es zu einer Aktion keine passende Activity gibt, meldet das System einen Fehler. Das passiert etwa, wenn wir die Intent-Definition
final Intent intent=new Intent(Intent.ACTION_DIAL);
durch
final Intent intent=new Intent("ACTION_FEHLER");
ersetzen: Es gibt keine Activities mit der Aktion ACTION_FEHLER
.
Stehen mehrere Activities zur Auswahl überlässt Android dem Anwender die Auswahl.
So gibt es beispielsweise zur Action ACTION_VIEW
[4] viele Activities.
Wenn wir unseren Intent wie folgt definieren
final Intent intent=new Intent(Intent.ACTION_VIEW);
bekommen wir ein Angebot an Activities:
-
Keine eindeutige Ziel-Activity
Je nach Anwendung kann es wichtig sein, vor dem Start des Intents zu wissen, ob das Ziel wirklich eindeutig beschrieben ist.
Dazu können wir aber den Kontext befragen:
boolean isUnique(intent){ return 1==getPackageManager().queryIntentActivities(intent, PackageManager.GET_INTENT_FILTERS); }
Wie schon gesagt, kann es zu einer Action mehrere Activities geben. Um hier spezifischer zu werden, können wir bei der Definition der Intents noch eine URI übergeben. Beispiel für URIs sind die bekannten URLs wie
http://de.wikibooks.org/
aber auch weniger gängige Ausprägungen wie
tel:(+49) 7723 9200
- An dem Teil der URI, der vor dem Doppelpunkt steht, kann der Kontext auch erkennen, welche der
ACTION_VIEW
-Activities benötigt wird. - Der Teil, der dem Doppelpunkt folgt, kann dann als Daten verwendet werden.
Beim Start des Intents
final Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse("http://de.wikibooks.org/"));
wird also die Web-Browser-Activity angezeigt und die Seite „de.wikibooks.de“ geladen. Dagegen wird mit
final Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse("tel:(+49) 7723 9200"));
die Dialer-Activity gestartet und gleich die Nummer „(+49) 7723 920 0“ gewählt.
Da es sehr viele Aktionen gibt, können sie nicht alle aufgezählt werden.
Mehr Informationen und Beispiele gibt es in der API-Dokumentation zum Typ Intent
oder auch auf der Seite www.openintents.org.
Neben der Angabe einer Aktion können wir mit Hilfe der Methode addCategory
[5] auch eine oder mehrere Kategorien angeben, denen die Activity zugeordnet sind. Auf diese Weise können wir die Anzahl der in Frage kommenden Ziele weiter einschränken und auch an unsere Anforderungen anpassen.
- So kennzeichnet
Intent.CATEGORY_LAUNCHER
[6] eine Activity, die auch die Start-Activity einer App ist; - Die Kategorie
Intent.CATEGORY_TAB
[7] identifiziert dagegen solche Activities, die als Teil einer Activity einen eigenen Reiter bekommen kann.
Actions und Intent-Filter
Natürlich wollen wir implizite Intents nicht nur benutzen, sondern auch unsere eigenen Komponenten über implizite Intents nutzbar machen. Um etwa unsere SecondaryActivity
auch impliziten Intents zur Verfügung zu stellen, reicht eine einzige Änderung im Manifest:
<activity android:name=".SecondaryActivity" android:label="@string/app_name"> <intent-filter> <action android:name="de.wikibooks.android.SECONDARY" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
Neu ist hier das Tag <intent-filter>
. Hier wird die Beschreibung der Activity durchgeführt.
Wir kennzeichnen sie hier mit unserer eigenen Aktion „de.wikibooks.android.SECONDARY
“, es wäre aber kein Problem gewesen, dafür eine der Aktionen aus der Klasse Intent
zu verwenden.
Zu jedem Intent-Filter muss es mindestens eine Aktion und mindestens eine Kategorie geben. Mehrere sind aber durchaus möglich.
Als Kategorie wird in den meisten Fällen bedeutungsfrei Intent.CATEGORY_DEFAULT
[8] gewählt. Bereits jetzt können alle Android-Anwendungen (und nicht nur unsere eigene) mit impliziten Intents der Form
final Intent intent=new Intent("de.wikibooks.android.SECONDARY")
zur SecondaryActivity
gelangen.
Wir überzeugen uns jetzt davon, dass unsere Activities auch „von außen“ erreichbar sind. Im Kapitel [[../_Activities|Activities]] haben wir uns mit einem Android-Standard-Projekt beschäftigt und der (von Eclipse erzeugten) Activity den Namen StartActivity
gegeben. In diesem Projekt gibt es auch ein Manifest. Dort wurde StartActivity
bereits automatisch verzeichnet. Erst jetzt verstehen wir den Manifest-Eintrag
<activity android:name=".StartActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
vollständig:
Die Activity hat die Kategorie CATEGORY_LAUNCHER
[9] und ist über die Action ACTION_MAIN
[10] implizit erreichbar.
Implizite Intents gehören zu den gewöhnungsbedürftigsten Komponenten in der Android-Programmierung. Wenn man ihre Vorteile aber einmal verstanden hat, mag man sie nicht mehr missen.
Datenübergabe mit Intents
Dass wir jetzt zwischen einzelnen Activities wechseln können, ist schon ein Fortschritt. Noch mehr Möglichkeiten erhalten wir, wenn wir es schaffen, dass der Ziel-Activity Daten aus der Quell-Activity übergeben werden. Wir können uns vorstellen, dass eine Datenübergabe auch für andere Komponenten wie Services oder Broadcast-Receiver interessant sein kann.
Die Datenübergabe erarbeiten wir uns wieder an einer einfachen Anwendung mit zwei Activities, die wir bei Bedarf modifizieren und auch für andere Komponenten ganz ähnlich verwenden können. Bei der App aus dem [[../_Activities|Activities]] gibt der Anwender einen Text ein, der dann in Großbuchstaben umgewandelt wird. Diese Anwendung verteilen wir jetzt auf PrimaryActivity
und SecondaryActivity
.
Die erste übernimmt die Eingabe und sendet den Text per Klick auf einen Button an die zweite Activity. In die Layout-Datei primary.xml
fügen wir noch ein Eingabefeld ein:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <EditText android:id="@+id/word" android:layout_width="fill_parent" android:layout_height="wrap_content" android:inputType="text"/> <Button android:id="@+id/next" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="Next"/> </LinearLayout>
Die Activities sehen jetzt wie folgt aus:
-
1.
PrimaryActivity
-
2.
SecondaryActivity
Wenn der Anwender den Button anklickt, wird der eingegebene Text abgelegt:
public void onClick(View v) { secondary.putExtra("word", word.getText().toString()); startActivity(secondary); }
Jeder Intent verfügt über ein Verzeichnis von Key-Value-Paaren, die auch mit an die Zielkomponente versendet werden.
Dieses Verzeichnis hat den Typ Bundle
[11] und wird auch als Extras bezeichnet.
Im Beispiel fügen wir den Extras mit der Methode putExtra
[12] das Paar hinzu, dass aus dem Schlüssel "word", sowie dem Text besteht, den der Anwender eingegeben hat.
Außer diesem einen Paar könnten wir Bedarf noch mehr Informationen mitgeben. Diese werden dann in SecondaryActivity
mit Hilfe der Methode getStringExtra
[13] aus der Klasse Intent
eingesammelt:
String word=getIntent().getStringExtra("word"); TextView view = (TextView) findViewById(R.id.tv); view.setText(word.toUpperCase());
wobei tv
, die id der TextView
ist, die wir in secondary.xml
vereinbart haben.
Den zu Intent, zu der die SecondaryActivity
gehört, finden wir mit getIntent
. Aus den Extras des Intents können wir dann unter Angabe des Schlüssels "word" auch wieder den zugehörigen Wert auslesen.
Pending-Intent
Ein PendingIntent
[14] Objekt ähnelt einem Intent mit einer Ausnahme. Das Objekt, das durch ein Pending Intent aktiviert wird, läuft unter den Rechten des Erzeugers der Pending Intent. Das ist sogar dann noch der Fall, wenn zum Beispiel die erzeugende Activity oder eben der erzeugende BroadcastReceiver nicht mehr existiert. Das ist offenbar ein interessantes Mittel für einen Receiver. Es gibt eine Reihe von statischer Factory-Methoden zur Erzeugung von PendingIntent-Objekten. Bsp.: getActivity(), getBroadcast(), getService(). Dieser Methoden erzeugen ein Pending Intent Objekt das bei dessen Nutzung jeweils eine Activity, einen Broadcast oder einen Service erzeugt – mit den Rechten des Erzeugers des Pending Intents! Man sollte sehr vorsichtig damit umgehen.
Einzelnachweise
- ↑ http://developer.android.com/reference/android/content/Intent.html
- ↑ http://developer.android.com/reference/android/content/Context.html#startActivity(android.content.Intent)
- ↑ http://developer.android.com/reference/android/content/Intent.html#ACTION_DIAL
- ↑ http://developer.android.com/reference/android/content/Intent.html#ACTION_VIEW
- ↑ http://developer.android.com/reference/android/content/Intent.html#addCategory(java.lang.String)
- ↑ http://developer.android.com/reference/android/content/Intent.html#CATEGORY_LAUNCHER
- ↑ http://developer.android.com/reference/android/content/Intent.html#CATEGORY_TAB
- ↑ http://developer.android.com/reference/android/content/Intent.html#CATEGORY_DEFAULT
- ↑ http://developer.android.com/reference/android/content/Intent.html#CATEGORY_LAUNCHER
- ↑ http://developer.android.com/reference/android/content/Intent.html#ACTION_MAIN
- ↑ http://developer.android.com/reference/android/os/Bundle.html
- ↑ http://developer.android.com/reference/android/content/Intent.html#putExtra(java.lang.String,%20android.os.Bundle)
- ↑ http://developer.android.com/reference/android/content/Intent.html#getStringExtra(java.lang.String)
- ↑ http://developer.android.com/reference/android/app/PendingIntent. Stand 14. November 2010
Das Manifest
Zurück zu Googles Android
Eine Pflicht für jede Android-Anwendung ist das AndroidManifest.xml-File. Es enthält die wichtigsten Informationen über eine Anwendung, welche es dem Android-System zur Verfügung stellt. Diese Informationen müssen dem System vor dem Start der Anwendung bekannt sein, damit es diese ausführen kann.
Folgendes wird im Manifest festgehalten:
- Als eindeutige ID für die Anwendung wird das Java-Package der Anwendung genannt
- Eine Auflistung aller Komponenten einer Anwendung. Dazu gehören Activities, Services, Broadcast-Receiver oder Content-Provider. Für jede dieser Komponenten werden der Klassenname und die jeweiligen Fähigkeiten angegeben.
- Alle Berechtigungen (Permissions), die benötigt werden, um bestimmte Teile der API in der Anwendung zu nutzen, z.B. Standort, Dateien, und Sensoren. Die Funktionsweise der Berechtigungen änderte sich im Verlauf der Android-Versionen ständig.
Struktur des Manifests
<?xml version="1.0" encoding="utf-8"?> <manifest> <uses-permission /> <permission /> <permission-tree /> <permission-group /> <instrumentation /> <uses-sdk /> <uses-configuration /> <uses-feature /> <supports-screens /> <compatible-screens /> <supports-gl-texture /> <application> <activity> <intent-filter> <action /> <category /> <data /> </intent-filter> <meta-data /> </activity> <activity-alias> <intent-filter> . . . </intent-filter> <meta-data /> </activity-alias> <service> <intent-filter> . . . </intent-filter> <meta-data/> </service> <receiver> <intent-filter> . . . </intent-filter> <meta-data /> </receiver> <provider> <grant-uri-permission /> <meta-data /> </provider> <uses-library /> </application> </manifest>
Hier eine Liste aller erlaubten Tags im Manifest:
- <action>[1]
- <activity>[2]
- <activity-alias>[3]
- <application>[4]
- <category>[5]
- [6]
- <grant-uri-permission>[7]
- <instrumentation>[8]
- <intent-filter>[9]
- <manifest>[10]
- <meta-data>[11]
- <permission>[12]
- <permission-group>[13]
- <permission-tree>[14]
- <provider>[15]
- <receiver>[16]
- <service>[17]
- <supports-screens>[18]
- <uses-configuration>[19]
- <uses-feature>[20]
- <uses-library>[21]
- <uses-permission>[22]
- <uses-sdk>[23]
Einzelnachweise
- ↑ http://developer.android.com/guide/topics/manifest/action-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/activity-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/activity-alias-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/application-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/category-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/data-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/grant-uri-permission-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/instrumentation-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/intent-filter-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/manifest-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/meta-data-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/permission-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/permission-group-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/permission-tree-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/provider-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/receiver-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/service-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/supports-screens-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/uses-configuration-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/uses-feature-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/uses-library-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/uses-permission-element.html. Stand 17. März 2011
- ↑ http://developer.android.com/guide/topics/manifest/uses-sdk-element.html. Stand 17. März 2011
Activities, Tasks und Launch Modes
Zurück zu Googles Android
Taskstacks
Die meisten Android-Anwendungen bestehen aus einer Abfolge von Activities. Startet ein Nutzer eine Android-Anwendung, so wird intern ein [[../_Intents oder "Ich hätte gern den Zucker"|Intent]] zum Starten (Launch) einer neuen Anwendung erzeugt. Im Manifest-File des Programms ist wiederum beschrieben, welche Activity bei einem solche „Launch-Intent“ zu starten ist. Diese „Root Activity“ wird im Manifest-File durch einen entsprechenden Intent-Filter definiert, zum Beispiel so:
... <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> ...
Wie wir bereits wissen, können Activities andere Activities aufrufen, was mittels eines Intent-Objektes erfolgt. Daraus ergibt sich eine Abfolge von Activities. Diese Abfolge erscheint den Anwendern dann als Android-Anwendung. Um in dieser Anwendung von einer laufenden Activity zu der Vorherigen zurückzukehren, kann der BACK-Button genutzt werden.
Es ist dem Anwender aber auch möglich eine weitere Anwendung zu starten. Dazu wird die aktuell laufende Anwendung in den Hintergrund versetzt und die neue Anwendung gestartet – nach den eben beschriebenen Abläufen. In der Regel erzeugt Android beim Start einer neuen Anwendung einen neuen Task. Dieser enthält einen Stack, in dem die Activities gespeichert und abgelegt werden.
Die Abbildung skizziert zwei Tasks. Der erste Task enthält zwei Activities – A1 und A2 –, da A2 oben auf dem Stack liegt, ist sie momentan aktiv und für den Anwender sichtbar. Nach Drücken des BACK-Buttons würde A2 vom Stack heruntergenommen („Pullen“) und A1 aktiv werden (Dadurch wird A2 gelöscht). Wechselt der Anwender in diesem Zustand zur Anwendung 2 (Task 2), so würde die Activity A5 sichtbar werden.
Das Standardverhalten von Android ist damit dieses: Wird eine neue Anwendung erzeugt, so wird ein neuer Task angelegt und eine neue Instanz der Root-Activity erzeugt. Diese erzeugt einen View und kann genutzt werden. Ruft die Root-Activity eine andere auf, wird wiederum eine neue Instanz dieser neuen Activity erzeugt und oben auf dem Task Stack gelegt („Pushen“). Sie interagiert nun mit dem Anwender – die rufende Activity ist nun im Hintergrund und passiv.
An dieser Stelle spricht man von der Affinität einer Activity. Im Standardfall hat eine Activity die Affinität bei dem Task zu bleiben, von dem aus sie gestartet wurde. Diese Verhalten kann für die gesamte Anwendung geändert werden, indem im Manifest-File die Affinität verändert wird, siehe auch android:taskAffinity.[1] Die Affinität kann aber auch für jede Activity einzeln verändert werden. So kann es in einigen Fällen sinnvoll sein, dass eine Activity nur einmal existiert (Singleton), sodass bei jedem weiteren Aufruf dieser Activity keine neue Instanz erzeugt, sondern immer die bereits vorhandene Instanz genutzt wird.
Das Verhalten beim Start einer neuen Activity kann an zwei Stellen gesteuert werden. Zum einen kann der aufrufende Content (meist eine Activity) mittels Flags im Intent den Prozess beeinflussen, siehe dazu.[2]
Die gerufene Activity kann ebenfalls den Erzeugungsprozess beeinflussen. Das erfolgt durch Deklaration einer Activity im Manifest-File.[3] Das Prinzip soll im Folgenden erläutert werden.
Intent-Flags zum Starten eines neuen Tasks
- FLAG_ACTIVITY_NEW_TASK:[4] Hierdurch wird (in der Regel) ein neuer Task erzeugt und die neuerzeugte Activity ist das erste Element im Stack des neuen Tasks. Für den Anwender erscheint es, als ob eine neue Anwendung erzeugt wurde.
Aber nicht immer wird dadurch ein neuer Task erzeugt. Falls die Activity bereits einmal in einem neuen Task erzeugt wurde, so wird auf die erneute Erzeugung eines Tasks verzichtet. Es wird stattdessen der bereits existierende Task in den Vordergrund gebracht. Aber auch dieses Verhalten kann beeinflusst werden:
- FLAG_ACTIVITY_MULTIPLE_TASK:[5] Durch dieses Flag ist es möglich, dass immer ein neuer Task erzeugt wird.
Activity Attribute
In der Anwendung kann die Affinität der Activities generell verändert werden. Das kann aber auch in einer einzelnen Activity erfolgen. Es wird das gleiche Attribut genutzt: android:taskAffinity allerdings in der Deklaration der Activity.[6]
Das Attribute android:allowTaskReparenting[7] definiert, dass eine Activity nach wiederholtem Aufruf den Tasks wechseln kann. Wird also eine Activity A1 in einem Task T1 erzeugt, so liegt sie im Stack von T1, anfangs oben, aber sie kann nach unten wandern. Wird die gleiche Activity nun in einem Task T2 erzeugt, so wechselt sie den Task T2.
Activity Launch Modes
Android erlaubt weitere Deklarationen bezüglich des Verhaltens beim Start einer Activity. Dazu dient das Attribut android:launchMode.[8] Es sind folgende Parameter erlaubt:
- standard (Defaultwert), singleTop, singleTask, singleInstance
Singletons
Die Parameter singleInstance und singleTask definieren, dass die Activity immer in der Wurzel (dem Boden) des Tasks-Stacks stehen. Das heißt, wenn eine solche Activity erzeugt wird, wird in jedem Fall ein neuer Task angelegt und die Activity ist die erste, die in diesen Task eingeordnet wird.
Die beiden Modi unterscheiden sich aber darin, was mit folgenden Activities passiert. Die Activities können wie alle anderen auch folgende Activities aufrufen. Es besteht kein Unterschied. Der Unterschied liegt aber darin, welchen Task die neue Activities zugeordnet werden:
Die folgenden Activities bleiben im gleichen Task bei der Nutzung von singleTask. Es wird in jedem Fall ein weiterer neuer Task erzeugt, wenn singleInstance genutzt wurde. Es hat den gleichen Effekt, als wenn das Flag FLAG_ACTIVITY_NEW_TASK im Intent genutzt würde.
SingleInstance erzwingt demnach, dass ein Task exakt eine einzige Activity enthält.
SingleTask erzwingt, dass die Activity in jedem Fall am Boden des Task-Stacks liegt, darüber können aber weitere Activities liegen.
Standard
Der Standardfall wurde bereits beschrieben. Bei jedem Aufruf wird eine neue Instanz der gewünschten Activity erzeugt und oben auf den Stack des rufenden Tasks gelegt.
SingleTop
Dieser Parameter ähnelt dem Standardverhalten, mit einem Unterschied: Wird in einem Task ein Intent erzeugt, dass eine Activity verlangt, von der aber bereits eine Instanz oben auf dem Stack liegt, so wird keine weitere Instanz angelegt. Liegt die Instanz aber weiter unten im Stack, so wird eine neue Instanz angelegt und oben auf den Stack gelegt. Beispiel. In obigen Abbildung seien A1 und A2 als singleTop deklariert. Wird in Task 1 wiederholt eine A2 verlangt, so wird keine neue Instanz angelegt, sondern die Instanz von A2 bleibt oben auf dem Stack liegen. A2 wird über den Ruf der Methode onNewIntent() über diesen Vorgang informiert. Wird im Gegensatz dazu A1 gewünscht, so wird eine neue Instanz erzeugt und oben auf den Stack gelegt. Die Reihenfolge wäre dann: A1, A2, A1.
Einzelnachweise
- ↑ http://developer.android.com/guide/topics/manifest/application-element.html#aff. Stand 22. Oktober 2010
- ↑ http://developer.android.com/guide/topics/fundamentals.html#acttask. Stand 22. Oktober 2010
- ↑ http://developer.android.com/guide/topics/manifest/activity-element.html. Stand 22. Oktober 2010
- ↑ http://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK. Stand 22. Oktober 2010
- ↑ http://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MULTIPLE_TASK. Stand 22. Oktober 2010
- ↑ http://developer.android.com/guide/topics/manifest/activity-element.html#aff. Stand 22. Oktober 2010
- ↑ http://developer.android.com/guide/topics/manifest/activity-element.html#reparent. Stand 22. Oktober 2010
- ↑ http://developer.android.com/guide/topics/manifest/activity-element.html#lmode. Stand 22. Oktober 2010
Services
Zurück zu Googles Android
Manchmal ist es sinnvoll, Prozesse auf einem Gerät dauerhaft laufen zu lassen und unterschiedlichen Anwendungen einen Zugriff darauf zu erlauben. Solche Prozesse mögen Anwendungen sein, die regelmäßig Daten von einer externen Quelle beziehen und lokal auf dem Android-Gerät speichern. Es mögen lang laufende Berechnungen sein oder Dienste, die die Werte der Sensoren des Handy regelmäßig auslesen und verarbeiten. Es können aber auch Dienste sein, die keine Werte für Anwendungen berechnen und danach ihren Dienst einstellen.
In Android existiert das Konzept der Services. Services laufen parallel zu Activities und können von unterschiedlichen Aktivitäten genutzt werden. Mit Services wird mittels RPC kommuniziert. RPC steht für Remote Procedure Call und wird Ihnen gegebenenfalls bereits in der Variante RMI begegnet sein.
Das Konzept der RPC ist schon recht alt. Es geht auf einen Artikel von Nelson und Birrell aus dem Jahre 1984 zurück; zu einigen wenigen Details der Geschichte siehe auch T. Schwotzer.[1] RPC ähneln lokalen Prozeduraufrufen. Wird innerhalb eines Prozesses eine Prozedur/Funktion/Methode aufgerufen, so werden die Eingabewerte auf den Stack des Prozesses (konkret des Threads, wenn es mehrere gibt) gelegt. Die aufgerufene Funktion entnimmt am Anfang die Parameter, danach wird die Prozedur ausgeführt. Die Rückgabewerte werden wiederum auf den Stack gelegt und die Funktion wird beendet. Die rufende Funktion entnimmt nun die Rückgabewerte und arbeitet weiter.
Sollen Prozeduren über Prozessgrenzen aufgerufen werden, so müssen die Eingabewerte von rufenden Prozess zum aufgerufenen Prozess übertragen werden. Umgekehrt müssen die Ergebnisse vom gerufenen Prozess zum Aufrufer zurückgeschickt werden. Funktionen, die diese Parameter in ein Datenpaket packen, versenden und entpacken können anhand einer Interfacebeschreibung generiert werden. überlegen Sie einmal, wie Sie das machen würden!
In Android dient dazu eine Interfacebeschreibung und das Tool AIDL.[2] Lesen Sie dort Details zur Nutzung von AIDL und der Interfacebeschreibung nach. Hier wird nur das grobe Prinzip erklärt.
IDL
IDL steht für Interface Definition Language. Es gibt eine Fülle von IDL-Sprachen. Viele sind an Programmiersprachen angelehnt. Das erscheint auch logisch. Hier ist ein Beispiel.
interface Test { int increment(int i); }
Eine solche Beschreibung muss in Android in einer Datei Test.aidl gespeichert werden. Das Tool aidl erzeugt daraus eine einzige Datei: Test.java. Diese Datei enthält Javacode und konkret Folgendes:
- die Benutzeroberfläche „Test“. Diese enthält
- Stub
- Proxy
Wenn Sie Eclipse nutzen, so wird aidl automatisch gestartet, sobald Sie irgendeine Änderung in der aidl-Datei durchführen. Sollten Sie also das Beispielprogramm importieren, so müssen Sie einmal eine Leerzeile einfügen oder Ähnliches und aidl generiert ein (neues) Java-File.
Server
Es soll zunächst die Serverseite eines Services diskutiert werden. Auf der Serverseite wird der Dienst erbracht. Dazu sind folgende Dinge notwendig:
- Es muss eine Implementierung existieren.
- Es muss Code existieren, der die ankommenden Daten deserialisiert und der die Rückgabewerte serialisiert und zurücksendet.
- Es muss ein Prozess laufen, der auf Dienstaufrufe reagiert und sie an die Implementierung weiterreicht.
Die zweiten Aufgabe wird durch einen Stub bereitgestellt, die durch den aidl-Compiler erzeugt wird. Die Implementierung wird in das Gesamtsystem integriert, indem vom Stub abgeleitet wird und die Implementierung hinzugefügt wird. In der Beispielanwendung sieht das wie folgt aus:
public class TestImplementation extends Test.Stub { public int increment(int i) throws RemoteException { return i+1; } }
Die Implementierung ist offenbar denkbar simpel. Der Parameter wird entgegengenommen und das Inkrement wird zurückgegeben. Die Serialisierung und Deserialisierung bleibt den implementieren vollständig verborgen. Die einzige Ausnahme bildet die RemoteException, die innerhalb der Methode erzeugt und geworfen werden kann. Damit kann angezeigt werden, dass ein Fehler entstanden ist, der sich aus der Verteilung ergibt. Von einem Server wird erwartet, dass er auf Anfragen von Clients wartet und diese bearbeitet. Dazu ist ein Prozess nötig. In Android dient dazu die Klasse „Service“. Um einen eigenen Service zu definieren, wird von dieser Klasse abgeleitet. Beispiel:
public class SomeService extends Service { private TestImplementation impl; public void onCreate() { super.onCreate(); impl = new TestImplementation(); } public IBinder onBind(Intent intent) { return impl; } }
Die Implementierung stellt lediglich ein Beispiel dar, man kann den Service auch anders implementieren. Man muss allerdings auf zwei Ereignisse reagieren: Erzeugung und Binden.
Ein Service wird einmal erzeugt. Das erfolgt, wenn der erste Aufruf eines Clients erfolgt. Der Client versucht, sich in dem Moment an den Service zu binden. Binden bedeutet, dass eine Verbindung zwischen Client und Server aufgebaut wird. Nach erfolgreichem Binden können RPCs aufgerufen werden. Das Binden erfolgt immer, wenn ein Client eine neue Verbindung aufbauen will.
Diese Implementierung ist sehr einfach. Sobald ein Service erzeugt werden soll (onCreate()), wird ein Objekt der Implementierung erzeugt. Diese Serviceimplementierung merkt sich dieses eine Objekt. Bei jedem Bindeversuch wird eine Referenz auf dieses Objekt geliefert, siehe onBind(). Es ist zu erkennen, dass die Implementierung offenbar auch IBinder implementiert. Auch dieser Code ist generiert: Der Stub implementiert IBinder und damit erbt die Implementierung (TestImplementation) diese Fähigkeit.
Der Service muss nun noch Android bekannt gegeben werden. Wenig verwunderlich erfolgt das im Manifest der Anwendung:
<service android:name=".SomeService" />
Client
Auf Clientseite sind folgende Aufgaben zu erfüllen:
- Es ist eine Implementierung notwendig, die dem Client „vortäuscht“, dass er den Service direkt aufruft.
- Vor dem Aufruf muss sich der Client an den Service binden können. Letzteres erfolgt über eine ServiceConnection. Eine Instanz einer ServiceConnection repräsentiert die Bindung an einen Service, der den entsprechenden Dienst erbringen kann. Eine beispielhafte Implementierung ist diese:
public class TestServiceConnection implements ServiceConnection { private Test testService = null; public Test getTestService() { return this.testService; } public void onServiceConnected(ComponentName name, IBinder service) { Log.d(null, "Service Connected"); this.testService = (Test) service; } public void onServiceDisconnected(ComponentName name) {} }
Objekte konkreter ServiceConnections werden erzeugt und danach im Prozess des Bindens von einem Binder genutzt. Folgender Code initiiert beispielsweise das Binden:
mConnection = new TestServiceConnection(); this.bindService(new Intent(this, SomeService.class), mConnection, Context.BIND_AUTO_CREATE);
In diesem Fall wird exemplarisch eine Verbindung zu einem Service aufgebaut, der durch den Klassennamen bekannt ist. Informieren Sie sich über andere Möglichkeiten des Bindens im Handbuch! Das Prinzip ist in jedem Fall gleich. Es wird verlangt, dass eine Verbindung zu einem Service erzeugt werden soll (bindService()). Zur Beschreibung des Services dient das Intent. Das Connection-Objekt wird als Referenz übergeben, der letzte Parameter ermöglicht das Setzen von Optionen für den Verbindungsaufbau.
Dieser Aufruf erfolgt asynchron, das heißt im Hintergrund wird nun versucht, eine Verbindung zum Service aufzubauen. Konnte eine Verbindung zum Serviceprozess aufgebaut werden, so wird auf der ServiceConnection die Methode onServiceConnected() aufgerufen. Als Parameter wird eine Referenz auf einen IBinder übergeben. Tatsächlich zeigt diese Referenz auf den Clientstub der RPC-Implementierung. Er implementiert ebenfalls das Interface „Test“. In dieser Implementierung wird dieses Objekt gespeichert und bei Bedarf wird es mittels getTestService() herausgegeben. In der Beispielanwendung erfolgt das, wenn auf einen Knopf gedrückt wird. Der Clientcode sieht dort etwa so aus:
int value = 0; Test testSrv = this.mConnection.getTestService(); value = testSrv.increment(value);
Die ServiceConnection (mConnection) wurde bereits zuvor erzeugt und als Parameter dem Binder übergeben, siehe oben. An dieser Stelle wird davon ausgegangen, dass das Binden erfolgreich war. Es wird eine Referenz auf den Service (konkret den Clientstub) verlangt (getTestService() – siehe Implementierung von TestServiceConnection oben). Der Service implementiert die Servicebenutzeroberfläche „Test“. Der Service kann nun aufgerufen werden. An dieser Stelle ist er von einem lokalen Prozeduraufruf nicht zu unterscheiden. In der vollständigen Implementierung ist aber zu erkennen, dass zusätzlich auf eine RemoteException reagiert werden muss. Die Verteilung bleibt also nicht völlig transparent.
Einzelnachweise
- ↑ T. Schwotzer, Entfernter Mailzugriff auf RPC-Basis, Diplomarbeit, TU Chemnitz-Zwickau, Juli 1994 http://www.sharksystem.net/paper/diplom_schwotzer.pdf
- ↑ http://developer.android.com/guide/developing/tools/aidl.html. Stand 12. November 2010.
BroadcastReceiver
Zurück zu Googles Android
Broadcasts
Auf einem Mobiltelefon passiert ein Menge: SMS-Nachrichten und Telefonanrufe treffen ein, WLAN-Netze tauchen auf und verschwinden, die Uhrzeit ändert sich – selbst wenn nichts dergleichen passiert, geht irgendwann der Akku zur Neige. All dies sind Beispiele für Ereignisse die behandelt werden können. Aus eigener Erfahrung wissen wir, dass Einiges bereits vom Android-System übernommen wird: Wenn etwa ein Anruf eintrifft, wird die Telefon-Activity aufgerufen.
In diesem Kapitel erfahren wir, wie wir solche Ereignisse
- mit selbstentwickelten Komponenten verarbeiten und
- selbst auslösen können.
GUIs werden im Allgemeinen entwickelt, um dem Anwender Interaktion zu ermöglichen. Wenn er beispielsweise einen Button drückt, dann arbeitet ein Listener, der zuvor mit dem Button verbunden wurde, diesen Klick ab.
Diese Vorgehensweise hat sich etwa innerhalb homogener Java-Anwendungen bewährt, kann aber für die oben beschriebenen Android-Szenarien nicht verwendet werden: Eine Android-Anwendung ist modular aufgebaut und besteht aus mehreren DVM-Prozessen, die wir nicht mit Java-Bordmitteln auf Ereignisse ihrer Kollegen lauschen lassen können. Eine Android-Komponente kann aber DVM-übergreifend Broadcasts versenden, die andere Komponenten dann verarbeiten können. Beispielsweise versendet die Komponente, die eine SMS empfängt, einen Broadcast aus. Dieser Broadcast wird in einen Intent (siehe das Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“) eingebettet, dem wir über seine Extras auch Daten übergeben können. Wenn ein geeigneter Intent-Filter vorliegt, kann der Broadcast von einer speziellen Komponente, dem Broadcast-Receiver empfangen werden.
Der Receiver kann die Bundle-Daten – und dazu gehört etwa auch der Inhalt einer SMS – auslesen und verarbeiten. Broadcast-Receiver gehören zusammen mit Activities und Services zu den wichtigsten Komponenten einer Android-Anwendung.
Um Broadcasts zu abonnieren, müssen wir
- den Namen der zugehörigen Action kennen
- einen entsprechenden Intent-Filter im Manifest definieren
- einen Broadcast-Receiver für diese Action entwickeln
BroadcastReceiver – ein einfaches Beispiel
Das probieren wir gleich mal aus. Ansprechende Beispiele kann man für den SMS-Empfang entwickeln.
Da es hier aber wieder um das grundsätzliche Verständnis des Typs BroadcastReceiver
[1] geht, backen wir kleinere Brötchen und beschränken uns auf den Broadcast, der versendet wird, wenn jemand die Uhrzeit am Telefon ändert.
Die zugehörige Action heisst
Intent.ACTION_TIME_CHANGED
[2], der hinterlegte Text ist android.intent.action.TIME_SET.
Wir legen ein neues Android-Projekt an oder nehmen einfach ein Bestehendes und fügen in das Manifest die folgende Receiver-Definition mit einem Intent-Filter ein:
<receiver android:name=".TimeChangedReceiver" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.TIME_SET" /> </intent-filter> </receiver>
Eine Kategorie ist nicht erforderlich.
Die Klasse TimeChangedReceiver
aus dem <receiver>
-Tag sind wir natürlich noch schuldig.
Sie muss eine Unterklasse von BroadcastReceiver
sein.
Wir zeigen einfach nur einen Toast (siehe das Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“), wenn ein Broadcast eingetroffen ist:
public class TimeChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Toast toast = Toast.makeText(context, "Time Changed", Toast.LENGTH_LONG); toast.show(); } }
Uns fallen zwei Dinge auf:
- Die Methode
onReceive
[3] ist für die Entgegennahme und Verarbeitung des Broadcasts verantwortlich. - Am ganzen Ablauf ist keine Activity beteiligt. Tatsächlich können wir eine App entwickeln, die nichts anderes macht, als Broadcasts zu verarbeiten und gar nicht mit dem Anwender kommuniziert.
Jeder BroadcastReceiver
lebt nur so lange, wie seine onReceive
-Methode aktiv ist.
Nach dem Abschluss der Methode haben wir auf die zugehörige Instanz keinen Zugriff mehr, insbesondere nimmt er keine weiteren Broadcasts mehr entgegen.
Das ändert natürlich nichts an der Zuordnung der Broadcast-Action zu der BroadcastReceiver
-Klasse, so wie sie im Intent-Filter definiert wurde.
Für jeden Broadcast wird ein neues Objekt erzeugt.
Die Zuordnung bleibt bestehen, so lange die Anwendung installiert ist!
Wenn wir nach der Installation der App die Uhrzeit im Emulator ändern, sollte das ungefähr so wie in der folgenden Abbildung aussehen.
-
Aktivierung eines Broadcast-Receivers
Grundsätzlich
- ist ein Receiver nicht nur auf eine Action festgenagelt, sondern kann auch verschiedene Broadcast-Arten verarbeiten.
- kann der zum Broadcast gehörende Intent auch Daten enthalten. In dieser Hinsicht hat
ACTION_TIME_CHANGED
aber wenig zu bieten.
Wir verändern unsere Anwendung jetzt so, dass auch diese beiden Optionen umgesetzt werden: Ähnlich einfach wie ACTION_TIME_CHANGED
wird Intent.ACTION_TIMEZONE_CHANGED
[4] immer dann ausgelöst, wenn in den Einstellungen unseres Android-Systems die Zeitzone geändert wird.
Anders als im ersten Beispiel, enthalten die Extras des Intents Daten in Form der neuen Zeitzone. Diese holen wir ab und zeigen sie an.
@Override public void onReceive(Context context, Intent intent) { String action=intent.getAction(); String msg=""; if (action.equals(Intent.ACTION_TIME_CHANGED)) msg="Time Changed"; else if(action.equals(Intent.ACTION_TIMEZONE_CHANGED)) msg="time-zone"; else msg="Unknown Broadcast"; Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); }
Unser Receiver kann jetzt zwei verschiedene Arten von Actions verarbeiten.
Im Fall einer ACTION_TIMEZONE_CHANGED
wird aus den Extras der zum Schlüssel „time-zone“ gehörende Wert ausgelesen und in einer Toast-Nachricht angezeigt.
Der Fall der ACTION_TIME_CHANGED
wird praktisch genauso wie im ersten Beispiel behandelt. In der Manifestdatei muss der Broadcast für ACTION_TIMEZONE_CHANGED angefordert werden.
Komplexe Broadcast-Verarbeitung
Die Grundzüge der Broadcast-Verarbeitung haben wir an zwei einfachen Beispielen kennen gelernt. So ganz lebensnah ist das aber noch nicht:
Nur in den wenigen Fällen reicht die Anzeige eines Toasts. Häufiger findet eine komplexere Verarbeitung des Broadcasts statt. Und genau hier treten zwei Probleme auf:
- Wenn wir eine Activity in der
onReceive
-Methode starten wollen, um mit unseren Benutzern zu interagieren, werden wir – wenn wir nicht aufpassen – wenig Freude haben. Der DVM-Prozess zu dem der Receiver und somit auch die Activity gehört, wird nämlich zusammen mit deronReceive
-Methode beendet. Das Gleiche passiert, wenn der Broadcast zur Verarbeitung einen Thread startet. Auch seine Lebenszeit ist durch dieonReceive
-Methode begrenzt. - Wenn wir keinen Thread oder keine Activity für die Verarbeitung starten, sondern den Broadcast an Ort und Stelle im gleichen Thread behandeln, lauert Android bei einer Laufzeit von mehr als 10 Sekunden mit Abschüssen in Form von 'Application Not Responding'.
Für beide Probleme gibt es aber Lösungen.
Im Kapitel „[[../_Activities, Tasks und Launch Modes|Activities, Tasks und Launch Modes]]“ haben wir gelernt, wie wir Activities in einem eigenen DVM-Prozess starten können.
Ein Implementierungsmuster sehen wir in der folgenden onReceive
-Methode:
@Override public void onReceive(Context context, Intent intent) { String action=intent.getAction(); if (action.equals(Intent.ACTION_TIME_CHANGED)) Toast.makeText(context, "Time Changed", Toast.LENGTH_LONG).show(); else if(action.equals(Intent.ACTION_TIMEZONE_CHANGED)) showTimezone(context, intent.getStringExtra("time-zone")); } private void showTimezone(Context context, String msg) { Intent intent=new Intent("de.wikibooks.android.TIMEZONEACTIVITY"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("Message", msg); context.startActivity(intent); }
Wir sehen, dass für ACTION_TIMEZONE_CHANGED
-Broadcasts unsere eigene Methode showTimezone
aufgerufen wird.
Ihr übergeben wir neben dem Kontext noch den Wert der neuen Zeitzone als Text.
Die Intent-Verarbeitung erfolgt wie im Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“) beschrieben; neben der Activity wurde dem Intent noch das Flag FLAG_ACTIVITY_NEW_TASK
[5] mitgegeben, um die Activity in einem separaten DVM-Prozess einzubetten.
Selbstverständlich müssen wir die zu unserer selbstdefinierten Action TIMEZONEACTIVITY
gehörende Klasse noch definieren und im Manifest erfassen.
AndroidManifest.xml
<activity android:name=".TimezoneActivity" android:label="@string/app_name"> <intent-filter> <action android:name="de.wikibooks.android.TIMEZONEACTIVITY" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
TimezoneActivity.java
public class TimezoneActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.timezone); String msg=getIntent().getStringExtra("Message"); TextView view = (TextView) findViewById(R.id.zone); view.setText("New Timezone: "+msg); } }
Der Inhalt der Layout-Datei timezone.xml
sollte klar sein und wird hier nicht weiter diskutiert: Die Datei enthält nur eine einfache TextView
[6] mit zone
als id
.
Auch die Entgegennahme der Extras in der onCreate
-Methode verläuft geschmeidig.
Im Kapitel über Services (siehe [[../_Services|Services]]) haben wir gesehen, dass wir auch Services in eine separate DVM einbetten können.
Nun ist es aber nicht die feine englische Art den Benutzer mit einer plötzlich erscheinenden Activity bei seiner 'User-Experience' zu stören. Geeigneter sind hier die Notfications, die wir auch noch in einem eigenen Kapitel (siehe [[../_Notifications|Notifications]]) kennen lernen werden.
Dynamische Receiver
Wenn wir einen Receiver über einen Intent-Filter im Manifest mit einer Action verbunden haben, dann gilt dieser Bund bis dass die zum Receiver gehörende Anwendung vom Gerät entfernt wird. Diese Art der Broadcast-Receiver wird daher auch statisch genannt. Statische Receiver sind aber nicht immer das, was wir brauchen. Oft soll die Bindung einer Action an einen Receiver nur so lange, wie eine bestimmte Komponente bestehen. Hier gibt es die Möglichkeit den Intent-Filter 'programmatisch' zu definieren und dann die Zuordnung innerhalb der Komponente und nicht im Manifest durchzuführen.
Im folgenden Beispiel definieren wir einen Receiver als innere Klasse einer Activity. In der onResume
-Methode der Activity werden Receiver und Intent-Filter erzeugt und über die registerReceiver
-Methode mit der Activity verbunden. In der onPause
-Methode werden Activity und Receiver wieder entkoppelt. So wird erreicht, dass der Receiver so lange lebt und Broadcasts entgegennehmen kann, wie die Activity im Vordergrund ist.
Erst beim Wechsel in den Hintergrund, wird die Zuordnung aufgehoben und der Receiver der Garbage-Collection übergeben.
private class TimeChangedReceiver extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent){ Toast.makeText(context, "Time Tick", Toast.LENGTH_LONG).show(); } } private BroadcastReceiver receiver; @Override public void onResume() { receiver=new TimeChangedReceiver(); IntentFilter filter =new IntentFilter(Intent.ACTION_TIME_TICK ); this.registerReceiver(receiver, filter); super.onResume(); } @Override public void onPause() { this.unregisterReceiver(receiver); receiver=null; super.onPause(); }
Während ihrer Lebenszeit können dynamische Receiver also, anders als statische Receiver, mehrere Broadcasts entgegenehmen.
Broadcasts versenden
Bisher haben wir Broadcasts nur empfangen.
Der Versand ist aber für uns kein Problem.
Er erfolgt ganz ähnlich wie mit der Methode startActivity
zum Start einer Activity im Rahmen der Intent-Verarbeitung mit Hilfe der Methode startBroadcast
, die ebenso wie startActivity
von der Klasse Context
geerbt wird.
Im folgenden Beispiel versenden wir einen ACTION_TIME_CHANGED
[7]-Broadcast aus einer beliebigen Activity.
Wenn unser kleiner Demo-Receiver TimeChangedReceiver
jetzt noch installiert ist, sollte er gleich beim Start dieser Activity anschlagen und einen Toast anzeigen.
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Intent intent = new Intent(Intent.ACTION_TIME_CHANGED); sendBroadcast(intent); }
Wir werden uns noch mit dem Empfang von SMS-Nachrichten beschäftigen und werden dann Gelegenheit haben, unser Wissen über Broadcasts weiter anzuwenden und an praxisnäheren Beispielen zu vertiefen.
Geordnete Broadcasts
Zusätzlich zur Methode sendBroadcast
gibt es noch
sendOrderedBroadcast
.[8] Auch hier wird ein Intent als Parameter übergeben, der neben der Beschreibung des Intents auch weitere Daten in Form von Extras enthalten kann.
Ein Broadcast, der mittels sendBroadcast
versendet wurde, wird an alle Empfänger (quasi) gleichzeitig übermittelt. Ein Broadcast, der mittels sendOrderedBroadcast
verschickt wurde, wird dagegen sequentiell an Receiver verschickt. Begonnen wird bei denen mit der höchsten Priorität. Bei Receivern mit gleicher Priorität ist die Reihenfolge willkürlich.
Die Priorität kann durch ein Tag <android:priority>
[9] innerhalb der Definition des Intent-Filters in der Deklaration des <receiver>-Tags[10] definiert werden. All diese Deklarationen sind aber optional.
Broadcasts, die mit sendOrderedBroadcast
gesendet wurden, bieten einzelnen Receivern die Möglichkeit, Nachrichten anzuhängen, die der nächste Empfänger lesen kann. Dazu dienen die Methoden setResult
[11] (und Varianten davon) und getResult
[Code[12]|Data|Extras]. Damit lassen sich Abarbeitungsketten aufbauen und Zwischenergebnisse und Status übermitteln. Das ist ein bekanntes und sinnvolles Entwurfselement für Software (siehe auch das Design Pattern Zuständigkeitskette).
Einzelnachweise
- ↑ http://developer.android.com/reference/android/content/BroadcastReceiver.html
- ↑ http://developer.android.com/reference/android/content/Intent.html#ACTION_TIME_CHANGED
- ↑ http://developer.android.com/reference/android/content/BroadcastReceiver.html#onReceive(android.content.Context,%20android.content.Intent)
- ↑ http://developer.android.com/reference/android/content/Intent.html#ACTION_TIMEZONE_CHANGED
- ↑ http://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK
- ↑ http://developer.android.com/reference/android/widget/TextView.html
- ↑ http://developer.android.com/reference/android/content/Intent.html#ACTION_TIMEZONE_CHANGED
- ↑ http://developer.android.com/reference/android/content/Context.html#sendOrderedBroadcast%28android.content.Intent,%20java.lang.String%29. Stand 14. November 2010
- ↑ http://developer.android.com/reference/android/R.styleable.html#AndroidManifestIntentFilter_priority. Stand 14. November 2010
- ↑ http://developer.android.com/reference/android/R.styleable.html#AndroidManifestReceiver. Stand 14. November 2010
- ↑ http://developer.android.com/reference/android/content/BroadcastReceiver.html#setResult%28int,%20java.lang.String,%20android.os.Bundle%29. Stand 14. November 2010
- ↑ http://developer.android.com/reference/android/content/BroadcastReceiver.html#getResultCode%28%29. Stand 14. November 2010
Notifications
Zurück zu Googles Android
Wenn auf einem Android-Telefon ein Anruf eintrifft, wird automatisch die Telefon-Activity aufgerufen. Wie das grundsätzlich funktioniert, haben wir im Kapitel „[[../_BroadcastReceiver|BroadcastReceiver]]“ gesehen. In den meisten Fällen ist es aber kein gutes GUI-Design wenn Activities plötzlich den Vordergrund dominieren. Trifft beispielsweise eine SMS ein, dann werden wir darüber, wie in der der folgenden Abbildung 1, durch ein kleines Icon oben links in der Statusleiste informiert.
-
1. Notification in der Statusleiste
-
2. Anzeige der Notification
-
3. Die SMS-Activity
Diese so genannte Notification kann noch durch einen Vibrationsalarm, eine Audiosignal oder eine blinkende LED begleitet werden. Wenn wir die Statusleiste wie in der Abbildung 2 nach unten ziehen, sehen wir eine kleine Activity, die zur Notification gehört. Wenn wir diese Activity anklicken gelangen wir zur eigentlichen SMS-Activity (Abbildung 3).
Ein Beispiel
So eine Notification wollen wir hier entwickeln. Ganz ähnlich wie im Kapitel über BroadcastReceiver wird der Broadcast, der ausgelöst wird, wenn ein Anwender die Zeitzone ändert, abgefangen. In unserem Beispiel verschickt der Receiver dieses Mal eine Notification. Wenn der Anwender diese Notification anklickt, wird er zu einer Activity geführt, die die Zeitzone – in Großbuchstaben – anzeigt.
In der onReceive
-Methode unseres Broadcast-Receivers, mit dem wir Änderungen an der Zeitzone verarbeiten (siehe auch das Kapitel über [[../_BroadcastReceiver|BroadcastReceiver]]) erzeugen wir mit Hilfe eines Konstruktors der Klasse Notification
[1] eine Notification:
public class TimeChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int icon = android.R.drawable.ic_dialog_alert; String title = "Settings Changed"; String text = "User select new timezone"; long when = System.currentTimeMillis(); String timezone = intent.getStringExtra("time-zone"); Notification notification = new Notification(icon, title, when); } }
Die Parameter des Konstruktors bedürfen einer kurzen Erläuterung.
- Jede Notification wird durch ein Symbol (
icon
) in der Statusleiste repräsentiert, dessen Code wir aus der RessourcenklasseR
erhalten. - Zusammen mit dem Symbol wird ein Kurztitel in der Statusleiste angezeigt (
title
). - Außerdem geben wir noch die Uhrzeit (
when
) an, zu der die Notification rausgeht.
Der Notification-Manager
An unserem Telefon tut sich jetzt aber noch nichts.
Die Notification muss einem Objekt vom Typ NotificationManager
[2] übergeben werden, der die Notification bearbeitet und sie in geeigneter Form zur Anzeige bringt.
... NotificationManager mgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); int notidication_id = (int) System.currentTimeMillis(); mgr.notify(notidication_id, notification);
Es gibt im Android-System verschiedene Manager-Klassen, die der Kontext über die getSystemService
-Methode zur Verfügung stellt.
So bekommen wir auch den NotificationManager
, über den wir dann die Notification mit der Methode notify
[3] (die nichts mit der gleichnamigen Methode der Klasse Object
zu tun hat) verschicken.
- Der erste Parameter, den wir dieser Methode übergeben, ist eine Zahl, die Android verwendet, um Notifications zu unterscheiden. Wenn wir hier immer die gleiche Zahl verwenden würden, dann würden auch alle Notifications wie eine einzige erscheinen.
- Der zweite Parameter ist die eigentliche Notification.
Wenn wir die Notifications wie beschrieben versenden, erhalten wir noch zur Laufzeit einen Fehler. Wir haben es nämlich versäumt eine Activity anzugeben, die uns beim Anklicken der Notification – ähnlich wie in Abbildung 3 – angezeigt werden soll.
Intents und Pending-Intents
Da wir die Activity verzögert ausführen wollen, müssen wir sie irgendwie 'einpacken'. Auf den ersten Blick scheint dazu ein Intent bestens geeignet zu sein.
... Intent notificationIntent = new Intent(context, UpperActivity.class); notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notificationIntent.putExtra("time-zone", timezone);
Auf diese Weise haben wir schon häufiger Intents erzeugt. Hier werden auch wieder Extras übergeben und ein Flag gesetzt, damit die Activity in einem eigenen DVM-Prozess ausgeführt wird.
Diesen Intent definieren wir in unserem Beispiel aber in der onReceive
-Methode eines BroadcastRecevier
-Objektes.
Nach dem Ende der Methode müssen wir davon ausgehen, dass der zugehörige DVM-Prozess nicht mehr existiert und die Activity, die wir in dem Intent verpackt haben, nicht mehr gestartet werden kann.
Wir können den Intent und damit die Activity aber konservieren, indem wir sie in ein Objekt vom Typ PendingIntent
(siehe auch das Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“) einbetten:
... PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_ONE_SHOT );
- Der erste Parameter repräsentiert ein Objekt vom Typ
Context
, - Der zweite wird derzeit (Android 2.2) noch nicht ausgewertet,
- Im dritten ist der Intent hinterlegt und
- Im vierten, wird beschrieben, was beim Anklicken der Notification passieren soll. Hier haben wir uns für die Variante FLAG_ONE_SHOT entschieden: Nur der erste Klick führt uns zur Activity.
Unsere Notification können wir um einen Pending-Intent erweitern. In dem Fall wird nicht nur eine Mitteilung angezeigt, sondern die Mitteilung auch mit einer Activity verbunden. Diese Activity ist im Pending-Intent definiert, den wir jetzt zur Notification hinzufügen.
... notification.setLatestEventInfo(context, title, text, contentIntent); mgr.notify(notidication_id, notification);
Es ist zu sehen, dass die Notification, die weiter oben erzeugt wurde, mittels setLastestEventInfo
[4] um ein PendingIntent
-Objekt erweitert wird und dass danach der NotificationManager gebeten wird, diese Notification anzuzeigen.
Alles zusammen
Wenn wir die Code-Fragmente dieses Kapitels zusammensetzen, ergibt sich die folgende Receiver-Klasse
public class TimeChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int icon = android.R.drawable.ic_dialog_alert; String title = "Settings Changed"; String text = "User select new timezone"; long when = System.currentTimeMillis(); String timezone = intent.getStringExtra("time-zone"); Notification notification = new Notification(icon, title, when); NotificationManager mgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); int notidication_id = (int) System.currentTimeMillis(); Intent notificationIntent = new Intent(context, UpperActivity.class); notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notificationIntent.putExtra("time-zone", timezone); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_ONE_SHOT ); notification.setLatestEventInfo(context, title, text, contentIntent); mgr.notify(notidication_id, notification); }
Die Gestaltung der sehr einfachen Klasse UpperActivity
sowie die Erfassung der Komponenten im Manifest haben wir bereits mehrfach erläutert.
Diese abschließenden Schritte bleiben dem Leser zur Übung überlassen.
Einzelnachweise
- ↑ http://developer.android.com/reference/android/app/Notification.html#Notification%28%29. Stand 14. Nov. 2010
- ↑ http://developer.android.com/reference/android/app/NotificationManager.html
- ↑ http://developer.android.com/reference/android/app/NotificationManager.html#notify%28java.lang.String,%20int,%20android.app.Notification%29. Stand 14. Nov. 2010
- ↑ http://developer.android.com/reference/android/app/Notification.html#setLatestEventInfo(android.content.Context,%20java.lang.CharSequence,%20java.lang.CharSequence,%20android.app.PendingIntent)
ContentProvider
Zurück zu Googles Android
Eine Eigenschaft von Programmierframeworks besteht in der Schaffung von einheitlichen Sichten auf unterschiedliche Dinge. Dieser Aufgabe wird sich mehrfach gestellt, so auch beim Thema Datenzugriff. Auf Betriebssystemebene hat sich die Abstraktion Stream etabliert, um Dateien, Netzwerkverbindungen etc. gleich zu behandeln. Es wird eine Quelle geöffnet, Daten werden in den Stream geschrieben oder von dort gelesen.
In Android ist die allgemeine Sicht auf Daten (aus Dateien, über eine Netzverbindung, berechnete Daten etc.) der ContentProvider. An einem Beispiel soll der Aufbau eines (höchst simplen) eigenen ContentProviders gezeigt werden, der aber als Rahmen dienen kann, um für realistische Anwendungen weiterentwickelt zu werden. Verwiesen sei auch hier auf die exquisite Online-Dokumentation von Android [1] und ein Android-Buch.[2]
Beim Programmieren gibt es in der Regel zwei Sichten: Die des Aufrufenden (Caller, Client) und die des Aufgerufenen (Callee, Server, Provider).
Datenzugriff durch den Caller
Für den Caller stellt ein ContentProvider Tabellen zur Verfügung, wie man sie von Datenbanken her kennt. Vor der Nutzung muss der Caller zunächst den konkreten ContentProvider ermitteln. Dies erfolgt in Android mittels einer URI: die URI adressiert also konkreten Content.
Es gibt in Android eine Fülle von fertigen ContentProvidern. Eine Übersicht findet sich zum Beispiel in der Online-Dokumentation.[3] Jede Klasse verfügt über eine statische Konstante namens URI, die zur Identifikation des gewünschten Providers genutzt werden kann.
Wird ein eigener ContentProvider geschrieben, so ist eine URI zu definieren. Diese muss den Callees bekannt sein.
Der ContentResolver [4] stellt die Verbindung zwischen Caller und ContentProvider her und bildet gleichzeitig eine Abstraktion des ContentProviders. Eine Referenz auf den ContentResolver kann ihrerseits innerhalb jedes Ausführungskontextes durch die Methode getContentResolver() beschafft werden. Über den ContentResolver kann man anschließend die Datenquelle (die durch die URI ausgewählt wurde) mittels einer Query auf Daten durchsuchen. Folgendes Codefragment zeigt eine höchst simple Anfrage, illustriert aber das Vorgehen.
String uriString = "content://de.htw-berlin.f4.ai.thsc.sampleContentProvider"; Uri sampleURI = Uri.parse(uriString); ContentResolver cr = this.getContentResolver(); Cursor cursor = cr.query(sampleURI, null, null, null, null);
In dem Beispiel wird als Abfrage lediglich die URI übergeben, alle andere Parameter sind leer. Das hat zum Ergebnis, dass alle Daten der Tabelle ausgewählt werden. Die Parameter sollen hier nur kurz genannt werden. Eine vollständige Beschreibung findet sich zum Beispiel in einem Online-Tutorial [ADevCP].
Query hat folgende Parameter:
- URI: Adresse der Datenquelle, das heißt des ContentProviders
- projection: ein String[], der die Spalten benennt, die selektiert werden sollen. Die Namen der Spalten muss der Content-Provider bekannt geben. Üblicherweise erfolgt das durch Konstanten, die in der Implementierung des Providers deklariert sind.
- selection: Auswahl der Zeilen. Das erfolgt mittels eines Strings, der die Syntax einer SQL WHERE clause haben muss.
- selection args: Hier können in einem Stringarray Argumente deklariert werden, die in der WHERE clause referenziert werden.
- sortOrder: Ein String in der Syntax der SQL ORDER BY clause.
Das Ergebnis ist eine Datenmenge, durch die in SQL-üblicher Weise mittels eines Cursors navigiert werden kann. Es empfiehlt sich, den Cursor direkt nach dessen Erzeugung auf die erste Ergebniszeile zu setzen.
cursor.moveToFirst();
Der Cursor weist nun auf die erste Zeile der Datensätze, die den Anfragekriterien entsprachen. Es können nun die Werte der Ergebniszeile entnommen werden. Die Auswahl der Spalten erfolgt durch einen Index. Beispiel:
cursor.getString(1);
Das Beispiel entnimmt dem aktuellen Datensatz das Element der ersten Spalte und liefert sie als String zurück. Dieser Methodenaufruf würde scheitern, wenn es die Spalte 1 nicht gäbe oder wenn der Inhalt kein String wäre.
Natürlich empfiehlt sich auch in Android die Nutzung von Konstanten, das heißt es sollte nicht explizit eine 1 als Index genutzt werden, sondern ein symbolischer Name, zum Beispiel
cursor.getString(SampleContentProvider.VALUE);
Die Nutzung eines ContentProviders unterscheidet sich nicht grundsätzlich von der Nutzung einer relationalen Datenbank. Die URI ist das Pendant zum Namen einer Tabelle. Es erfolgt über den ContentResolver eine Query, durch deren Ergebnismenge mittels eines Cursor navigiert werden kann.
ContentProvider (Callee)
Die Nutzung eines ContentProviders ist überschaubar einfach. Ebenso ist die Implementierung eines ContentProviders, basierend auf einer SQL-Datenbank, ebenfalls recht einfach, da das Grundkonzept identisch ist.
Sollen andere (nicht datenbankartige) Datenquellen angeschlossen werden, kann das Unterfangen allerdings ungleich komplexer werden. An dieser Stelle soll der grobe Rahmen gezeigt werden, wie ein eigener Provider geschrieben werden kann. Details, wie eine konkrete Quelle angebunden werden kann oder wie gar die Parameter der Query parsiert werden können, werden hier außen vorgelassen.
Auf den ersten Blick ist die Implementierung eines ContentProviders allerdings weiterhin überschaubar: Es muss zunächst eine Klasse für den ContentProvider angelegt werden, zum Beispiel
public class SampleContentProvider extends ContentProvider { ... } public static final Uri CONTENT_URI = Uri.parse("content://de.htw-berlin.f4.ai.thsc.sampleContentProvider"); /** the id */ public static final String _ID = "_id"; /** value */ public static final int VALUE = 1;
Der ContentProvider repräsentiert die enthaltenen Daten eine Tabelle. Wie oben beschrieben, wird die Adresse der Tabelle mittels einer URI definiert. Die Tabelle im Beispiel hat zwei Spalten (ID und VALUE), was der Mindestanforderung an jeden ContentProvider entspricht. An dieser Stelle lassen sich bei Bedarf beliebig viele weitere Spalten definieren.
Der Provider muss nun im System bekannt gemacht werden. Dies erfolgt mittels der Manifest-Datei und eines Eintrages wie folgendem:
<provider android:name=".SampleContentProvider" android:authorities="de.htw-berlin.f4.ai.thsc.sampleContentProvider"> </provider>
Damit wird definiert, dass es einen Provider mit der Adresse de.htw-berlin.f4.... gibt und dass die Klasse SampleContentProvider in diesem Package die Implementierung davon ist. Mittels dieses Eintrages kann der ContentResolver ein Objekt der Klasse (ContentProvider) ermitteln, um eine Query zu erfüllen.
Tatsächlich erzeugt der Resolver beim Aufruf der Query ein Objekt dieser Klasse und leitet die Parameter des Aufrufers weiter an den ContentProvider. Dieser muss anschließend darauf reagieren.
Es gibt noch eine Reihe weiterer Methoden, die ein ContentProvider erfüllen muss. Sie seien hier kurz notiert:
- insert
- delete
- query
- update
- getType
Die meisten Methoden sind selbsterklärend; sie sind semantisch identisch zu ihren Pendants in SQL. Die Methode getType soll hingegen den MIME-Typ der Daten liefern, die der ContentProvider anbietet.
Im beigefügten Beispielcode wird ein höchst simpler Provider realisiert. Er repräsentiert eine Tabelle, die nicht veränderlich ist. Daher ist die Implementierung von delete, insert und update nicht nötig. Lediglich die Query muss implementiert werden.
Auch das ist hinreichend komplex, wenn alle Parameter der Query unterstützt werden sollen. Das Vorgehen des Parsens der WHERE und ORDER-BY clause werden hier nicht gezeigt. Im Gegenteil, es wird davon ausgegangen, dass die Provider diese Parameter nicht unterstützt und ignoriert. Selbst mit diesen harschen Einschränkungen besteht noch die Notwendigkeit, einen Cursor zu realisieren. Warum? Rein syntaktisch, weil der Rückgabewert von query ein Cursor ist:
public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
Die Implementierung eines eigenen Cursor ist nicht zu empfehlen, wenn auch möglich. Ein eigener Cursor wird implementiert, indem das Interface „Cursor“ [5] implementiert wird.
In der Dokumentation ist aber schon erkennbar, dass es eine ganze Reihe von fertigen Implementierungen gibt, so zum Beispiel SQLLiteCursor. Die Realisierung eines solche Cursors erscheint einfach, da das Modell und damit auch die Parameter von update, insert, delete und query identisch einer SQL-Datenbank sind. Ein solcher SQLLiteCursor delegiert den Aufruf an die Datenbasis.
Im Beispielprogramm wird der MatrixCursor genutzt. Wie es der Name vermuten lässt, bilden Objekte dieser Klasse eine Matrix. Die Breite der Matrix entspricht der Anzahl der Spalten und die Länge der Anzahl der Zeilen. Ein MatrixCursor wird durch Auswahl der jeweiligen Spaltennamen erzeugt. Damit ist die Breite der Tabelle festgelegt. Anschließend können Daten hinzugefügt werden; dies erfolgt einfach durch das Hinzufügen von Object-Arrays. Beispiel:
private MatrixCursor createCursor() { String[] columns = new String[]{ "_id", "value"}; MatrixCursor mc = new MatrixCursor(columns); Object[] row = new Object[] {"ein Baum", "hat Blätter"}; mc.addRow(row); return mc; }
In diesem Beispiel wird eine Tabelle mit zwei Spalten simuliert, die lediglich eine Zeile enthält. Es ist einfach erkennbar, dass durch mehrfaches Aufrufen von addRow() weitere Zeilen hinzugefügt werden können. Die Nutzung von Object[] mag auf den ersten Blick irritieren, weil damit nahezu jede Typsicherheit umgangen wird. An dieser Stelle kann allerdings nur so vorgegangen werden, da alle Datentypen innerhalb einer Tabelle erlaubt sein sollen.
Im Beispielprogramm wird eine Cursormatrix genutzt, um query() zu implementieren:
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return this.createCursor(); }
Es ist erkennbar, dass diese Implementierung sämtliche Parameter der Query ignoriert und immer die gleichen Daten liefert. Das ist natürlich nur für ein Beispielprogramm sinnvoll, das heißt, es muss für reale Implementierungen angepasst werden.
Einzelnachweise
- ↑ http://developer.android.com/guide/topics/providers/content-providers.html. Stand 12. November 2010.
- ↑ Becker, Arno; Pant, Marcus: „Android – Grundlagen und Programmierung“, dpunkt.verlag, 1. Auflage 2009. Seite 189 ff.
- ↑ http://developer.android.com/reference/android/provider/package-summary.html. Stand 12. November 2010.
- ↑ http://developer.android.com/reference/android/content/ContentResolver.html. Stand 12. November 2010.
- ↑ http://developer.android.com/reference/android/database/Cursor.html. Stand 12. November 2010.
Datenbankzugriffe
Zurück zu Googles Android
SQLite – eine gute Wahl
Daten wie die Einstellungen des Android-Systems, Kontakte oder die Telefonprotokolle legt Android in einer SQLite-Datenbank ab.
Das zugehörige Datenbanksystem SQLite[1] gehört zu den stabilsten und am meisten ausgereiften Werkzeugen, das der Open-Source-Bereich zu bieten hat. Wenn es keine Anforderungen an Transaktionen oder Mehrbenutzerfähigkeit gibt – wie es im Embedded-Bereich meistens der Fall ist – dann ist SQLite auch an Geschwindigkeit kaum zu schlagen, und das bei einer Größe des Installationspaketes von deutlich unter 1 MB. Zwar kann man in Android-Anwendungen auch mit einem anderen Datenbanksystem arbeiten, doch gibt es keinen Grund dazu.
Für die Arbeit mit SQLite stellt Android Klassen wie SQLiteDatabase
[2] zur Verfügung, die auf das System zugeschnitten sind.
In Java-Anwendungen ist JDBC [3] (Java Database Connectivity) ja eigentlich der Standard, doch wird in Android-Anwendungen nicht zur Verwendung von JDBC geraten. Wir werden in diesem Kapitel sehen, wie einfach die Arbeit mit der SQLite-Schnittstelle des Android SDK von der Hand geht.
Konfigurieren statt Programmieren
Wenn wir in unserer Android-Anwendung mit SQLite arbeiten, müssen wir immer mal wieder SQL-Anweisungen formulieren. Dabei entstellt es den ganzen Programmcode, wenn wir mehrzeilige SQL-Anweisungen als Text in den Code schreiben. Außerdem müssen wir den Java-Code neu übersetzen, sobald sich etwas an den SQL-Anweisungen ändert. Hier sorgt das Android-Plugin von Eclipse für Erleichterung:
Wir können Texte – und somit auch SQL-Anweisungen – in XML-Dateien schreiben, die dann zur Laufzeit als Objekte vom Typ String
zur Verfügung stehen:
Im Android-Projekt gibt es parallel zum layout
-Ordner den Ordner values
. In diesem Ordner finden wir die Datei strings.xml
, in die wir einige SQL-Anweisungen wie create table
zum Initialisieren der Datenbank hinterlegen. Neben dem Array, das wir create
genannt haben, sind hier auch Texte für app_name
, version
und dbname
vereinbart:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">AndroidDatabase</string> <string name="version">1</string> <string name="dbname">songsdb</string> <string-array name="create"> <item> create table artists( id integer primary key autoincrement, name varchar(20) not null ) </item> <item> create table songs( id integer primary key autoincrement, title varchar(20) not null, artist int references artist ) </item> <item> insert into artists(name) values (\'The Beatles\') </item> <item> insert into artists(name) values (\'Pink Floyd\') </item> <item> insert into songs(title, artist) values (\'Yellow Submarine\', 1); </item> <item> insert into songs(title, artist) values (\'Help\', 1); </item> <item> insert into songs(title, artist) values (\'Get Back\', 1); </item> <item> insert into songs(title, artist) values (\'Wish You Were Here\', 2); </item> <item> insert into songs(title, artist) values (\'Another Brick in the Wall\', 2); </item> </string-array> </resources>
Im Rahmen des Build-Prozesses wird aus dieser Datei eine Java-Klasse namens R
[4] generiert, die wir – wie in der folgenden Abbildung gezeigt – im Android-Projektverzeichnis finden. Diese Klasse enthält IDs in Form von ganzen Zahlen für unsere Objekte aus strings.xml
.
-
Die Klasse
R
im Android-Projekt
Wir sehen jetzt, wie wir die in strings.xml
hinterlegten SQL-Anweisungen ganz einfach ausgeben können: Wir erzeugen ein neues Android-Projekt und hängen an die onCreate
-Methode der Standard-Activity die folgenden Zeilen Code:
for(String sql : getResources().getStringArray(R.array.create)) System.out.println(sql);
Zu jedem Android-Projekt kann es Ressourcen geben, wie etwa die Texte, die wir hinterlegt haben.
Die Methode Context.getResources
[5] liefert uns ein Objekt vom Typ Resources
,[6] das wiederum die Methoden wie getStringArray
[7] enthält, mit dem wir wiederum auf unser String-Array zugreifen können. Dieser Methode übergeben wir die IDs unseres Arrays aus der Klasse R
und können es dann wie jedes andere Array durchiterieren. Die println
-Methode schreibt unsere SQL-Anweisungen in das Protokoll.
Texte und Arrays von Texten sind nur ein Teil der Ressourcen, die der Entwickler konfigurieren kann. Die Dokumentation zu den Typen R
und Resources
zeigt uns weitere Möglichkeiten auf.
Die Datenbank erzeugen
Objekte vom Typ SQLiteOpenHelper
[8] versorgen uns mit Datenbankverbindungen, die wir brauchen, um überhaupt SQL-Anweisungen zu SQLite schicken zu können.
Da diese Klasse aber abstrakt ist, müssen wir eine eigene Unterklasse entwickeln. In unserem Beispiel haben wir sie SongDatabaseHelper
genannt. Das folgende Listing zeigt die Implementierung; ihre Details werden wir uns gleich erarbeiten.
public class SongDatabaseHelper extends SQLiteOpenHelper { private Context context; SongDatabaseHelper(Context context){ super( context, context.getResources().getString(R.string.dbname), null, Integer.parseInt(context.getResources().getString(R.string.version))); this.context=context; } @Override public void onCreate(SQLiteDatabase db) { for(String sql : context.getResources().getStringArray(R.array.create)) db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
Der (einzige) Konstruktor der Basisklasse SQLiteOpenHelper
hat die folgende Signatur:
SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)
Unser eigener Konstruktor hat nur den Kontext als Parameter, aus dem wir dann die Argumente für den Konstruktor der Basisklasse ermitteln, den super
repräsentiert.
- Den Namen der Datenbank und ihre Version lesen wir wie weiter oben beschrieben aus
strings.xml
aus. - Der Dokumentation entnehmen wir, dass
null
im dritten Parameter die Standard-CursorFactory
repräsentiert – und die soll erst einmal reichen. - Die Version der Datenbank bezeichnet nicht die Produktversion von SQLite, sondern eine Versionsnummer, die von unserer Anwendung verwaltet wird. Immer wenn sich die Anwendung ändert, kann dies auch Änderungen an der Datenbank erfordern: sei es, dass Tabellen und Spalten kommen und gehen oder dass neue Datensätze hinzugefügt werden. Wir sehen später in diesem Kapitel, wie die Methode
onUpgrade
für Maßnahmen beim Versionswechsel greifen kann.
Da wir den Kontext auch an anderer Stelle benötigen, kopieren wir ihn in ein privates Attribut.
Zunächst interessiert uns aber die Methode onCreate
.[9] Sie ist ebenso wie onUpgrade
[10] in der Basisklasse mit dem Schlüsselwort abstract
markiert und muss daher überschrieben werden. Die Methode onCreate
wird immer dann aufgerufen, wenn es beim Aufbau der Verbindung die Datenbank, die mit dem Konstruktorparameter name
bezeichnet wird, noch nicht gibt. Es ist sehr praktisch, dass diese Methode mit einem Parameter vom Typ SQLiteDatabase
aufgerufen wird. So können wir mit execSQL
[11] gleich unsere in strings.xml
hinterlegten SQL-Anweisungen ausführen. Bevor wir uns mit onUpgrade
beschäftigen, wollen wir die Datenbank erzeugen, einige Tabellen anlegen und ein paar Daten einfügen.
Dazu gehen wir in die Standard-Activity unseres Projektes und fügen an das Ende ihrer onCreate
-Methode die folgenden Zeilen:
SQLiteOpenHelper database = new SongDatabaseHelper(this); SQLiteDatabase connection = database.getWritableDatabase();
Wir erzeugen ein Objekt vom Typ SongDatabaseHelper
und geben dem Konstruktor eine Referenz auf unsere Activity mit, die ja vom Typ Context
abgeleitet ist. Über die Methode getWritableDatabase
erhalten wir dann eine Datenbankverbindung vom Typ SQLiteDatabase
, über die wir SQL-Anweisungen verschicken und wieder einsammeln können. Doch dazu später mehr.
Das ER-Diagramm, das zu den beiden Tabellen gehört, die wir mit den „create table
“-Anweisungen angelegt haben, finden wir in der folgenden Abbildung:
-
ER-Diagramm zur Demo-Datenbank
Ein Blick hinter die Kulissen
Zunächst schauen wir uns unsere Datenbank genauer an. Dazu führen wir auf der Unix-Shell oder der Win32-Konsole unserer Entwicklungsmaschine das folgende Kommando aus, das zum Android-SDK gehört:
adb shell
Wir erhalten so eine Shell auf unserem virtuellen Device. Es sollte ein einfacher Prompt erscheinen:
#
Wenn de.wikibooks.android
der Paketname unseres Android-Projektes ist, dann führt uns der Verzeichniswechsel
cd /data/data/de.wikibooks.android/databases
zu dem Verzeichnis, in dem auch die von SQLite angelegte Datenbankdatei liegt:
# ls songsdb
Wir starten SQLite und verbinden uns mit der Datenbank songsdb
:
# sqlite3 songsdb SQLite version 3.6.22 Enter ".help" for instructions Enter SQL statements terminated with a ";" sqlite>
Mit der Anweisung .help
bekommen wir eine Übersicht über alle Anweisungen, die für die interaktive Arbeit mit dem Datenbanksystem zur Verfügung stehen.
Zunächst reicht uns eine Übersicht über die Tabellen:
sqlite> .tables android_metadata artists songs
Die Tabellen artists
und songs
haben wir in der onCreate
-Methode unseres SQLiteOpenHelper
-Objektes angelegt, die Tabelle android_metadata
wurde vom Android-System angelegt. Sie enthält aber keine interessanten Informationen:
sqlite> select * from android_metadata; en_US
Bei SQL-Anweisungen müssen wir auch immer an das abschließende Semikolon denken.
Neue Versionen
Wir haben uns davon überzeugt, dass die Datenbank, ihre Tabellen und einige Daten jetzt angelegt sind.
Immer wenn wir unsere App neu starten, arbeiten wir mit dieser gleichen Datenbank. Die onCreate
-Anweisung wird nur ein einziges Mal ausgeführt.
Wenn es im Laufe der Zeit nötig wird, die Datenbank zu ändern, geben wir ihr einfach eine neue Version. In unserem Szenario ist die Versionsänderung bereits mit einer kleinen Änderung in strings.xml
getan:
<string name="version">2</string>
Wenn wir unser SQLiteOpenHelper
-Objekt jetzt erzeugen, wird erkannt, dass es einen Versionswechsel gegeben hat und die Methode
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
aufgerufen. Die Parameternamen verraten uns dabei bereits die Bedeutung. In unserem Fall soll beim Wechsel von der Version 1 auf die Version 2 einfach eine Spalte zur Tabelle artists
hinzugefügt werden, die aussagt, ob der Interpret eine Gruppe oder ein Solist ist. Da wir in unserem initialen Datenbestand nur die Gruppen ‚The Beatles‘ und ‚Pink Floyd‘ haben, setzen wir die entsprechenden Einträge auf ‚Y‘. Die zugehörigen Anweisungen verpacken wir in XML und fügen sie zu strings.xml
hinzu:
<string-array name="v1to2"> <item> alter table artists add column band char(1); </item> <item> update artists set band=\'Y\'; </item> </string-array>
Beim nächsten Start der Anwendung wird zwar nicht die onCreate
-Methode aufgerufen, dafür aber die Methode onUpgrade
, die jetzt wie folgt aussieht:
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(oldVersion==1 && newVersion==2) for(String sql : context.getResources().getStringArray(R.array.v1to2)) db.execSQL(sql); else System.out.println("onUpgrade called - version not handled"); }
Diese defensive Programmierweise gewährleistet, dass bei jedem Versionswechsel entweder eine Änderung an der Datenbank ausgeführt oder eine entsprechende Warnung im Protokoll verzeichnet wird. Besser wäre hier sicher die Nutzung der Klasse Log
, die den Eintrag noch mit einem entsprechenden Tag versieht, doch sind dies Details, die in diesem Kapitel keine übergeordnete Rolle spielen. Wenn wir die Protokolle gewissenhaft auswerten, entgeht uns jedenfalls nichts. Möglicherweise ist es sogar angemessener, eine Ausnahme zu machen, wenn nicht mit einem bestimmten Versionswechsel gerechnet wird.
Cursor
Weiter oben haben wir ja bereits gesehen, dass wir etwa innerhalb einer Activity mit
SQLiteOpenHelper database = new SongDatabaseHelper(this); SQLiteDatabase connection = database.getWritableDatabase();
eine Verbindung zur Datenbank bekommen. Wenn wir nicht die Absicht haben, Daten zu verändern, können wir die letzte Zeile auch durch die defensivere Variante
SQLiteDatabase connection = database.getReadableDatabase();
ersetzen.
Genauso wie in der onUpdate
-Methode unserer SongDatabaseHelper
-Klasse können wir jetzt SQL-Anweisungen gegen die Datenbank laufen lassen.
connection.execSQL("insert into artists(name) values ('The Rolling Stones')")
Dieses Mal haben wir die Anweisung der Einfachheit halber nicht in strings.xml
eingetragen. Ganz ähnlich können wir auch Datensätze mit update
ändern oder mit der SQL-Anweisung delete
löschen. Grundsätzlich können wir der Methode execSQL
auch select
-Anweisungen übergeben, doch werden wir daraus keinen Nutzen ziehen: Da die Methode void
als Rückgabetyp hat, erfahren wir nicht, welche Daten der select
gefunden hat. Für Abfragen gibt es etwa die Methode rawQuery
[12] mit der folgenden Signatur:
public Cursor rawQuery (String sql, String[] selectionArgs)
Die select
-Anweisung wird einfach als Text übergeben. Zusätzlich haben wir auch die Möglichkeit, mit Platzhaltern zu arbeiten und diese dann über den zweiten Parameter mit Werten zu versorgen. Doch dazu später mehr.
Zunächst interessiert uns der Rückgabetyp. Die Ergebnisse unserer Abfrage werden uns als Cursor
[13] zurückgegeben. Ein Cursor
ist eine listenartige Datenstruktur, aus der wir alle gefundenen Datensätze abrufen können. In der einfachsten Form sieht das dann so aus:
Cursor result=connection.rawQuery("select name from artists", null); String s=""; while(result.moveToNext ()) s+=result.getString(0)+"\n"; Toast.makeText(this, s, Toast.LENGTH_LONG).show();
Die App blendet einen Toast mit allen Interpreten ein, die in unserer Datenbank verzeichnet sind.
Mit Hilfe von Methoden wie moveToNext
[14] und moveToPrevious
[15] können wir uns in der Ergebnismenge der select
-Anweisung bewegen.
Mit Methoden wie getString
[16] kopieren wir Daten aus dem Cursor in unsere eigenen Variablen. Wie der Cursor das macht, ist für uns als Anwender transparent. Im Idealfall fordert er von SQLite nur einige Datensätze an. Diese bilden einen Ausschnitt, in dem wir uns bewegen. Sobald die Grenzen diese Teilmenge überschritten werden, fordert der Cursor neue Daten vom Datenbanksystem – und das alles ohne, dass wir etwas davon merken. Neben getString
gibt es für einige Standard-Datentypen eine eigene Methode. Für Zahlen vom Typ int
gibt es etwa getInt
,[17] für solche vom Typ float
gibt es entsprechend getFloat
.[18]
Das Argument ist bei jeder dieser Methoden der Spaltenindex. Der Index 0 bezieht sich dabei auf die erste Spalte in der Ergebnismenge und 1 auf die zweite Spalte. Der Leser sollte sich mit Hilfe der Dokumentation zum Typ Cursor
einen Überblick über dessen vielfältige Methoden verschaffen.
Insgesamt hat es sich als eine gute Praxis erwiesen, die Daten dort zu belassen, wo sie sind.
Oft wird der Fehler gemacht, die Daten aus dem Cursor in „eigene“ Datenstrukturen – vorzugsweise Arrays – zu kopieren. Doch sind Cursor gerade für solche Ergebnismengen von select
-Anweisungen entwickelt worden und sollten auch von uns dazu genutzt werden.
Eine weitere gute Praxis besteht darin, Ressourcen, die wir angefordert haben, auch wieder freizugeben. In Java haben wir die Garbage-Collection, die ja hinter uns herräumt und etwa nicht benötigten Speicherplatz freigibt. Wir müssen uns darum nicht kümmern. Die Welt außerhalb von Java kennt aber vielfach keine Garbage-Collection. Wenn wir also Datenbankverbindungen angefordert haben, sollten wie sie nach getaner Arbeit auch wieder schließen, um zu vermeiden, dass SQLite Ressourcen für die Verbindung bunkert:
connection.close();
Nicht nur der Typ SQLiteDatabase
hat eine close
-Methode,[19] sondern auch andere Typen aus dem Paket android.database
. Und dazu gehört auch Cursor
. Wenn wir den Cursor aus dem Beispiel nicht mehr brauchen, teilen wir das SQLite über die folgende Anweisung mit:
result.close();
Prepared Statements
Wenn eine SQL-Anweisung bei SQLite eintrifft, wird es einer echten Rosskur unterzogen:
- Die Syntax wird geprüft.
- Es wird überprüft, ob die Tabellen und Spalten aus der Anweisung überhaupt existieren und
- ob der Benutzer überhaupt berechtigt ist, auf sie zuzugreifen.
- Es wird optimiert.
- Es wird in eine ausführbare Form gebracht
- … und schließlich ausgeführt.
Und das passiert bei jedem execSQL
aufs Neue. Dabei hätte es gereicht, die ersten fünf Schritte ein einziges Mal je Anweisung auszuführen und dann den Befehl in seiner ausführbaren Form wieder zu verwenden.
Und genau das geht mit Hilfe des Typs SQLiteStatement
. Die Methode
public SQLiteStatement compileStatement (String sql)
aus der Klasse SQLiteDatabase
bringt eine als Text vorliegende SQL-Anweisung in seine ausführbare Form. Objekte vom Typ SQLiteStatement
haben wiederum eine parameterfreie Methode namens execute
[20] zum Ausführen der SQL-Anweisung. Insgesamt ergibt sich so
String sql="insert into artists(name) values('Beach Boys')"; SQLiteStatement insert=connection.compileStatement(sql); for(int i=0; i<10; i++) insert.execute(); insert.close();
Die insert
-Anweisung wird einmal „präpariert“ und zehnmal ausgeführt. Die Verwendung von Prepared Statements stellt in der Entwicklung von Enterprise-Anwendungen, in denen sekündlich hunderte, wenn nicht gar tausende von Anweisungen beim Datenbanksystem eintreffen, eine ganz zentrale Tuning-Technik dar. Da in Android-Anwendungen typischerweise nur vergleichsweise wenig Datenbankaktivität stattfindet, spielen Prepared Statements hier eine nicht ganz so zentrale Rolle.
Unserem Beispiel mangelt es etwas an Dynamik: Kein Mensch fügt zehnmal einen Datensatz ein, der im Wesentlichen das Gleiche enthält. Wenn wir aber jedes Mal einen anderen Künstler wählen, müssen wir auch immer wieder aufs Neue präparieren und unser ohnehin sehr bescheidener Tuning-Effekt wäre ganz hinüber. Genau hier greifen Platzhalter. Wenn wir die insert
-Anweisung folgendermaßen formulieren:
String sql="insert into artists(name) values(?)";
können wir mit der Methode bindString
dem Platzhalter '?'
neue Werte zuweisen, ohne die Anweisung neu zu präparieren. Im Idealfall wird eine Anweisung nur ein einziges Mal präpariert und dann immer wieder recycelt:
String[] artists={"The Who", "Jimi Hendrix", "Janis Joplin", "The Doors"}; String sql="insert into artists(name) values(?)"; SQLiteStatement insert=connection.compileStatement(sql); for(String s : artists){ insert.bindString(1, s); insert.execute(); } insert.close();
SQL häppchenweise
Mit der Methode rawQuery
können wir eine SQL-Anweisung in Textform zu SQLite schicken. Es besteht aber auch die Möglichkeit, die Anweisung in ihre Bestandteile zu zerlegen und diese dann mit der Methode query
[21] wegzuschicken. Das Datenbanksystem baut die Teile wieder zu einer select
-Anweisung zusammen und führt sie aus. Wir wollen uns das mal anhand der folgenden etwa komplexeren Abfrage klarmachen:
select artist, count(title) from songs where id>1 group by artist having count(*)>1 order by id", null)
In dieser Form würden wir die Anweisung an rawQuery
übergeben. Die Zerlegung sehen wir an dem folgenden äquivalenten Aufruf der Methode query
:
Cursor result =connection.query( "songs", new String[]{"artist","count(title)"}, "id>?", new String[]{"1"}, "artist", "count(*)>1", "id" );
Welche der beiden Varianten man bevorzugt, ist allein eine Frage des Programmierstils. Aus technischer Sicht sind beide Methoden gleichberechtigt.
Einzelnachweise
- ↑ www.sqlite.org/
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html
- ↑ http://www.oracle.com/technetwork/java/javase/jdbc/index.html#corespec40
- ↑ http://developer.android.com/reference/android/R.html
- ↑ http://developer.android.com/reference/android/content/Context.html#getResources()
- ↑ http://developer.android.com/reference/android/content/res/Resources.html
- ↑ http://developer.android.com/reference/android/content/res/Resources.html#getStringArray(int)
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html#onCreate(android.database.sqlite.SQLiteDatabase)
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html#onUpgrade(android.database.sqlite.SQLiteDatabase,%20int,%20int)
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#execSQL(java.lang.String)
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#rawQuery(java.lang.String,%20java.lang.String[])
- ↑ http://developer.android.com/reference/android/database/Cursor.html
- ↑ http://developer.android.com/reference/android/database/Cursor.html#moveToNext()
- ↑ http://developer.android.com/reference/android/database/Cursor.html#moveToPrevious()
- ↑ http://developer.android.com/reference/android/database/Cursor.html#getString(int)
- ↑ http://developer.android.com/reference/android/database/Cursor.html#getInt(int)
- ↑ http://developer.android.com/reference/android/database/Cursor.html#getFloat(int)
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#close()
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteStatement.html#execute()
- ↑ http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#query(java.lang.String,%20java.lang.String%5B%5D,%20java.lang.String,%20java.lang.String%5B%5D,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String)
Positionsbestimmung
Zurück zu Googles Android
Eine Vielzahl der heutigen Smartphone Apps kommt ohne die Bestimmung der eigenen Position nicht aus. Aus diesem Grund bietet Android eine einfache Schnittstelle, um diese Daten auszulesen und zu verwenden. Wie man die Position eines Android-Smartphones bestimmt, werden wir in diesem Kapitel kennen lernen.
Lokalisierung über GPS und Netzwerk
Die Bestimmung der Position über GPS oder über das Netzwerk unterscheidet sich im Androidsystem nur hinsichtlich der Genauigkeit. Über beide Varianten erhalten wir Geokoordinaten (Latitude & Longitude). GPS-Daten sind dabei bis auf einige Meter genau, Daten über das Netzwerk können durchaus eine Toleranz von bis zu 1km haben, je nachdem wie weit man sich vom nächsten Router befindet, der seine Geokoordinaten kennt.
Wie man nun diese Koordinaten in Android erhält, sehen wir jetzt.
Code
Zunächst muss man sich den LocationManager[1] vom Androidsystem holen, der für das Bereitstellen der Positionsdaten verantwortlich ist.
LocationManager locationManager = (LocationManager) this.getSystemService(LOCATION_SERVICE);
Um auch immer die aktuellsten Geodaten zu bekommen, braucht es noch einen LocationListener[2] der auf die Aktivitäten des LocationManagers reagiert:
LocationListener locationListener = new LocationListener() { // Wird Aufgerufen, wenn eine neue Position durch den LocationProvider bestimmt wurde public void onLocationChanged(Location location) { TextView tv = (TextView) this.findViewById(R.id.Latitude); tv.setText(String.valueOf(location.getLatitude())); ... } public void onStatusChanged(String provider, int status, Bundle extras) {...} public void onProviderEnabled(String provider) {...} public void onProviderDisabled(String provider) {...} };
Als Letztes muss das Bestimmen der Position durch den LocationManager gestartet und dabei der Listener und der gewünschte LocationProvider[3] dem Manager übergeben werden.
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener);
Wenn man nicht die Position über das Netzwerk, sondern über GPS erhalten möchte, muss in requestLocationUpdates()[4] anstatt von LocationManager.NETWORK_PROVIDER der LocationManager.GPS_PROVIDER gesetzt werden.
Permissions
Damit der obige Code auch genutzt werden kann, müssen noch zwei Permissions im Manifest-Files gesetzt werden.
Für die Positionierung über das Netzwerk benötigen wir:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
Und für die Positionierung per GPS:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
Diese einfachen Schritte sind alles was man braucht, um in einer Applikation die Position eines Smartphones zu bestimmen.
Bestimmung einer Adresse
Mit Android ist es sogar möglich, sich aus den Geokoordinaten spezielle Ortsinformationen zu erhalten. Dazu benötigen wir eine Instanz des GeoCoder.[5]
Geocoder gc = new Geocoder(this); List<Address> addressList=null; try { addressList = gc.getFromLocation(location.getLatitude(), location.getLongitude(), 1); } catch (IOException e) { ... } Address address = addressList.get(0); System.out.println("Address: "+ address.getThoroughfare()+ " " + address.getSubThoroughfare());
Durch die Methode getFromLocation()[6] erhält man eine Liste von Adressen, die mit der Geokoordinate in Verbindung stehen. Die maximale Anzahl der Adressen können dabei begrenzt werden. Aus den Ergebnissen lassen sich Informationen wie Land, Postleitzahl, Stadt, Straße, Straßennummer etc. gewinnen.
Einzelnachweis
- ↑ LocationManager: http://developer.android.com/reference/android/location/LocationManager.html. Stand 16. Februar 2011
- ↑ LocationListener: http://developer.android.com/reference/android/location/LocationListener.html. Stand 16. Februar 2011
- ↑ LocationProviderhttp://developer.android.com/reference/android/location/LocationProvider.html. Stand 16. Februar 2011
- ↑ requestLocationUpdates(): http://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates%28java.lang.String,%20long,%20float,%20android.app.PendingIntent%29. Stand 16. Februar 2011
- ↑ GeoCoder: http://developer.android.com/reference/android/location/Geocoder.html. Stand 16. Februar 2011
- ↑ getFromLocation(): http://developer.android.com/reference/android/location/Geocoder.html#getFromLocation%28double,%20double,%20int%29. Stand 16. Februar 2011
HTTP
Zurück zu Googles Android
Viele Anwendungen bauen auf dem Client-Server-Prinzip auf, welche über das Internet Verbindung zueinander aufnehmen. Zu diesem Zweck wird meist HTTP als Übertragungsmittel genutzt. Wie man seine Daten in einer Android-Applikation via HTTP versendet, lernen wir in diesem Kapitel kennen.
Eine Voraussetzung hierfür ist, dass man über die grundlegenden Mechaniken von HTTP im Bilde ist.
Request-Methode
Je nachdem wie bzw. welche Informationen von einer URL oder Server bekommen möchte, nutzt man bei HTTP verschiedene Request-Methoden. Die meist genutzten sind GET, POST, PUT und DELETE. Und für diese gibt es in Android eine jeweilige Entsprechung: HttpGet,[1] HttpPost,[2] HttpPut[3] und HttpDelete.[4]
HttpGet get = new HttpGet("http://www.example.com/"); ... HttpPost post = new HttpPost("http://www.example.com/"); ... HttpPut put = new HttpPut("http://www.example.com/"); ... HttpDelete delete = new HttpDelete("http://www.example.com/"); ...
Diesen Objekten können weitere Informationen wie Header durch addHeader()[5] hinzugefügt werden … ganz wie man es von HTTP gewohnt ist.
Request und Response
Nachdem das Request-Päckchen gepackt ist, will es auch verschickt werden und man möchte gegebenenfalls auch eine Antwort erhalten. Zu diesem Zweck benötigen wir ein DefaultHttpClient[6]-Objekt, welches die Aufgabe des Senden und Empfangen übernimmt.
HttpGet get = new HttpGet("http://www.example.com/"); DefaultHttpClient client = new DefaultHttpClient(); try { HttpResponse response = client.execute(get); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
Durch die execute()[7]-Methode wird der Request ausgeführt und an die angegebene URL geschickt. Als Antwort erhält man ein Response-Objekt, welches die gewollten Informationen enthält. Es können Header und/oder Body-Informationen ausgelesen und genutzt werden.
Einzelnachweise
- ↑ HttpGet: http://developer.android.com/reference/org/apache/http/client/methods/HttpGet.html. Stand 18. Februar 2011
- ↑ HttpPost: http://developer.android.com/reference/org/apache/http/client/methods/HttpPost.html. Stand 18. Februar 2011
- ↑ HttpPut: http://developer.android.com/reference/org/apache/http/client/methods/HttpPut.html. Stand 18. Februar 2011
- ↑ HttpDelete: http://developer.android.com/reference/org/apache/http/client/methods/HttpDelete.html. Stand 18. Februar 2011
- ↑ http://developer.android.com/reference/org/apache/http/message/AbstractHttpMessage.html#addHeader%28org.apache.http.Header%29. Stand 18. Februar 2011
- ↑ http://developer.android.com/reference/org/apache/http/impl/client/DefaultHttpClient.html. Stand 18. Februar 2011
- ↑ http://developer.android.com/reference/org/apache/http/impl/client/AbstractHttpClient.html#execute%28org.apache.http.client.methods.HttpUriRequest%29. Stand 18. Februar 2011
Bluetooth
Zurück zu Googles Android
Erlaubnis (Permission)
Wie auch bei vielen andere Systemen in Android, kann Bluetooth in einer Anwendung nur genutzt werden, wenn die entsprechenden Erlaubnisse im Manifest gesetzt wurden. Für die Nutzung von Bluetooth werden zwei Erlaubnisse gebraucht:
- BLUETOOTH: Diese erlaubt es die grundlegendsten Funktionen der Bluetooth-Kommunikation zu nutzen. Darunter fallen das Anfragen einer Verbindung, das Akzeptieren einer eingehenden Verbindung und die Datenübertragung.
- BLUETOOTH_ADMIN: Durch diese ist es möglich, eine Suche nach anderen Bluetooth-Geräten zu starten oder die Bluetooth-Einstellungen zu verändern.
<manifest ... > ... <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> ... </manifest>
Sind Sie da?
Auch wenn man davon ausgehen kann, dass die allermeisten (Android-)Smartphones über Bluetooth verfügen, sollte man jedoch als guter Entwickler immer testen, ob ein zu benutzender Dienst auch existiert. Das in Android zu überprüfen, ist simpel ab API-Level 5:
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter == null) { // Das Gerät verfügt über kein Bluetooth. }
Um zu testen, ob das Smartphone über Bluetooth verfügt, holen wir uns vom System den BluetoothAdapter[1] über die Methode getDefaultAdapter().[2] Dieser BluetoothAdapter stellt die Repräsentation des eigenen Bluetooth-Gerätes dar. Wenn man als Rückgabewert ein NULL erhält, besitzt das Gerät kein Bluetooth.
Wenn das Gerät über Bluetooth verfügt, muss sichergestellt werden, dass es auch zum Zeitpunkt der Nutzung aktiviert ist. Die Aktivierung muss aber nochmals explizit durch den Nutzer bestätigt werden.
if (!mBluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }
Wenn Bluetooth noch nicht aktiv ist, dann wird ein Intent mit der BluetoothAdapter.ACTION_REQUEST_ENABLE-Aktion erzeugt, welche per startActivityForResult()[3]-Methode ins System abgesetzt wird. Dadurch öffnet sich ein Dialog, welcher den Nutzer fragt, ob Bluetooth aktiviert werden soll oder nicht. Durch startActivityForResult() wird nach Abarbeitung des Intents bzw. die Antwort des Nutzers, die Callback-Methode onActivityResult()[4] aufgerufen. Dieser ResultCode muss allerdings zunächst definiert werden. [5]
private final static int REQUEST_ENABLE_BT = 123;
Durch Auslesen des ResultCodes kann abgelesen werden, ob die Aktivierung erfolgreich war oder nicht.
public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { // Bluetooth ist aktiv. } else { // Bluetooth wurde nicht aktiviert. } }
Andere Geräte aufspüren
Um eine Verbindung zu einem anderen Bluetooth-Gerät herzustellen, ist es zunächst notwendig, dass man nach allen in der Nähe befindlichen Geräten sucht und von diesen Informationen abruft. Diese Informationen beinhalten den Gerätenamen, dessen Klasse und seine MAC-Adresse. Anhand dieser Informationen ist es dann möglich, sich mit einem anderen Gerät zu „paaren“ (vom engl. Begriff pairing), das heißt diese Geräte tauschen zum Zweck der Authentifizierung und Verschlüsselung einen Schlüssel aus. Alle mit dem eigenen Gerät gepaarten Geräte werden gespeichert und können durch die Bluetooth-API jederzeit abgerufen werden. So kann man anhand der MAC-Adresse jederzeit eine Verbindung zu einem Gerät herstellen ohne eine neue Suche anstoßen zu müssen. Voraussetzung dafür ist natürlich, dass sich das gewünschte Gerät in Reichweite befindet.
Gepaarte Geräte
Wenn man eine Verbindung zu einem bereits bekannten Gerät aufbauen möchte, muss man keine neue Suche nach anderen Geräten starten, da man alle Informationen auf dem eigenen Gerät finden kann.
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); // wenn gepaarte Geräte existieren if (pairedDevices.size() > 0) { // Durchgehen der gepaarten Geräte for (BluetoothDevice device : pairedDevices) { // einem Array die Adresse und den Namen der Geräte hinzufügen mArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } }
Durch die Methode getBondedDevices()[6] wird die Liste aller gespeicherten und gepaarten Geräte abgerufen.
Neue Geräte suchen
Wenn man eine Verbindung zu einem dem eigenen Gerät noch unbekanntem Gerät herstellen will, muss man zunächst nach diesem Gerät suchen. Dies erreicht man durch den Aufruf der Methode startDiscovery().[7] Der Aufruf dieser Methode löst eine asynchrone Suche aus. Nach Beendigung dieser Suche, wird durch das Aussenden der Aktion ACTION_FOUND in das System das Ergebnis dem System zugänglich gemacht und kann abgefragt werden. Zu diesem Zweck benötigt man einen BroadcastReceiver,[8] der auf die Beendigung der Suche reagiert.
// BroadcastReceiver für ACTION_FOUND private final BroadcastReceiver mReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // wenn durch die Suche ein Gerät gefunden wurde if (BluetoothDevice.ACTION_FOUND.equals(action)) { // das Bluetooth-Gerät aus dem Intent holen BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // Hinzufügen des Namens und der Adresse in ein Array mArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } } }; // den BroadcastReceiver im System registrieren IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); registerReceiver(mReceiver, filter);
Sich sichtbar machen
Um sein Gerät für andere sichtbar zu machen einfach den folgenden Code kopieren.[9]
Intent sichtbarMachen = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); startActivity(sichtbarMachen);
Geräte verbinden
Um die Verbindung zwischen zwei Geräten herzustellen, benötigt eine Anwendung jeweils eine Server- und Client-Implementierung. Wie bei diesem Modell üblich öffnet der Server eine Verbindung und wartet, bis diese von einem Client genutzt wird. Ziel beider Seiten ist es, einen BluetoothSocket[10] zu öffnen, auf welchem Daten zwischen den beiden Seiten übertragen werden können.
Server
Um auf dem Server die Verbindung zustande zu bringen, sind folgende Schritte zu beachten:
1. Durch den Aufruf der Methode listenUsingRfcommWithServiceRecord(String, UUID)[11] wird ein BluetoothServerSocket[12] geöffnet. listenUsingRfcommWithServiceRecord() schreibt einen Eintrag ins Service Discovery Protocol (SDP). Dabei wird ein Name und eine UUID für den eigenen Service benötigt. Dieser Name ist beliebig. Die UUID muss auf der Server- und der Client-Seite identisch sein, damit eine Verbindung zustande kommen kann.
2. Auf eine eingehende Verbindung warten
Durch den Aufruf von accept()[13] wartet der Server auf eine eingehende Verbindung und gibt ein BluetoothSocket-Objekt zurück. Der Aufruf von accept() blockiert den Thread bis zu dieser Verbindung, weshalb er nicht im UI-Thread der Anwendung laufen sollte.
3. Schließen des BluetoothServerSockets
Nach dem Erhalt des BluetoothSockets muss der BluetoothServerSocket durch den Aufruf von close()[14] geschlossen werden. Dies gibt den BluetoothServerSocket und all seine Ressourcen frei, schließt dabei aber nicht den BluetoothSocket. Dieser kann weiter für die Datenübertragung genutzt werden.
Beispiel-Implementierung für die Server-Seite:
private class AcceptThread extends Thread { private final BluetoothServerSocket mmServerSocket; public AcceptThread() { // Use a temporary object that is later assigned to mmServerSocket, // because mmServerSocket is final. BluetoothServerSocket tmp = null; try { // MY_UUID is the app’s UUID string, also used by the client code. tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID); } catch (IOException e) { } mmServerSocket = tmp; } public void run() { BluetoothSocket socket = null; // Keep listening until exception occurs or a socket is returned. while (true) { try { socket = mmServerSocket.accept(); } catch (IOException e) { break; } // if a connection was accepted if (socket != null) { // Do work to manage the connection (in a separate thread). manageConnectedSocket(socket); mmServerSocket.close(); break; } } } /** Will cancel the listening socket, and cause the thread to finish. */ public void cancel() { try { mmServerSocket.close(); } catch (IOException e) { } } }
Client
Für die Verbindung zum Server muss der Client folgende Schritte durchlaufen:
1. Erstellen eines BluetoothSockets zum gewählten Gerät
Durch den Aufruf von createRfcommSocketToServiceRecord(UUID) auf dem gewählten Gerät, erhalten wir ein BluetoothSocket-Objekt. Die verwendete UUID muss dieselbe sein wie auf der Server-Seite.
2. Verbindung aufbauen
Durch den Aufruf von connect() auf dem BluetoothSocket versucht das System, eine Verbindung zum Socket auf dem Server herzustellen. Ist dies von Erfolg gekrönt, kann zwischen beiden eine Kommunikation stattfinden.
Beispiel-Implementierung für die Client-Seite:
private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { // Use a temporary object that is later assigned to mmSocket, // because mmSocket is final. BluetoothSocket tmp = null; mmDevice = device; // Get a BluetoothSocket to connect with the given BluetoothDevice. try { // MY_UUID is the app’s UUID string, also used by the server code. tmp = device.createRfcommSocketToServiceRecord(MY_UUID); } catch (IOException e) { } mmSocket = tmp; } public void run() { // Cancel discovery because it will slow down the connection. mAdapter.cancelDiscovery(); try { // Connect the device through the socket. This will block // until it succeeds or throws an exception. mmSocket.connect(); } catch (IOException connectException) { // Unable to connect; close the socket and get out. try { mmSocket.close(); } catch (IOException closeException) { } return; } // Do work to manage the connection (in a separate thread). manageConnectedSocket(mmSocket); } /** Will cancel an in-progress connection, and close the socket. */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } }
Daten-Handhabung
Der Aufbau einer Verbindung zwischen zwei Bluetooth-Geräten ist nur die eine Seite der Medaille. Nach dem Aufbau dieser Verbindung muss dann auch noch der Austausch der Daten bewältigt werden. Bei der Übertragung der Daten kommen Input- und Outputstreams zum Einsatz.
Beispiel-Implementierung für die Handhabung der Daten:
private class ConnectedThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) { mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; // Get the input and output streams, using temp objects because // member streams are final. try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { byte[] buffer = new byte[1024]; // buffer store for the stream int bytes; // bytes returned from read() // Keep listening to the InputStream until an exception occurs. while (true) { try { // Read from the InputStream bytes = mmInStream.read(buffer); // Send the obtained bytes to the UI Activity. mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { break; } } } /* Call this from the main Activity to send data to the remote device. */ public void write(byte[] bytes) { try { mmOutStream.write(bytes); } catch (IOException e) { } } /* Call this from the main Activity to shutdown the connection. */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } }
Einzelnachweise
- ↑ BluetoothAdapter: http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html. Stand 24. Februar 2011.
- ↑ http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getDefaultAdapter%28%29. Stand 24. Februar 2011.
- ↑ http://developer.android.com/reference/android/app/Activity.html#startActivityForResult%28android.content.Intent,%20int%29. Stand 24. Februar 2011.
- ↑ http://developer.android.com/reference/android/app/Activity.html#startActivityForResult%28android.content.Intent,%20int%29. Stand 24. Februar 2011.
- ↑ http://stackoverflow.com/questions/8188277/error-checking-if-bluetooth-is-enabled-in-android-request-enable-bt-cannot-be-r
- ↑ http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getBondedDevices%28%29, Stand 25. Februar 2011.
- ↑ http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#startDiscovery%28%29. Stand 25. Februar 2011.
- ↑ BroadcastReceiver: http://developer.android.com/reference/android/content/BroadcastReceiver.html. Stand 25. Februar 2011.
- ↑ https://androidcookbook.com/Recipe.seam?recipeId=1978&recipeFrom=home Stand 03. März 2016.
- ↑ BluetoothSocket:http://developer.android.com/reference/android/bluetooth/BluetoothSocket.html, Stand 25. Februar 2011.
- ↑ http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#listenUsingRfcommWithServiceRecord%28java.lang.String,%20java.util.UUID%29. Stand 25. Februar 2011.
- ↑ BluetoothServerSocket: http://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html. Stand 25. Februar 2011.
- ↑ http://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html#accept%28%29. Stand 25. Feb. 2011
- ↑ http://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html#close%28%29. Stand 25. Februar 2011.
TCP-Sockets
Zurück zu Googles Android
Die erste Frage, die sich bei der Betrachtung von TCP bei Android stellt, ist: Haben Sie schon mal Sockets in Java programmiert? Wenn die Antwort „Ja“ lautet, dann herzlichen Glückwunsch. Sie brauchen sich das Kapitel über TCP-Sockets in Android nicht anschauen, da Android exakt die gleichen Java-Klassen und Strukturen zum Erzeugen von Sockets benutzt und sie somit schon alles kennen.
Da dieses Buch aber so vollständig wie möglich sein soll, möchten wir nicht so sein und erklären hier, wie Sie TCP-Sockets auf der Client- und der Server-Seite in Android implementieren können, um eine Verbindung zwischen Geräten herstellen zu können.
Client
Socket client; try{ client = new Socket("www.example.com", 4321); out = new PrintWriter(client.getOutputStream(),true); in = new BufferedReader(new InputStreamReader(client.getInputStream())); } catch(UnknownHostException e) { System.out.println("Unknown host: www.example.com"); } catch(IOException e) { System.out.println("No I/O"); }
Um einen Socket auf der Client-Seite zu öffnen wird einfach ein Socket[1]-Objekt erstellt. Diesem wird die Server-Adresse in Form einer URL oder auch einer IP-Adresse und die Portnummer, auf der der Socket arbeitet, übergeben. Sollte auf der angegebenen Adresse bzw. dem Port kein anderer Socket vorhanden sein, wird eine UnkownHostException[2] ausgegeben.
Wenn der Socket auf der anderen Seite existiert, dann lassen sich von diesem der Input- und Outputstream bestimmen, um auf dem Socket Daten zu senden und zu empfangen.
Server
ServerSocket server; try{ server = new ServerSocket(4321); } catch (IOException e) { System.out.println("Could not listen on port 4321"); } Socket client; try{ client = server.accept(); } catch (IOException e) { System.out.println("Accept failed: 4321"); } try{ in = new BufferedReader(new InputStreamReader(client.getInputStream())); out = new PrintWriter(client.getOutputStream(),true); } catch (IOException e) { System.out.println("Read failed"); }
Das Manifest
Im Manifest wird dann nur noch folgender Eintrag benötigt, um die Berechtigung zu erhalten, Netzwerk übergreifende Dienste nutzen zu können:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
Besonderheiten Emulatorbetrieb
Adresse | Funktion |
---|---|
127.0.0.1 | Abhängig davon, in welchem Netzwerk ein Programm läuft (Emulator oder Rechner) |
10.0.2.1 | IP-Adresse des Routers (Gateway-Adresse) |
10.0.2.2 | IP-Adresse des Rechners, auf dem der Emulator läuft (Entwicklungsrechner) |
10.0.2.15 | IP-Adresse des Emulators |
10.0.2.3 | Erster DNS-Server des Routers |
10.0.2.4–6 | Weitere DNS-Server des Routers |
Für den Socket auf der Server-Seite ist ein Schritt mehr zu machen als auf der Client-Seite. So muss zuerst ein ServerSocket[4]-Objekt erstellt werden, welcher auf dem gewünschten Port arbeitet. Dieser ServerSocket wartet durch den Aufruf der Methode accept()[5] auf eine eingehende Verbindung eines anderen Sockets. Kommt so eine Verbindung zustande, gibt accept() ein Socket-Objekt zurück, auf dem man, wie im Client-Abschnitt, arbeiten kann. Das heißt, dass man durch den Abruf der Input- und Outputstreams Daten zwischen den beiden Sockets senden kann.
Einzelnachweis
- ↑ Socket: http://developer.android.com/reference/java/net/Socket.html. Stand 05. März 2011
- ↑ UnkownHostException: http://developer.android.com/reference/java/net/UnknownHostException.html. Stand 05. März 2011
- ↑ Android Grundlagen und Programmierung, 1. Auflage 2009, dpunkt.verlag GmbH, ISBN 978-3-89864-574-4.
- ↑ ServerSocket: http://developer.android.com/reference/java/net/ServerSocket.html. Stand 05. Stand 2011
- ↑ http://developer.android.com/reference/java/net/ServerSocket.html#accept%28%29. Stand 05. März 2011
Spezielle Activities
MapActivity
Zurück zu Googles Android
Neben der eigenständigen „Google Maps“-Anwendung, welche auf Android-Geräten zu finden ist, besteht die Möglichkeit, auch in der eigenen Anwendung Kartenmaterial von Google bereitzustellen. Zu diesem Zweck können Sie Activities als MapActivities implementieren, die genau diese Aufgabe erfüllen. Wie man diese Funktion umsetzt, wird nun im Folgenden gezeigt.
Vorbereitung
Um Karten in Ihrer Anwendung zu benutzen, müssen einige Voraussetzungen geschaffen werden.
SDK mit Google-API
Da es sich bei der Bereitstellung von Karten um einen Dienst von Google handelt, benötigen Sie nebst dem Android-SDK auch die API von Google. Zu diesem Zweck existiert zu jeder SDK-Version auch eine SDK-Version, die mit der Google-API ausgestattet ist. Um an das SDK mit Google-API zu kommen, müssen Sie diese, analog zur normalen „SDK-Version“, über den „Android SDK and AVD Manager“ auswählen und installieren.
Google-API-Key
Um den Kartendienst zu nutzen, müssen Sie sich für diesen zusätzlich noch registrieren. Eine genaue Anleitung, wie Sie Ihren Maps-API-Key erhalten, finden Sie hier. Für unsere Zwecke reicht es, wenn Sie die Registrierung mit Ihrem „SDK Debug“-Zertifikat durchführen. Sollten Sie später Anwendungen schreiben, die ihren Weg in den Android-Markt finden und Sie sich selbst ein privates Zertifikat erstellen, dann müssen Sie mit diesem einen neuen API-Key generieren (vorausgesetzt, Ihre Anwendung benötigt diesen).
Hinweis: Unter Java7 wird standardmäßig der SHA1-Schlüssel angezeigt. Um den MD5 Schlüssel zu sehen einfach -v an den keystore-Befehl anhängen
MapActivity selbst
Um nun einmal die MapActivity in Aktion zu erleben, brauchen wir ein neues Projekt. Also auf auf. Eclipse geöffnet und ein neues Androidprojekt gestartet. Geben Sie Ihrer Start-Activity den Namen HelloMapView ein!
Manifest
Zunächst müssen ein paar Änderungen am Manifest vorgenommen werden. So müssen wir als erstes die Maps Library unserem Projekt hinzufügen, da diese nicht zur Standard Library von Android gehört. Dazu fügen Sie bitte die Zeile
<uses-library android:name="com.google.android.maps" />
dem Manifest als untergeordneten Tag des <application>-Tags hinzu.
Des Weiteren benötigt die MapActivity Zugriff zum Internet, um neues Kartenmaterial herunterzuladen. Mit der Zeile
<uses-permission android:name="android.permission.INTERNET" />
als untergeordnetes Tag des <manifest>-Tags, erlauben wir der Anwendung die Kommunikation mit dem Internet.
Layout
Um später in der Activity die Karte einzublenden und mit dieser zu interagieren, müssen Sie im Layoutfile ein MapView-Tag anlegen, welches die Karte repräsentiert. Verändern Sie ihr main.xml, sodass es folgendem Code entspricht:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainlayout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <com.google.android.maps.MapView android:id="@+id/mapview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:clickable="true" android:apiKey="Your Maps API Key" /> </RelativeLayout>
- android:apiKey – Für dieses Attribut geben Sie Ihren API-Key an, welches Sie dazu berechtigt, den Kartendienst von Google zu nutzen und die Daten herunterzuladen.
Activity
Um die Anwendung abzurunden, müssen wir jetzt noch die MapActivity etwas bearbeiten.
1. Öffnen Sie die HelloGoogleMaps Activity und erweitern Sie diese mit MapActivity[1]
public class HelloMapView extends MapActivity
Daraufhin müssen Sie mit Hilfe von Eclipse die benötigte Bibliothek importieren und die abstrakten Methoden der MapActivity einfügen. In diesem Fall wird nur die Methode boolean isRouteDisplayed() eingefügt. Diese wird genutzt, wenn Sie auf der Karte eine Route darstellen wollen. Da das bei uns nicht der Fall sein wird, geben wir false zurück.
2. In der onCreate()-Methode fügen Sie jetzt noch diese beiden Zeilen hinzu:
MapView mapView = (MapView) findViewById(R.id.mapview); mapView.setBuiltInZoomControls(true);
Damit holen wir uns die Referenz auf unseren im Layoutfile definierten MapView.[2] Mit setBuiltInZoomControls(true) fügen wir dem MapView einen ZoomIn- und ZoomOut-Button hinzu. Ohne diese beiden könnten wir die Karte auf nur einem Detaillevel hin- und herverschieben.
Im Grunde genommen haben wir bereits jetzt eine kleine (nicht unbedingt umfangreiche) Maps-Anwendung gebaut. Wir können die Karte verschieben und in diese hin- oder herauszoomen. Nun wollen wir aber noch die Möglichkeit schaffen, unsere Position durch einen Marker auf der Karte zu verdeutlichen (Nach dem Motto: Das X markiert die Stelle).
Dazu benötigen wir in unserem Projekt eine neue Klasse.
3. Erstellen Sie die Klasse HelloItemizedOverlay. Wenn Sie die Klasse über den Eclipse-Dialog erstellen, dann geben Sie als Superklasse com.google.android.maps.ItemizedOverlay an[3] und machen Sie ein Häkchen bei Constructors from superclass. Anderfalls lassen Sie die neue Klasse von Hand von der Superklasse ItemizedOverlay erben. Achten Sie darauf, dass bei der Superklasse in den Pfeilen NICHT Item, sondern OverlayItem[4] steht. Nachdem Sie auch noch die abstrakten Methoden der Superklasse eingefügt haben, sollte die Klasse wie folgt aussehen:
public class HelloItemizedOverlay extends ItemizedOverlay<OverlayItem> { public HelloItemizedOverlay(Drawable defaultMarker) { super(defaultMarker); // TODO Auto-generated constructor stub } @Override protected OverlayItem createItem(int i) { // TODO Auto-generated method stub return null; } @Override public int size() { // TODO Auto-generated method stub return 0; } }
Diese Klasse stellt eine zusätzliche Schicht dar, welche über die eigentliche Karte gelegt wird. Wir legen auf dieser Schicht unsere Marker ab, um von uns bestimmte Punkte besser hervorzuheben.
4. Als Nächstes braucht unsere Schicht eine Liste, in der sie die einzelnen Marker ablegen kann. Dafür erweitern wir die Klasse um ein ArrayList<OverlayItem>, in der wir die Marker speichern.
private ArrayList<OverlayItem> mOverlays = new ArrayList<OverlayItem>();
5. Später in der Anwendung wird die Position der Marker durch Geokoordinaten (Längen- und Breitengraden) angegeben. Damit sie nicht irgendwie auf dieser Position angezeigt werden, geben wir an, welche Stelle des Markers auf der Position liegt. In unserem Fall sagen wir, dass die Mitte der unteren Kante des Markers auf dem Geoposition liegt. Dazu erweitern Sie den Konstruktor um eine kleine statische Methode:
public HelloItemizedOverlay(Drawable defaultMarker) { super(boundCenterBottom(defaultMarker)); }
6. Nun müssen unsere Marker ja auch noch in das Overlay gebracht werden. Zu diesem Zwecks spendieren wir der Klasse eine weitere Methode:
public void addOverlay(OverlayItem overlay) { mOverlays.add(overlay); populate(); }
Mit addOverlay(OverlayItem overlay) fügen wir dem Overlay beziehungsweise der Liste einen weiteren Marker hinzu. Nachdem wir das gemacht haben, MÜSSEN wir die Methode populate() aufrufen, um bekannt zu geben, dass sich etwas an den zugrunde liegenden Daten beziehungsweise der Anzahl der Marker getan hat. In Folge des Aufrufs von populate() werden die Methoden size() und createItem(int i) unserer Klasse HelloItemizedOverlay aufgerufen. Dadurch werden die alten und auch der neue Marker auf dem Overlay neu dargestellt. Damit diese beiden Methoden richtig arbeiten, müssen sie noch an die Begebenheiten angepasst werden.
@Override protected OverlayItem createItem(int i) { return mOverlays.get(i); } @Override public int size() { return mOverlays.size(); }
In beiden Fällen wird an unsere OverlayItem-Liste herangetreten, um die entsprechenden Informationen, die nötig sind, zu bekommen.
Damit sind wir mit der Arbeit an dieser Klasse für dieses Beispiel fertig. Als Letztes müssen wir in der Activity noch einige Veränderungen vornehmen, damit wir auch ein paar Marker auf die Karte bringen können.
-
1. Kartenmarker
7. Bevor wir zu den letzten Schritten im Programmcode kommen, brauchen wir noch die Grafik, die unseren Marker auf der Karte darstellt. Dazu kopieren Sie sich bitte die nebenstehende Abbildung 1 in den drawable-Ordner Ihres Projekts unter res/drawable-mdpi/ (res/drawable/, wenn Sie eine Android-Version unter 1.6 verwenden).
8. Zunächst benötigt wir noch einige Klassenvariablen:
private List<Overlay> mapOverlays; // eine Liste mit allen Overlays des ''MapViews'' private Drawable drawable; // das ''Drawable'' für unseren Marker private HelloItemizedOverlay itemizedOverlay; // unser Overlay
Welche dann in onCreate() instanziiert werden:
mapOverlays = mapView.getOverlays(); // Damit wir später unser Overlay auf die Karte anwenden können, müssen wir uns vom ''MapView'' die Liste aller Overlays holen. drawable = this.getResources().getDrawable(R.drawable.arrow_down); // Aus den Ressourcen holen wir uns unser Bild des Markers … itemizedoverlay = new HelloItemizedOverlay(drawable); // … und setzten es als Defaultmarker in unseren Overlay.
Jetzt brauchen wir noch die Position, an der unser Marker auf der Karte angezeigt werden soll.
GeoPoint point = new GeoPoint(52457270,13526380); OverlayItem overlayitem = new OverlayItem(point, "", "");
Dem GeoPoint[5] werden die Koordinaten als Längen- und Breitengraden angegeben. Zu beachten ist, dass die normalen Grade mit 10^6 mal genommen werden müssen. Bsp: Aus 52,45° werden 52450000. Diese Position wird einem OverlayItem als Parameter übergeben, damit dieses an der gewünschten Position angezeigt wird.
Als letzten Schritt fügen wir das gerade erstellte OverlayItem unserem Overlay[6] und dieses Overlay der Liste der Overlays des MapView hinzu.
itemizedOverlay.addOverlay(overlayitem); mapOverlays.add(itemizedOverlay);
Wenn wir nichts vergessen haben, können Sie die Anwendung einmal laufen lassen. Sie können über die gesamte Weltkarte navigieren, hinein- und herauszoomen und Sie werden einen Marker finden, den Sie aber erst einmal finden müssen.
Einzelnachweise
- ↑ MapActivity: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/MapActivity.html, Stand 18. November 2010.
- ↑ MapView: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/MapView.html. Stand 18. November 2010.
- ↑ ItemizedOverlay: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/ItemizedOverlay.html. Stand 18. November 2010.
- ↑ OverlayItem: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/OverlayItem.html. Stand 18. November 2010.
- ↑ GeoPoint: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/GeoPoint.html, Stand 18. November 2010.
- ↑ Overlay: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/Overlay.html, Stand 18. November 2010.
ListActivity
Zurück zu Googles Android
Bei der Entwicklung von Android-Anwendungen werden Sie noch oft in die Lage kommen, bestimmte Inhalte als Liste anzeigen zu wollen oder zu müssen. Als kleiner Einstieg in die Welt der Listen soll dieses Kapitel dienen.
Einfache Darstellung
Bei einfachen Listen bestehen die einzelnen Listenelemente meist nur aus einem Eintrag bzw. einem String. Das heißt, jeder Eintrag besitzt nur eine Information wie einen Namen oder eine Bezeichnung. Für diesen Fall lässt sich die Darstellung in Android auch sehr einfach realisieren:
1. Wie immer erstellen wir ein neues Android-Projekt, welches wir diesmal SimpleListView nennen. Dabei nennen wir auch die StartActivity SimpleListView.
2. Öffnen Sie die HelloListView.java und erweitern sie mit ListActivity.[1]
public class SimpleListView extends ListActivity
3. Die onCreate()-Methode muss jetzt noch wie folgt abgeändert werden:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, CARS)); }
Wie Sie sehen können, haben wir diesmal keine setContentView()-Methode in onCreate(), da wir kein extra Layout für die Activity laden, sondern die setListAdapter()-Methode automatisch ein ListView der Activity hinzufügt. Der setListAdapter() wird dabei ein ArrayAdapter[2] übergeben, welcher einmal den Activity Context,[3] die Layoutbeschreibung für die Listenelemente und ein String-Array mit den anzuzeigenden Inhalten erhält. Als Layoutbeschreibung nehmen wir hier ein von Android vorgefertigtes Layout, das für die Darstellung von einzelnen Strings gut geeignet ist. Natürlich können auch komplexere Layouts für die einzelnen Listenelemente gebaut und verwendet werden. Wie das gemacht wird, sehen wir später in diesem Kapitel. Das String-Array zum Befüllen unserer Liste erstellen wir jetzt.
4. Fügen Sie das Array vor oder nach der onCreate()-Methode ein (wobei das reine Geschmackssache ist):
static final String[] CARS = new String[] { "Audi", "Opel", "VW", "BMW", "FIAT", "Mercedes", "SEAT", "Ferrari", "Nissan"};
Damit haben wir eine erste einfache Liste in Android erstellt. Wie man aber nun komplexere Layouts mit mehreren Strings oder auch Bildern in die Liste bringt, sehen wir jetzt.
Komplexe Darstellungen
Um Listen mit komplexeren Layouts ausstatten zu können, benötigen wir zwei Dinge: 1. Ein eigenes Layout, welches die einzelnen Listenelemente beschreibt, und 2. einen eigenen ListAdapter.
Im Folgenden wird eine Listenanwendung entstehen, in der jedes Listenelement den Vor- und Nachnamen einer Person untereinander darstellt. Wir gehen wie immer vor und erstellen ein neues Androidprojekt, welchem wir den Namen AWListView geben.
Bevor wir zu unserem Layout und ListAdapter kommen, passen wir zunächst unsere StartActivity an und fügen unserem Projekt eine neue Klasse hinzu, welche die Vor- und Nachnamen repräsentiert.
public class AWListView extends ListActivity { private AWListAdapter mAdapter; private ArrayList<AWName> mData; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initiateData(); mAdapter = new AWListAdapter(this, mData); this.setListAdapter(mAdapter); } private void initiateData(){ mData = new ArrayList<AWName>(); AWName name = new AWName("Max", "Mustermann"); mData.add(name); name = new AWName("Hans", "Wurst"); mData.add(name); name = new AWName("Struwwel", "Peter"); mData.add(name); } }
Unserer StartActivity geben wir den Namen AWListView, die schon wie beim SimpleListView durch ListActivity erweitert wird. Des Weiteren fügen wir der Activity zwei neue Attribute hinzu: Zum einen unseren eigenen ListAdapter (AWListAdapter) und eine ArrayList mit unseren Vor- und Nachnamen. In onCreate() befüllen wir zunächst unsere ArrayList mit initiateData() mit einigen Beispielnamen, damit in der Liste auch etwas angezeigt werden kann. Nach den Befüllen erstellen wir eine neue Instanz des ListAdapters und geben diesen an setListAdapater() weiter. Dadurch wird bei Start der Activity unsere Liste erzeugt und dargestellt. Bevor wir zum ListAdapter kommen, hier noch kurz, wie die Klasse AWName aufgebaut ist (da sie sehr simpel ist, spare ich mir weitere Kommentare):
public class AWName { private String mForename; private String mSurname; public AWName(String pForename, String pSurname){ mForename = pForename; mSurname = pSurname; } public void setForename(String pForename) { mForename = pForename; } public String getForename() { return mForename; } public void setSurname(String pSurname) { mSurname = pSurname; } public String getSurname() { return mSurname; } }
Da wir diesmal ein etwas komplexeres Layout haben wollen, reicht es nicht, sich ein von Android vorgefertigtes Layout wie im vorangegangenen Beispiel zu nehmen. Hierfür müssen wir wieder ein eigenes Layout definieren, welches wir row_layout.xml nennen:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/row_forename" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="20dip"/> <TextView android:id="@+id/row_surname" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="18dip"/> </LinearLayout>
Im Grunde besteht unser Layout nur aus zwei TextViews, die untereinander angeordnet sind.
Da alle Vorbereitungen getroffen sind, können wir uns nun unserem ListAdapter zuwenden.
ListAdapter
Für unseren ListAdapter benötigen wir eine neue Klasse, welche wir AWListAdapter nennen. Diese erweitern wir durch die Klasse BaseAdapter.[4]
public class AWListAdapter extends BaseAdapter
Die Klasse BaseAdapter beherbergt einige abstrakte Methoden (die aus der Super-Klasse Adapter[5] stammen), welche wir im Anschluss noch importieren müssen. Zum Inhalt der einzelnen Methoden kommen wir später. Hier erstmal nur die Methodenköpfe.
@Override public int getCount() {} @Override public Object getItem(int pPosition) {} @Override public long getItemId(int arg0) {} @Override public View getView(int pPosition, View convertView, ViewGroup parent) {}
Als Nächstes bekommt der Adapter noch zwei Attribute und seinen Konstruktor spendiert:
private ArrayList<AWName> mData = new ArrayList<AWName>(); private final LayoutInflater mLayoutInflater; public AWListAdapter(Context pContext, ArrayList<AWName> pData){ mData = pData; mLayoutInflater = (LayoutInflater) pContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); }
Mit der ArrayList mData behalten wir unsere Vor- und Nachnamen, da wir diese im Verlauf des Aufbaus unserer ListViews benötigen. Des Weiteren erstellen wir einen LayoutInflater[6] der uns später aus unserem oben erstellten Layout das dazugehörige View-Objekt generiert, auf welchem wir dann arbeiten können.
Nun machen wir uns daran, die oben genannten abstrakten Methoden zu befüllen, damit diese auch einen Zweck erfüllen können.
@Override public int getCount() { return mData.size(); }
- getCount() gibt die Anzahl der Elemente zurück, die sich in der ArrayList befinden
@Override public Object getItem(int pPosition) { return mData.get(pPosition); }
- getItem() gibt das Element an der gewünschten Stelle der ArrayList zurück
@Override public long getItemId(int arg0) { return 0; }
- getItemId() gibt normalerweise die Reihen-ID zurück, welche mit dem Element an der Position in der ArrayList in Verbindung gebracht wird. Für unseren Zweck aber uninteressant.
@Override public View getView(int pPosition, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mLayoutInflater.inflate(R.layout.row_layout, null); } ((TextView) convertView.findViewById(R.id.row_forename)).setText(((AWName)getItem(pPosition)).getForename()); ((TextView) convertView.findViewById(R.id.row_surname)).setText(((AWName)getItem(pPosition)).getSurname()); return convertView; }
- getView() ist die Methode, die aus den Daten der ArrayList die Views zusammenbaut, die wir später in dem ListView zu sehen bekommen. Zu diesem Zweck bekommt die Methode die Position des Elements, das dargestellt werden soll. Des Weiteren erhält sie einen „convertView“. Dieser kann ein bereits früher erstellter View des ListViews sein, der nun wiederverwendet bzw. recycelt werden kann und auch sollte. Das Recyceln eines solchen Views erleichtert die Darstellung von Listen um ein Vielfaches. Da man aber nicht wissen kann, ob ein convertView bereits verwendet wurde, prüft man ihn vorher, ob er NULL ist oder nicht. Bei NULL erstellen wir uns mittels LayoutInflater aus unserem Layout einen neuen View und referenzieren den convertView darauf. Danach setzen wir im convertView für die beiden TextViews' die Werte der Vor- und Nachnamen und geben den „neuen“ convertView zurück. Sollte der convertView schon von Beginn an NICHT NULL sein, können wir ihn gleich weiterbenutzen und müssen keinen neuen View aus dem Layout generieren. getView() wird so oft aufgerufen, wie getCount() an Wert zurückgegeben hat.
Wichtig zu sagen ist noch, dass bis auf getItem() keine der oben genannten Methoden von uns selbst, sondern nur von Android in Verbindung mit setListAdapter() aufgerufen werden.
Damit haben wir es geschafft! Es sollte nun eine Anwendung vorhanden sein, die einen ListView darstellt, welche ihre Daten aus einem eigenen ListAdapter erhält. Natürlich ist unser Adapter ein nur sehr kleiner. Die Layouts, die verwendet werden, können beliebig viel größer sein und/oder auch andere Elemente wie Bilder enthalten.
Don’t, Do, Even Better
Bei der Implementierung eines Adapters für einen ListView kann eigentlich nicht viel falsch gemacht werden. Aber das, was falsch gemacht werden kann, ist dann umso schwerwiegender. Dabei geht es um die Implementierung der getView()-Methode. Hier gibt es Wege, wie es auf keinen Fall gemacht werden sollte, und solche, welche sich wesentlich besser eignen.
Dieser kleine „Knigge“ für ListViews wurde auf der Google I/O 2009 im Rahmen der Keynote „Turbo-charge Your UI: How to Make Your Android UI Fast and Efficient“ [7] vorgestellt.
Don’t
@Override public View getView(int pPosition, View convertView, ViewGroup parent) { View row = (View) mLayoutInflater.inflate(R.layout.row_layout, null); ((TextView) row.findViewById(R.id.row_forename)).setText(mData.get(pPosition).getForename()); ((TextView) row.findViewById(R.id.row_surname)).setText(mData.get(pPosition).getSurname()); return row; }
So wie in diesem Beispiel sollte es nie gemacht werden. Hier wird für jedes Listenelement ein neuer View erzeugt, mit Daten gefüttert und an den ListView zurückgegeben. Das bedeutet, dass für jeden dieser neuen Views natürlich neuer Speicherplatz belegt werden muss, was bei einer großen Anzahl von Listenelementen schnell den Speicher verstopft und auch die Darstellung stark verlangsamt.
Do
@Override public View getView(int pPosition, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mLayoutInflater.inflate(R.layout.row_layout, null); } ((TextView) convertView.findViewById(R.id.row_forename)).setText(((AWName)getItem(pPosition)).getForename()); ((TextView) convertView.findViewById(R.id.row_surname)).setText(((AWName)getItem(pPosition)).getSurname()); return convertView; }
Dieses Beispiel stellt die optimale Lösung dar. Hierbei wird zuerst getestet, ob bereits ein View für ein Listenelement erstellt wurde, welches dann gegebenenfalls recycelt werden kann. Nur wenn noch kein View existiert, wird ein neuer erzeugt, der dann mit Daten gefüttert und zurückgegeben wird.
Even Better
Es gibt aber eine Möglichkeit, seinem ListView noch einen kleinen Geschwindigkeitsschub zu geben beziehungsweise ihn noch ressourcenschonender zu machen. Hierzu benötigt man eine kleine, aber feine Zusatzklasse:
static class ViewHolder { TextView forename; TextView surname; }
@Override public View getView(int pPosition, View convertView, ViewGroup parent) { ViewHolder holder; if(convertView == null){ convertView = mLayoutInflater.inflate(R.layout.row_layout, null); holder = new ViewHolder(); holder.forename = (TextView) convertView.findViewById(R.id.row_forename); holder.surname = (TextView) convertView.findViewById(R.id.row_surname); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.forename.setText(mData.get(pPosition).getForename()); holder.surname.setText(mData.get(pPosition).getSurname()); return convertView; }
Neben dem Recyceln der Views muss nun auch nicht mehr die Referenz auf die einzelnen Views innerhalb des Listenelements gesucht werden. Diese werden im ViewHolder-Objekt gespeichert und als Tag dem Listenelement mitgegeben werden. So wird beim Recyceln des Listenelements dessen Tag ausgelesen und man spart sich den Aufruf der findViewById()-Methode.
Performance
-
1. Performance durch einzelne Techniken
Durch das Recyceln der Views bzw. durch Verwendung eines ViewHolders lassen sich neben dem verringerten Speicherbedarf auch Verbesserungen in der Darstellung einer Liste feststellen. Welche Auswirkungen das Recyceln und die ViewHolder auf die Frames Per Second (FPS) haben, zeigt Abbildung 1. Beim heutigen Stand der mobilen Technik, sprich der Größe des Speichers und der schnellen Prozessoren, fallen einem diese Unterschiede erst bei einer gewissen beziehungsweise großen Anzahl von Listenelementen auf, was nicht bedeuten soll, dass auch bei definitiv geringer Anzahl an Elementen in der Liste schlechter Code geschrieben werden darf.
Daten ändern
Sicherlich wird es dazu kommen, dass die Daten, die der Liste zur Verfügung stehen, sich ändern. Das heißt, dass einige Daten hinzukommen oder wegfallen. Dann will man auch, dass sich diese Änderung in der Liste widerspiegeln. Zu diesem Zweck ändert man im Adapter die zugrunde liegenden Daten. Wichtig dabei zu beachten ist, dass man dem Adapter mitteilt, dass sich etwas an den Daten geändert hat. Ansonsten ist er etwas sauer auf Sie und beendet einfach die Anwendung. :)
Hier ein kleines Beispiel, wie Sie ihn beschwichtigen können:
public void changeData(ArrayList<AWName> pData){ mData = pData; this.notifyDataSetChanged(); }
Zum einen wird dem Adapter eine neue (oder auch nur aktualisierte Liste) mit den geänderten Daten übergeben. Daraufhin muss dann noch die Methode notifyDataSetChanged() aufgerufen werden, um dem Adapter zu signalisieren, dass sich etwas an den Daten geändert hat. Hierdurch wird die gesamte Liste neu zusammengestellt und angezeigt.
Einzelnachweise
- ↑ ListActivity: http://developer.android.com/reference/android/app/ListActivity.html. Stand 7. Januar 2011.
- ↑ ArrayAdapter: http://developer.android.com/reference/android/widget/ArrayAdapter.html. Stand 7. Januar 2011.
- ↑ Context: http://developer.android.com/reference/android/content/Context.html. Stand 7. Januar 2011.
- ↑ BaseAdapter: http://developer.android.com/reference/android/widget/BaseAdapter.html. Stand 7. Januar 2011.
- ↑ Adapter: http://developer.android.com/reference/android/widget/Adapter.html. Stand 7. Januar 2011.
- ↑ LayoutInflaterhttp://developer.android.com/reference/android/view/LayoutInflater.html, Stand 7. Januar 2011,
- ↑ Turbo-charge Your UI: How to Make Your Android UI Fast and Efficient
Zusammenfassung des Projekts
Zurück zu Googles Android
- Zielgruppe:
- Für Leute mit Programmiererfahrung in Java
- Lernziele:
- Am Ende dieses Buches soll der Leser dazu fähig sein, die Architektur des Android-Betriebssystems zu verstehen und Android-Anwendungen zu schreiben.
- Buchpatenschaft/Ansprechperson: Zur Zeit niemand, das Buch darf gerne übernommen werden.
- Sind Co-Autoren gegenwärtig erwünscht? jeder ist willkommen
- Richtlinien für Co-Autoren:
- Themenbeschreibung:
- Dieses Buch beschäftigt sich mit den Elementen und der Programmierung des Android-Betriebssystems.