Wenn man sich mit Android-UI Design beschäftigt, stößt man früher oder später auf Google IO Talks, die sich damit beschäftigen wie eine gute Android App aussehen und sich anfühlen soll.
So wird in diesem Talk davon gesprochen, das eine gute App folgende Eigenschaften haben sollte:
- Fresh – Apps sollen sich immer auf aktuelle Daten beziehen, sie soll “immer Up to Date” sein.
- Psychic – Apps sollen die System-Ressourcen und das Android-Framework verwenden um Informationen über den Benutzer zu verwenden um dem Benutzer Arbeit zu ersparen. In solche Kategorien fallen z.B. Scrobbling und Vorschläge bei last.fm, automatisch in den Energiesparmodus nach 22 Uhr etc.
- Adaptiv – Die App existiert für den Benutzer nur als Kontext in dem er etwas erledigen möchte. Das bedeutet, dass die App niemals schlecht auffallen darf, z.B. wenn viel manuelle Texteingabe verlangt wird oder ähnlich “unmobile” Anforderungen in der UI gestellt werden. Die App soll wie die Unterhose sein: Hübsch wenn man sie sich bewusst anschaut aber im Alltag unsichtbar und bequem.
- Smooth – Die App soll schnell, flüssig und angenehm bzw. responsive reagieren.
Mit dem letzten Punkt möchte ich mich in diesem kleinen Artikel beschäftigen. Doch zu aller erst die Fragen: Wie werden Anwendungen langsam, zäh und warum frieren sie ein?
Normalerweise werden im onCreate einer Activity-Klasse alle Objekte die während der Lebenszeit der Activity verwendet werden müssen initialisiert.
Das sind zum einen die relativ günstigen findView-Aufrufe und auf der anderen Seiten Objekte wie ListView-Adaptars, EventHandler, Code um auf Server zuzugreifen, Code um auf die SQLite-DB zuzugreifen, etc. Die Manigfaltigkeit der Performance-Fresser ist erschlagend groß.
Dazu kommen noch andere Todfeinde der Android-Smoothness:
- Der Garbace Collector, ein Erzschurke dessen Durchlauf mitunter 600ms Zeit kosten kann. Man spricht auch davon, dass er die Welt “anhält”.
- Die versteckten Objektbrüter die den GC in Zugzwang bringen können. (Anonyme Klassen für Event-Handler, foreach-Schleifen die Iterator-Objekte anlegen, etc.)
Doch auf diese Aspekte werde ich im folgenden nicht eingehen, sondern mich auf die onCreate-Problematik stürzen. Zu dem Thema GC und wie man ihn vermeidet sind vor allem Ressourcen zum Thema Spieleentwicklung sehr zu empfehlen. Siehe z.B. (Beginning Android Game Development, Google IO Talks Memory Management, Android Real Time Game Development)
Doch zurück zu onCreate, im folgenden möchte ich ein kleines Framework vorstellen, mit dem ich massive Verzögerungen durch ORMLite -und PageViewer- Instantiierungen schmerzlos gemacht habe.
Dem möchte ich allerdings eine Annahme vorausschicken, und zwar dass es dem Benutzer vollkommen egal ist ob eine Operation 200ms oder 2s dauert, solange die App präsent ist und nicht einfach vor sich hinstockt bis endlich die Activity aufpoppt.
Unter dieser Prämisse sollte klar sein was im onCreate passieren darf:
- Die Content-View muss gesetzt werden.
- Die View-Referenzen können gesetzt werden. Wie oben bereits geschrieben, sind die Aufrufe relativ günstig und verzögern den Aufbau der Activity auf meinem schwachbrüstigen Wildfire kaum. (Aufpassen: findView-Aufrufe dürfen nicht in andere Threads ausgelagert werden, weil nur der Thread auf die Referenz zugreifen darf, der sie auch hergestellt hat.)
- Code anstarten, der die teuren Operationen in einem anderen Thread anstartet.
Schauen wir uns doch einfach ein kleines Code-Beispiel an bei dem es sehr einfach ist die Schnecke zu finden:
public class SlowAppTestActivity extends Activity {
private ListView lvItems;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
lvItems = (ListView)findViewById(R.id.lvItems);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item, getItems());
lvItems.setAdapter(adapter);
}
public List<String> getItems() {
final ArrayList<String> items = new ArrayList<String>();
for(int i = 0; i < 35; i++) {
items.add(String.format("Number %d and very useful!", i));
// simple code to sleep ...
try {
Thread.sleep(200);
} catch(InterruptedException e) {e.printStackTrace();}
}
return items;
}
}
Das zugehörige Layout sieht so aus:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Ready!" />
<ListView
android:id="@+id/lvItems"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
Also wenden wir jetzt mit dem AsyncTask aus dem Android-Framework die drei Punkte an. Zu erst setContentView und findView-Aufrufe erledigen. Im nächsten Schritt den Code für die asynchrone Ausführung anstarten und im letzten Schritt, wieder im UI-Thread, die UI updaten.
Mit diesen Anpassungen sieht das onCreate nun so aus:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
lvItems = (ListView)findViewById(R.id.lvItems);
AsyncTask<Void, Void, ArrayAdapter<String>> task = new AsyncTask<Void, Void, ArrayAdapter<String>>() {
@Override
protected ArrayAdapter<String> doInBackground(Void... params) {
ArrayAdapter<String> adapter = new ArrayAdapter<String>(SlowAppTestActivity.this, R.layout.item, getItems());
return adapter;
}
@Override
protected void onPostExecute(ArrayAdapter<String> adapter) {
lvItems.setAdapter(adapter);
}
};
task.execute();
}
Es steht jedem frei beide Varianten auszuprobieren. Der Unterschied ist enorm. Doch wie kann man diesen Teil sinnvoll abstrahieren um es an mehreren Stellen zu verwenden?
Schauen wir uns, unsere drei Schritte noch einmal an. Der erste Schritt kann genau wie er ist im onCreate bleiben. Hier macht es also keinen Sinn herumzudoktern, dito für den zweiten Schritt. Der dritte Schritt wird schon interessanter, denn es soll etwas asynchron ausgeführt werden.
Im letzten Teil soll wieder etwas synchron im UI-Thread ausgeführt werden. Daraus lassen sich nun zwei Methoden ableiten die wir gleich in ein Interface gießen:
public interface IAsyncWorker {
public void processAsync();
public void onAsyncFinish();
}
Jetzt ist klar was passieren soll, aber wer erledigt es? Eine einfache Klasse namens AsyncWorkerLauncher bietet sich dafür an.
public class AsyncWorkerLauncher extends AsyncTask<Void, Void, Void> {
private final IAsyncWorker worker;
public static AsyncWorkerLauncher from(IAsyncWorker worker) {
return new AsyncWorkerLauncher(worker);
}
public AsyncWorkerLauncher(IAsyncWorker worker) {
this.worker = worker;
}
@Override
protected Void doInBackground(Void... params) {
worker.processAsync();
return null;
}
@Override
protected void onPostExecute(Void result) {
worker.onAsyncFinish();
}
}
Mit diesen paar Zeilen Code kann jetzt unsere Activity umgebaut werden:
public class SlowAppTestActivity extends Activity implements IAsyncWorker{
private ListView lvItems;
ArrayAdapter<String> adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
lvItems = (ListView)findViewById(R.id.lvItems);
AsyncWorkerLauncher.from(this).execute();
}
public List<String> getItems() {
final ArrayList<String> items = new ArrayList<String>();
for(int i = 0; i < 35; i++) {
items.add(String.format("Number %d and very useful!", i));
// simple code to sleep ...
try {
Thread.sleep(200);
} catch(InterruptedException e) {e.printStackTrace();}
}
return items;
}
@Override
public void processAsync() {
adapter = new ArrayAdapter<String>(SlowAppTestActivity.this, R.layout.item, getItems());
}
@Override
public void onAsyncFinish() {
lvItems.setAdapter(adapter);
}
}
Und Bumm: Plötzlich haben wir eine Activity die sich tatsächlich smooth anfühlt und deren Code nicht umständlicher zu lesen ist.
Das Projekt findent man übrigens hier: SlowAppTest.