Googles Android/ 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 Bearbeiten

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) Bearbeiten

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 Bearbeiten

  1. http://developer.android.com/guide/topics/providers/content-providers.html. Stand 12. November 2010.
  2. Becker, Arno; Pant, Marcus: „Android – Grundlagen und Programmierung“, dpunkt.verlag, 1. Auflage 2009. Seite 189 ff.
  3. http://developer.android.com/reference/android/provider/package-summary.html. Stand 12. November 2010.
  4. http://developer.android.com/reference/android/content/ContentResolver.html. Stand 12. November 2010.
  5. http://developer.android.com/reference/android/database/Cursor.html. Stand 12. November 2010.