Donnerstag, 14. April 2011

Android BallApp - Iteration 3 - Einlesen der Ballliste aus einer JSON-Datei

Ziel der dritten Iteration meiner Android BallApp:

Einlesen der vollständigen Ball-Liste in die SQLite Datenbank

Die vollständige Ball-Liste verwalte ich bisher in einer Excel-Datei.

Frage 1: Wie bekomme ich die Excel-Daten in die Android-Applikation?

Die Antwort ist einfach: Die Excel-Daten werden in eine ASCII-Datei exportiert und als Resource (in Android als Assets bezeichnet) ins Projekt kopiert.

Zur Speicherung der strukturierten Ball-Daten in einer ASCII-Datei wähle ich ein JSON-Datenformat. JSON erlaubt genau wie XML die Definition einer eigenen Datenstruktur, benötigt dafür aber deutlich weniger Platz, da die schließenden Tags wegfallen. Außerdem ist JSON ein gängiges Datei-Format und kann von Android gut verarbeitet werden (wie wir später noch sehen werden).

Die JSON-Daten lassen sich mit Excel gut generieren. Auf Details dazu will ich jetzt hier nicht näher eingehen, wen das interessiert, der möge sich bitte melden. Als kleiner Tipp: die Excel-Funktion VERKETTEN hilft.

Die ASCII-Datei ballliste.json sieht wie folgt aus:

[{hrst: "3D", name: "BOF BM 2007 Catherine Massem", gr: "k", 
         l: "l", s: "7", h: "", gw: "41"},
    {hrst: "3D", name: "BOF BMM 2008 GSP Malonne", gr: "k", 
        l: "l", s: "15", h: "", gw: "42"},
    {hrst: "3D", name: "BOF DkJM 2005 Oliver Rex Wiile", gr: "k", 
        l: "l", s: "1", h: "", gw: "40"},
    ...
    {hrst: "", name: "T4", gr: "k", 
        l: "r", s: "79", h: "", gw: "30"}]

Jedes Ball-Element besteht aus einer komma-separierten Liste von Name-Wert-Paaren und wird begrenzt von geschweiften Klammern. Die eckigen Klammern am Anfang und Ende definieren die Liste der einzelnen Ball-Elemente, ebenfalls komma-separiert.

Die JSON-Datei lege ich im Verzeichnis /assets in meinem Android-Projekt ab.

Frage 2: Wie lese ich die ASCII-Datei ein?

Das Einlesen von Dateien erfolgt in Android weitestgehend analog zu Java - genauso umständlich.

Den Zugriff auf die Asset-Datei erhalte ich über den Android Context, also über meine Activity. Über die Methode getAssets() bekomme ich den AssetManager. Dieser stellt mir wiederum die Methode open(String fileName) zur Verfügung, über die ich Asset-Dateien, also Dateien die im Verzeichnis /assets abgelegt sind, öffnen kann. Ich erhalte als Ergebnis einen InputStream.

Um die Datei einigermaßen komfortabel einlesen zu können, kapsele ich den InputStream in einen InputStreamReader und diesen wiederum in einen BufferedReader. So kann ich den Inhalt der Datei zeilenweise einlesen und in einem StringBuffer sammeln.

private String readJSONBallliste(Context context) {
      StringBuffer content = new StringBuffer();
      try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(
                context.getAssets().open("ballliste.json")));
        String nextLine = reader.readLine();
        while (nextLine != null) {
          content.append(nextLine).append("\n");
          nextLine = reader.readLine();
        }
      } catch (IOException e) {
        Log.e(LOG_TAG, "Fehler beim Lesen der JSON-Datei", e);
      }
      return content.toString();
    }

Frage 3: Wie parse ich die JSON-Daten?

Das parsen der JSON-Daten ist in Android relativ einfach. Android bringt dafür die notwendigen Klassen im Package org.json bereits mit.

Bei meinem JSON-String handelt es sich um eine Liste von Objekten. Daher verwende ich ein JSONArray welches ich mit dem JSON-String initialisiere. Das JSONArray stellt die üblichen Methoden eines Arrays bzw. einer Liste bereit, die ich verwenden kann um durch die Liste zu iterieren. Leider implementiert das JSONArray kein Iterator oder Iterable Interface, so dass ich den altmodischen Weg gehen muss.

JSONArray jsonArray = new JSONArray(
        readJSONBallliste(context));
    for (int i = 0; i < jsonArray.length(); i++) {
      JSONObject nextBall = (JSONObject) 
          jsonArray.getJSONObject(i);
      Log.d(LOG_TAG, nextBall.getString("hrst") + 
          " - " + nextBall.getString("name"));
      ...
    }
Über die Methode getJSONObject(int index) lassen sich die einzelnen Ball-Elemente aus dem JSONArray auslesen. Das JSONObject stellt eine Reihe von Methoden zur Verfügung um zu einem Attribut-Namen den entsprechenden Wert abzufragen. Schreiben der JSON-Daten in die SQLite Datenbank Jetzt muss ich nur noch die Daten aus den JSONObjecten verarbeiten indem ich sie in die Datenbank schreibe. Die vollständige Methode dazu sieht wie folgt aus:
private void createData(SQLiteDatabase db) {
      Log.d(LOG_TAG, "Create Data...");
      String insert = "INSERT INTO " + TABLE_BAELLE + 
          " ( " + KEY_BALL_ID + ", " + KEY_BALL_FULL_NAME + 
          " ) VALUES ( ?, ? );";
      try {
        JSONArray jsonArray = new JSONArray(
            readJSONBallliste(context));
        for (int i = 0; i < jsonArray.length(); i++) {
          JSONObject nextBall = (JSONObject) 
              jsonArray.getJSONObject(i);
          Log.d(LOG_TAG, nextBall.getString("hrst") + 
              " - " + nextBall.getString("name"));
          db.execSQL(insert, new String[] { 
              String.valueOf(i), 
              nextBall.getString("name") });
        }
      } catch (JSONException e) {
        Log.e(LOG_TAG, "Fehler beim parsen der JSON-Daten", e);
      }
    }
Ich rufe die Methode beim initialen Anlegen der Datenbank auf, also in der onCreate(SQLiteDatabase db) Methode meines BallDbAdapters. Dem muss ich beim Initialisieren aus der Activity noch die Activity als Context mitgeben, damit ich Zugriff auf den AssetManager habe. Nach dem starten meiner App wird die Ball-Liste in die Datenbank geschrieben und die Liste mit den Ball-Namen wird angezeigt. Der vollständige Source Code kann wieder unter https://github.com/bobbyout/ballapp-android abgerufen werden.

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.