Mittwoch, 6. April 2011

Android BallApp - Iteration 2 - Laden der Ballliste aus einer SQLite Datenbank

Hier nun endlich die nächste Iteration meiner Android BallApp.

Ziel dieser Iteration:

Laden der Ballliste aus einer Datenbank

Mit dieser Iteration werfe ich zunächst meinen ehrgeizigen Ansatz über Bord, meine Applikation test-getrieben zu entwickeln. Stattdessen versuche ich entsprechende Tests im Nachgang zu ergänzen. So fühlt sich die Einarbeitung in komplett neue Themen besser an.

Im Data Storage Developer Guide findet sich eine Dokumentation welche Data Storage Optionen zur Auswahl stehen:

  • Shared Preferences zum Speichern einfacher Daten in Form von Key-Value-Paaren.
  • Internal Storage zum Speichern privater Daten im Gerätespeicher.
  • External Storage zum Speichern öffentlicher Daten in einem externen Speicherbereich, der auch für andere Applikationen zugänglich ist.
  • SQLite Databases zum Speichern strukturierter Daten in einer privaten Datenbank.
  • Network Connection zum Speichern von Daten im Web über einen entsprechenden Network Service.

Für meine Ballliste wähle ich eine SQLite Datenbank. Die Zugriffe auf die Datenbank kapsele ich in einer Klasse BallDbAdapter. Entsprechend der empfohlenen Vorgehensweise im Developer Guide leite ich eine private DatabaseHelper Klasse von SQLiteOpenHelper ab. Darüber erhalte ich innerhalb des BallDbAdapters die eigentliche SQLiteDatabase.

private static class DatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "balldb";
    private static final int DATABASE_VERSION = 1;

    DatabaseHelper(Context context) {
      super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
}

Eine SQLite Datenbank erhält einen eindeutigen Namen (balldb) sowie eine Versionsnummer (1) Auf die gehe ich gleich noch ein.

onCreate

Jetzt, wo ich Zugriff auf eine Datenbank-Instanz habe, stellt sich die Frage, wie definiere ich deren Struktur?
Dazu wird in der DatabaseHelper Klasse die abstrakte Methode onCreate der SQLiteOpenHelper Klasse überschrieben. Diese Methode wird aufgerufen, wenn die Datenbank zum ersten mal geöffnet wird. Hierin erfolgt die Initialisierung einer neuen Datenbank über einen Create String der in meinem Fall zunächst eine Tabelle baelle mit zwei Spalten _id und full_name definiert.

@Override
    public void onCreate(SQLiteDatabase db) {
      db.execSQL(databaseCreateString());
    }

    private String databaseCreateString() {
      StringBuilder builder = new StringBuilder();
      builder.append("CREATE TABLE baelle ( ");
      builder.append("_id INTEGER PRIMARY KEY AUTOINCREMENT, ");
      builder.append("full_name TEXT NOT NULL );");
      return builder.toString();
    }

onUpgrade und onDowngrade

Neben der onCreate Methode definiert die SQLiteOpenHelper Klasse weiterhin die abstrakte Methode onUpgrade die mein DatabaseHelper demnach überschreiben muss. Die onUpgrade Methode wird aufgerufen, wenn die Datenbank Versionsnummer, die im Konstruktor übergeben wird, größer ist als die Versionsnummer der bestehenden Datenbank. Das kann der Fall sein, wenn die Applikation aktualisiert wurde und danach zum ersten Mal wieder gestartet wird. Die Methode bekommt die bestehende und die aktuelle Versionsnummer übergeben. Sie ist dafür zuständig die notwendigen Änderungen durchzuführen um die bestehende Datenbank auf die neue Version anzuheben.

Analog zur onUpgrade Methode stellt die SQLiteOpenHelper Klasse auch eine onDowngrade Methode zur Verfügung. Diese wird dementsprechend aufgerufen wenn die aktuelle Datenbank Version der Applikation kleiner ist als die bereits vorhandene Datenbank. Die Methode ist in der SQLiteOpenHelper Klasse nicht als abstrakt definiert und muss daher auch in meiner DatabaseHelper Klasse nicht implementiert werden.

onOpen

Die onOpen Methode wird aufgerufen wenn die Datenbank geöffnet wird (beim erneuten Aufruf der Applikation). Sie ist ebenfalls nicht als abstrakt definiert.


Definition von Testdaten

Da meine BallApp noch keine Funktionalität besitzt um neue Datensätze zu erzeugen, muss ich nach wie vor ein paar Ball-Datensätze fest hinterlegen. Dafür definiere ich mir eine Methode fillTestData, die ich aus der onCreate Methode aufrufe.


private void fillTestData(SQLiteDatabase db) {
      String insert = "INSERT INTO baelle ( _id, full_name ) " + 
                      "VALUES ( ?, ? );";
      db.execSQL(insert, new String[] { 
            "1", "mg Maier Magic 2" });
      db.execSQL(insert, new String[] { 
            "2", "Nifo 2" });
      db.execSQL(insert, new String[] { 
            "3", "Reisinger Bo 1" });
    }

Damit ich während der weiteren Entwicklung beim Neustart meiner Applikation immer einen sauberen Datenbank-Stand erhalte, egal was ich geändert habe, ergänze ich schließlich noch die Methoden onUpgrade und onOpen. In beiden Methoden lösche ich jeweils die Tabelle baelle und erzeuge sie anschließend neu indem ich die onCreate Methode aufrufe.


@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, 
                          int newVersion) {
      Log.w(LOG_TAG, "Upgrading database from version " + 
            oldVersion + " to " + newVersion + ", which will " +
            " destroy all old data");
      db.execSQL("DROP TABLE IF EXISTS baelle");
      onCreate(db);
    }

    @Override
    public void onOpen(SQLiteDatabase db) {
      Log.w(LOG_TAG, "Opening database -> destroy all old " +
            "data");
      db.execSQL("DROP TABLE IF EXISTS baelle");
      onCreate(db);
    }


Öffnen und Schließen der Datenbank

Die Implementierung der privaten DatabaseHelper Klasse ist damit zunächst vollständig. Im BallDbAdapter fehlen allerdings noch ein paar Methoden. Zunächst muss die Datenbank geöffnet werden, um damit zu agieren. Dafür erstelle ich die Methode open. Die Methode initialisiert zunächst eine Instanz des DatabaseHelper. Anschließend wird über diese die eigentliche SQLiteDatabase im Lesemodus (readable) geöffnet und zurückgegeben.

public void open() throws SQLException {
      databaseHelper = new DatabaseHelper(context);
      database = databaseHelper.getReadableDatabase();
    }


Zum Schließen der Datenbank dient die Methode close. Diese ruft lediglich die entsprechende Methode des DatabaseHelper auf.

public void close() {
      databaseHelper.close();
    }


Abfrage der Bälle

Schließlich fehlt mir noch eine Methode, die mir die in der Datenbank gespeicherten Bälle ausliest und zurückliefert.

public Cursor findAllBalls() {
      return database.query(TABLE_BAELLE,
      new String[] { KEY_BALL_ID, KEY_BALL_FULL_NAME },
      null, null, null, null, KEY_BALL_FULL_NAME);
    }

Die query Methode der SQLiteDatabase erwartet die üblichen Teile eines SQL SELECT-Statements in Form von einzelnen Parametern.

  • Als ersten Parameter erhält die Methode den Namen der Tabelle aus welcher die Daten abgefragt werden sollen. In unserem Fall ist das die Tabelle baelle (welche auch sonst, wir haben ja nur die eine). Den Tabellennamen habe ich zwischenzeitlich in eine Konstante TABLE_BAELLE ausgelagert.
  • Der zweite Parameter definiert die zu selektierenden Spalten der Tabelle. Die Spaltennamen werden in Form eines String-Arrays übergeben. Auch für die Spalten habe ich Konstanten definiert.
  • Der dritte Parameter erwartet einen Selection-String, d.h. eine SQL WHERE-Klausel (ohne das Schlüsselwort WHERE). In meinem Fall möchte ich die zu selektierenden Daten nicht einschränken, ich übergebe daher null.
  • Im zuvor beschriebenen Selection-String kann ich ? als Platzhalter für einzelne Argumente verwenden. Die für die Platzhalter zu verwendenden Werte werden im vierten Parameter in Form eines String-Arrays erwartet. Da ich keinen Selection-String benötige, benötige ich auch keine Argumente dafür und übergebe hierfür ebenfalls null.
  • In Parameter fünf kann ich einen SQL GROUP BY Ausdruck definieren (ohne das Schlüsselwort GROUP BY). Ich übergebe ebenfalls null.
  • Parameter sechs erlaubt die Definition einer SQL HAVING Klausel zum vorangegangenen GROUP BY (ohne das Schlüsselwort HAVING). Auch hier übergebe ich null.
  • Der siebte Parameter erwartet schließlich einen ORDER BY Ausdruck (ohne das Schlüsselwort ORDER BY). Hier übergebe ich die Spalte mit dem Ball-Namen.

Die Methode gibt einen Datenbank Cursor zurück, über den die einzelnen Datensätze abgerufen werden können. Diesen Cursor gebe ich als Ergebnis meiner findAllBalls Methode zurück.

Anpassung der BallListActivity

Nachdem ich alle Methoden zum Zugriff auf die Datenbank definiert habe, muss ich sie nur noch verwenden. Ich passe dazu die BallListActivity wie folgt an.

In der onCreate Methode initialisiere ich den BallDbAdapter und öffne darüber die Datenbank.

/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      ballDbAdapter = new BallDbAdapter(this);
      ballDbAdapter.open();
      setListAdapter(getBallListAdapter());
    }

In der onDestroy Methode schließe ich die Datenbank wieder.

@Override
    protected void onDestroy() {
      super.onDestroy();
      ballDbAdapter.close();
    }

Schließlich überarbeite ich die getBallListAdapter Methode, so dass diese die Ball-Liste aus der Datenbank lädt. Zuerst hole ich mir über den BallDbAdapter den Cursor mit den Ball-Datensätzen. Anschließend teile ich in der Activity mit, dass sie den Cursor managen soll. D.h. wenn die Activity angehalten wird, gibt sie automatisch den Cursor frei. Wird die Activity fortgesetzt, führt sie automatisch ein Requery auf dem Cursor aus.

Cursor cursor = ballDbAdapter.findAllBalls();
      startManagingCursor(cursor);

Anschließend erzeuge ich einen SimpleCursorAdapter, dem ich die ListItem-Layout Definition sowie den Cursor übergeben. Außerdem muss ich noch das Mapping der Spalten im Cursor auf die View-Elemente im ListItem definieren. Dafür erzeuge ich ein String-Array mit den zu mappenden Cursor-Spalten sowie ein int-Array mit den IDs der View-Elemente. Dazu muss ich zuvor in der Layout-Definition meines Balllist-Items dem TextView Element eine ID zuweisen.

String[] from = new String[] { 
            BallDbAdapter.KEY_BALL_FULL_NAME };
    int[] to = new int[] { R.id.balllist_item_fullname };

    SimpleCursorAdapter baelle = new SimpleCursorAdapter(this, 
        R.layout.balllist_item, cursor, from, to);

Den erzeugten Adapter gebe ich zurück, so dass dieser als ListAdapter der ListActivity übergeben wird.

Das war's. Meine BallApp basiert damit auf einer internen SQLite Datenbank.

Der vollständige Source Code kann unter https://github.com/bobbyout/ballapp-android abgerufen werden.

Keine Kommentare: