Googles Android/ 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
[Bearbeiten]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
[Bearbeiten]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
[Bearbeiten]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
[Bearbeiten]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
[Bearbeiten]@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
[Bearbeiten]@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
[Bearbeiten]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
[Bearbeiten]-
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
[Bearbeiten]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
[Bearbeiten]- ↑ 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