Start - Publikationen - Wissen - TOGAF - Impressum -

Android Framework Design


Dieser Artikel gibt keine (weitere) Einführung in die Grundlagen der Android Programmierung. Es werden stattdessen grundlegende Designmuster des Android Frameworks erläutert. Die Artikel zum MVC-Designpattern und zur nebenläufigen Verarbeitung mit Swing sind dabei als Einführung empfohlen.

Android ist MVC konform, allerdings mit der Besonderheit, dass eine Android GUI keine isolierte Anwendung ist. Eine Android Anwendung läuft in einem UI Kontainer (context genannt), ihr Lifecycle wird von diesem maßgeblich gesteuert. Aus diesem Grund hat eine Android Anwendung keine main-Methode und kann sich auch nicht selbst beenden. Jede Android Anwendung hat die Pflicht, auf Lifecycleereignisse des UI Kontainers sinnvoll zu reagieren, das geschieht über die Implementierung von Callback-Schnittstellen in den Implementierungen der Klasse Activity. Adapter übernehmen die Rolle von Viewmodell-Implementierungen (oder sind trivial).

Hier eine Übersicht des MVC bei Android und zur Orientierung die entsprechenden Pendants in Swing:

Android Swing
view deklarativ oder programmatisch erzeugte Views und Fragments, sowie Adapter als Viewmodell-Implementierungen Swing Komponenten (wie JTree) sowie die Viewmodell-Implementierungen ihrer Viewmodell-Interfaces (wie TreeModel)
control Code in den Implementierungen der Lifecycle-Callbacks einer Activity, Code in Action- und Model-Listener, Code zum Data Binding Code in static void main(String[] args), Code in Action- und Model-Listener, Code zum Data Binding
model Fachdaten und Schnittstellen zum Anmelden von Model-Listener Implementierungen
GUI Thread MainThread - warnt mit "Application Not Responding", wenn er bei der Abarbeitung verzögert oder blockiert wird, führt nicht den Code potenziell blockierender Pakete aus (FileIO, NetzwerkIO etc.) Event Dispatching Thread - keine Warnungen, blockierender Code wird ausgeführt (mit negativen Folgen für das Antwortverhalten der GUI)

Designfehler in Android?


Wer sucht der findet Kritik am Android GUI-Framework Design. Ich schließe mich dem nicht an und betrachte zwei dieser Kritikpunkte:

  1. These: Die Activties sind überladen mit control-Code. Das scheint auf dem ersten Blick korrekt, wenn man die vielen Callback-Methoden der Activity-Klassen betrachtet. Eine fertige Activity erscheint dann eine Mischung aus view (denn die Activity liefert in onCreate deklarativ oder programmatisch eine Hierarchie von GUI-Komponenten, die ihr Aussehen bestimmen) und control (in den überschriebenen Callback-Methoden). Ich empfehle, eine Activity als control zu behandeln, und wie in jeder MVC Anwendung ist ein Teil der Aufgabe des control im Rahmen einer Initialisierung view und control zu verdrahten.
  2. These: Die Viewmodells werden über Ableitungen von Adapter-Klassen bereit gestellt und sind damit nicht flexibel genug. Hier wende ich ein, dass die view und ihre Viewmodells ohnehin sehr eng gekoppelt sind, beispielsweise über die unvermeidliche Anmeldung des view im Viewmodell als Listener. Das legt eine Standardimplementierung nahe, in der solches garantiert richtig erledigt wird.

Auf alle Fälle kann man eine Android-Anwendung MVC konform paketieren: die Adapter repräsentieren die view, die Activties mit ihren Callback-Methoden control-Code und von beiden Teilen unabhängig können die Fachdaten im model organisiert werden.

Nebenläufige Verarbeitung


Wie in der Tabelle beschrieben, erzwingt Android eine Vermeidung langsamer und blockierender Aufrufe im MainThread. Jede (nicht triviale) Android Anwendung muss deshalb für die nebenläufige Abarbeitung konform zu den Erläuterungen in nebenläufigen Verarbeitung mit Swing geplant werden. Die Übergabe der Kontrolle von WorkerThreads zum MainThread erfolgt dabei im Kontext einer Activity mit dem Konstrukt

runOnUiThread(new Runnable() {
  @Override
  public void run() {
    // das hier wird auf dem Main Thread ausgeführt
  }
});
Insbesondere die Implementierungen der Model-Listener-Interfaces in view und control der Anwendung werden so grundsätzlich implementiert. Es ist das SwingUtilities.invokeLater von Android.

BaseAdapter Implementierung


BaseAdapter liefert eine Standardimplementierung für die abstrakte Adapter-Klasse und erledigt viele organisatorische Aufgaben bei der Arbeit mit Adpatern korrekt. Dazu zählt insbesondere die richtige Benachrichtigung der verbundenen View-Klassen. Im Allgemeinen wird man für eigenen Adapter-Implementierungen von BaseAdapter ableiten und die Modell-Methoden wie getCount überschreiben. Für eine korrekte Aktualisierung wird man weiterhin den Adapter als Listener am Fachmodell anmelden und bei Änderung der Modelldaten ein

notifyDataSetChanged()
aufrufen.

Solange diese Abläufe allein auf dem MainThread erledigt werden funktioniert alles reibungslos. Werden die Fachdaten jedoch von einem Workerthread geändert, kann es zu folgender Fehlermeldung kommen, obwohl notifyDataSetChanged gerufen wird:

IllegalStateException ... The content of adapter has changed but listview did not receive a notification...
Schlimmer noch: diese Fehler tauchen sporadisch auf, ein Indiz für ein Timing-Problem. Während ein Workerthread die Fachdaten verändert hindert den MainThread bis zum Aufruf von notifyDataSetChanged nichts daran, weiter seine Views zu bedienen. Dies resultiert in Aufrufen der Modell-Methoden (getCount etc.), die dann, aus Sicht des MainThreads, inkonsistente Daten liefern.

Korrekte Implementierungen eines BaseAdapters der nebenläufige Verarbeitung gestattet, müssen deshalb folgendes berücksichtigen:

  1. In den Modell-Methoden greift der Adapter nicht direkt auf die (nebenläufig bewirtschafteten) Daten des Fachmodells zu, sondern auf die Daten eines internen Caches.
  2. Die Aktualisierung dieses Caches wird vom MainThread durchgeführt, der zuletzt notifyDataSetChanged an sich selbst aufruft.
In Pseudocode könnte eine solche Implementierung dann so aussehen:
MyAdapter extends BaseAdapter {
  /*
   * Ein nebenläufig bewirtschafteter BaseAdapter
   * muss zwingend seine Daten in einem internen Cache vorhalten
   */
  private cache;
  /*
   * Der Adapter ist als Listener im Fachmodell angemeldet und wird 
   * hier von einem Workerthread über Modelländerungen informiert
   */ 
  onModelChanged() {
    /* 
     * die Ausführung wird an den MainThread übergeben
     */
    context.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        /*
         * der cache des Adapters wird aktualisiert, da diese Aktualisierung 
         * auf dem MainThread ausgeführt wird, kann es in der Zwischenzeit
         * nicht Inkonsistenzen aus Sicht der bedienten Views geben
         */
        cache = leseDatenAusFachmodell();
        /*
         * alle Views werden nun benachrichtigt
         */
        notifyDataSetChanged();
      }
    });
  } 
  /*
   * alle Modell-Methoden bedienen sich ausschließlich dem Cache des Adapters
   */ 
  int getCount() {
    return cache.getCount();
  }
  // etc.
}
Der Aufruf leseDatenAusFachmodell darf nun einerseits nicht blockieren muss aber andererseits synchronisiert sein, damit ein konsistentes Abbild des Adptercaches gelesen werden kann. Deshalb darf der Workerthread seinerseits innerhalb der Synchronisation nicht blockieren. Anders ausgedrückt: in Android muss ein lesender Zugriff auf die Fachdaten immer in nicht blockierender Weise möglich sein! Haben die Schöpfer des Android-Frameworks mit dieser Auswirkung gerechnet, als sie sich entschieden, in den Views eine strikte Prüfung des der Adapterdaten vorzunehmen?
copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved