Freitag, 14. Januar 2011

Android BallApp - Iteration 1 - Einfügen einer ListView

Seit meinem letzten Beitrag „Einrichten einer Android Entwicklungsumgebung und Aufsetzen eines Projektes“ ist eine ganze Weile vergangen. Eigentlich hatte ich vor die darin generierte Projektstruktur etwas zu beschreiben, aber irgendwie macht es mehr Spaß weiter zu entwickeln J. Vielleicht hole ich das später noch mal nach. Heute will ich aber endlich mal richtig loslegen.

Das Ziel für die zweite Iteration:

Anzeige einer Ball-Liste

Im ersten Schritt möchte ich die zuvor angelegte Anwendung um eine einfache Liste mit ein paar Ball-Namen erweitern. Die Ball-Namen werde ich zunächst fix in der Anwendung hinterlegen. Eine dynamische Liste aus einer Datenbank werde ich erst in einer späteren Iteration in Angriff nehmen.

Ich arbeite testgetrieben, daher ist das erste, was ich erstelle ein Test in meinem Test-Projekt. Im Package de.mandry.android.ballapp.test erstelle ich eine neue Klasse. Als Namen der Klasse wähle ich BallListTest. Die Klasse wird abgeleitet vom android.test.ActivityInstrumentationTestCase2 parametrisiert mit meiner Activity BallAppActivity.

Die erzeugte Test-Klasse weist zunächst noch Kompilierungsfehler auf. Die BallAppActivity muss importiert werden. Außerdem muss ein Default-Konstruktor definiert werden, welcher den Konstruktor der Super-Klasse aufruft und diesem das Package „de.mandry.android.ballapp“ sowie die Klasse der zu testenden Activity BallAppActivity.class übergibt.

Die kompilierbare Test-Klasse sieht also wie folgt aus:


package de.mandry.android.ballapp.test;

import android.test.ActivityInstrumentationTestCase2;
import de.mandry.android.ballapp.BallAppActivity;

/**
 * Testet die Ball-Liste des BallApp.
 * @author Torsten Mandry
 */
public class BallListTest extends 
       ActivityInstrumentationTestCase2< BallAppActivity > {

  /**
   * Default Konstruktor.
   */
  public BallListTest() {
    super( "de.mandry.android.ballapp", BallAppActivity.class );
  }

}


Als nächstes ergänze ich eine Setup-Methode, die mir die zu testende BallAppActivity in einer Klassen-Variable ablegt. Dazu rufe ich die getActivity() Methode der Super-Klasse ActivityInstrumentationTestCase2 auf. Über den Konstruktor-Aufruf habe ich der Klasse zuvor mitgeteilt, welchen Activity-Typ ich in diesem Fall zurückerwarte.

  /** Die BallApp Activity unter Test. */
  private BallAppActivity ballAppActivity;


  /**
   * Setup-Methode, holt die Activity.
   */
  @Override
  protected void setUp() throws Exception {
    super.setUp();
    ballAppActivity = this.getActivity();
  }


Da ich die Zeit nicht abwarten kann versuche ich schon mal die Test-Klasse auszuführen (Run As - Android JUnit Test). Falls noch nicht erfolgt startet Eclipse jetzt das bereits im vorangegangenen Beitrag eingerichtete Virtual Device und installiert die BallApp darin. Anschließend versucht es die Tests auszuführen und bricht mit einer Fehlermeldung ab:

Test run failed: Test run incomplete. Expected 1 tests, received 0

OK, ich brauche mindestens einen Test, das macht Sinn! Ich ergänze also einen minimalen Test, der sicherstellt dass die Activity vorhanden ist. Es ist zu beachten, dass das Android JUnit keine Annotationen kennt, wie sie im Java JUnit mittlerweile üblich sind. Das heißt, dass die Test-Methoden einer TestCase-Klasse wie ehemals in Java durch den Präfix „test“ im Methodennamen gekennzeichnet werden.

  /**
   * Initialer Test, stellt sicher dass die BallAppActivity
   * gesetzt ist.
   */
  public void testActivityExists() {
    assertNotNull( ballAppActivity );
  }


Ich starte den Test noch einmal. Die Tests laufen erfolgreich durch und ich erhalte den begehrten grünen Balken. Super!

Dann kann ich jetzt ja endlich anfangen mein erstes Feature zu entwickeln.

Nach dem TDD-Ansatz schreibe zunächst einen weiteren Test. Ich will sicherstellen, dass meine BallApp eine Liste anzeigt. Dazu prüfe ich zunächst, ob meine Activity eine View mit einer entsprechenden Id enthält. Die Id erwarte ich in der generierten Klasse R im AndroidBallApp-Projekt. Die Id ist aktuell noch nicht vorhanden, daher wird mir diese als Kompilierfehler angemahnt.


  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    assertNotNull( ballAppActivity.findViewById( de.mandry.
                       android.ballapp.R.id.ballList ) );
  }


Die Klasse R enthält Konstanten für alle innerhalb eines Projektes verwendeten IDs, Strings, Layouts und Grafikelemente. Sie wird vom Eclipse ADT-Plugin automatisch ergänzt sobald neue Elemente hinzukommen oder wegfallen. So wird z.B. automatisch eine neue ID-Konstante hinzugenommen, sobald ich in einem Layout eine neue View mit einer neuen ID ergänze und dieses speichere. Dieses Vorgehen finde ich ziemlich gut, da ich so gar nicht erst in die Versuchung komme, in meinen Klassen der Einfachheit halber „erst mal“ die Id als String einzutragen.

Die einfachste Möglichkeit den oben geschriebenen Test „grün“ zu bekommen ist, im main-Layout das vorhandene TextView-Element mit einer entsprechenden Id zu versehen.

<TextView android:id="@+id/ballList"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/hello"
    />


Nach dem Speichern des Layouts verschwindet der Kompilierfehler im Test. Der Test kann erfolgreich ausgeführt werden und ist grün. Der Test ist aber noch nicht vollständig. Ich habe zwar ein View-Objekt mit der entsprechenden Id, es handelt sich aber nicht um eine Liste. Daher ergänze ich den Test um eine Prüfung auf den View-Typ.

  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    View ballListView = ballAppActivity.findViewById( de.mandry.
                            Android.ballapp.R.id.ballList );
    assertNotNull( ballListView );
    assertEquals( ListView.class, ballListView.getClass() );
  }

Damit schlägt der Test wieder fehl und ich habe einen Grund weiter zu machen.

Ich will das TextView-Element im main-Layout durch ein ListView-Element ersetzen. Nähere Einzelheiten zur ListView entnehme ich dem ListView Abschnitt im Hello Views Tutorial der Android Developers Seite.


Das erste was ich lerne ist: So einfach ist das nicht!

ListViews werden anscheinend nicht innerhalb des XML Layouts definiert, sondern programmatisch in der Activity. Das im Tutorial behandelte Beispiel passt ziemlich gut zu meinem heutigen Ziel, also gehe ich frohen Mutes an die Programmierung.

Als erstes lösche ich das TextView-Element aus meinem main-Layout. Das scheine ich ja nicht zu brauchen. Es erscheint der bekannte Kompilierfehler wieder in meinem Test, da ich damit auch gleichzeitig die ID-Konstante aus der R-Klasse entfernt habe. Ich ignoriere das erst mal und mache weiter.

Wie im ListView-Tutorial beschrieben lege ich zuerst eine neue Layout-Definitionsdatei balllist_item.xml an. Für den Anfang übernehme ich die Definition aus dem Tutorial. Dann leite ich meine BallAppActivity von der Superklasse ListActivity ab. Dabei fällt mir der ungeschickte Name meiner Klasse auf, den lasse ich aber zunächst so. Ein entsprechendes Refactoring führe ich später durch, wenn mein Test grün ist.

Den Aufruf setContentView( R.layout.main ) im Konstruktor meiner ListActivity entferne ich. Die ListActivity setze ich laut Tutorial über die Methode setListAdapter(…). Den in dieser Methode zu übergebenen ArrayAdapter erzeuge ich in einer eigenen Methode getBallListAdapter().

Meine Activity-Klasse sieht damit wie folgt aus:

public class BallAppActivity extends ListActivity {
 
  /** Called when the activity is first created. */
  @Override
  public void onCreate( Bundle savedInstanceState ) {
    super.onCreate( savedInstanceState );
    setListAdapter( getBallListAdapter() );
  }

  /**
   * Gibt den BallListAdapter zurück.
   * @return der BallListAdapter.
   */
  private ListAdapter getBallListAdapter() {
    String[] baelle = new String[] {
      "3D type 543 ml",
      "mg Maier Classic 1",
      "Nifo 2"
    };
    return new ArrayAdapter< String >( this,
                  R.layout.balllist_item, baelle );
  }
}


Ich führe das AndroidBallApp-Projekt manuell als Android Application auf dem Virtual Device aus und sehe mir das Ergebnis an. Sieht gut aus!



Mein Test läuft allerdings noch nicht, der Kompilierfehler ist immer noch da (irgendwie verliere ich beim TDD immer wieder die Orientierung L ). Da ich die ListView nun mit der ListActivity schon mitbekomme, habe ich keinen Einfluss auf deren ID. Ich muss mir die ListView auf einem anderen Weg herholen. Ich schreibe meinen Test also wie folgt um:

  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    View ballListView = ballAppActivity.getListView();
    assertNotNull( ballListView );
    assertEquals( ListView.class, ballListView.getClass() );
  }


Der Kompilierfehler ist wieder weg und mein Test ist grün. Fertig!

Nein, da war doch noch was…?! Richtig, ich wollte die Activity noch umbenennen. Also werde ich mal die Eclipse Refactoring Tools zusammen mit Android ausprobieren. Ich wähle den Klassennamen der BallAppActivity aus und wähle über‘s Kontext-Menü „Refactor“ -> „Rename“. Ich gebe den neuen Namen BallListActivity ein und bestätige ihn mit Return. Auf den ersten Blick sieht alles gut aus. Nach dem ausführen der Tests weiß ich es jedoch besser, beide Tests schlagen fehl. Bei genauerem Hinsehen entdecke ich in den Consolen-Ausgaben eine Fehlermeldung:

Error in an XML file: aborting build.

Welche XML-Datei könnte damit gemeint sein? Die Layout-Definitionen sehr wahrscheinlich nicht. Bleibt eigentlich nur noch die AndroidManifest.xml. Bingo! Hier wird nach wie vor auf die BallAppActivity verwiesen, die gibt es nicht mehr. Nachdem dieser Fehler korrigiert ist laufen auch die Tests wieder.

<activity android:name=".BallListActivity"
          android:label="@string/app_name">


Sieht so aus, als wenn ich mein Tagesziel erreicht hätte. Hier noch mal die vollständige BallListActivity und der zugehörige Test.

package de.mandry.android.ballapp;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;

public class BallListActivity extends ListActivity {

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

  /**
   * Gibt den BallListAdapter zurück.
   * @return der BallListAdapter.
   */
  private ListAdapter getBallListAdapter() {
    String[] baelle = new String[] {
      "3D type 543 ml",
      "mg Maier Classic 1",
      "Nifo 2"
    };
    return new ArrayAdapter< String >( this,
                   R.layout.balllist_item, baelle );
  }
}



package de.mandry.android.ballapp.test;

import android.test.ActivityInstrumentationTestCase2;
import android.view.View;
import android.widget.ListView;
import de.mandry.android.ballapp.BallListActivity;

/**
 * Testet die Ball-Liste der BallApp.
 * @author Torsten Mandry
 */
public class BallListTest extends
    ActivityInstrumentationTestCase2< BallListActivity > {

  /** Die BallListActivity unter Test. */
  private BallListActivity ballListActivity;

  /**
   * Default Konstruktor.
   */
  public BallListTest() {
    super( "de.mandry.android.ballapp", 
           BallListActivity.class );
  }

  /**
   * Setup-Methode, holt die Activity.
   */
  @Override
  protected void setUp() throws Exception {
    super.setUp();
    ballListActivity = this.getActivity();
  }

  /**
   * Initialer Test, stellt sicher dass die BallAppActivity 
   * gesetzt ist.
   */
  public void testActivityExists() {
    assertNotNull( ballListActivity );
  }

  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    View ballListView = ballListActivity.getListView();
    assertNotNull( ballListView );
    assertEquals( ListView.class, ballListView.getClass() );
  }
}


Keine Kommentare: