Model-View-Presenter anstelle von Massive-View-Controller

Ein Großteil der entwickelten Android Apps verwendet eine typische Model-View-Controller Architektur. Mit diesem Ansatz werden typischer Weise kleine und überschaubare Anwendungen oder Features angefangen und zügig implementiert. Dabei werden Fragments und Activities verwendet, die sämtliche Logik implementieren, die UI kontrollieren und den Weg der Daten bis in die Views steuern. Es entstehen sogenannte Massive-View-Controller.

Häufig werden die zunächst überschaubaren Anwendungen Schritt für Schritt erweitert. Der Code wächst zu einem komplexen Gebilde und wird schwer wartbar. Activity, diverse Klassen, Listener, OnClickListener und DataHandler sind schnell eng miteinander gekoppelt und greifen quer durch alle Schichten der Anwendung, ähnliche wie in Abbildung 1 dargestellt.

Abbildung 1: Typische gewachsene Android Architektur

Abbildung 1: Typische gewachsene Android Architektur

Durch die enge Verzahnung der Klassen mit dem Android-SDK ist der Code häufig auch nicht testbar. Dies erkennt man dann schon beim ersten Unit-Test, wenn die Fehlermeldung „Method ... not mocked.“ aufkommt. Denn im Gegensatz zur Android-Umgebung auf den Geräten ist die Android-Umgebung, die für Unit-Tests genutzt wird, nur eine leere Hülle, die eine Exception wirft, wenn Methoden aus dem Framework verwendet werden.

Auch wenn es Möglichkeiten gibt, diesen Verhalten durch Einstellungen im Gradle oder Frameworks wie Roboelectric zu umgehen, verweist Google zurecht auf den Sinn dieser Maßnahme, denn es sollte der eigene Code getestet werden und nicht der von Google.

Model-View-Presenter (MVP)

Eine Möglichkeit, den in Abbildung 1 dargestellten Knoten bereits auf der Präsentationsschicht aufzulösen, bietet das Prinzip des Model-View-Presenters (MVP). Beim MVP-Ansatz agiert der Presenter als ein Mittelsmann, jegliche Präsentationslogik wird durch den Presenter geleitet. Im Vergleich zum Model-View-Controller Ansatz ist hierbei das Model separierter vom View, da der Presenter als Mediator agiert. Im Detail unterscheiden sich die Implementierungen vom MVP zwischen verschiedenen Anwendungen, die folgenden Grundzüge sind aber in allen gleich:

  • Presenter
    Der Presenter agiert als Mittelsmann zwischen View und Model. Er erhält Daten vom Model und der Businesslogik und leitet diese formatiert an den View zur Anzeige. Der Presenter hat die Referenz auf View und Model.
  • View
    Der View ist die Anzeige der Daten und routet Nutzereingaben und Events an den Presenter. Der View hält also eine Referenz auf den Presenter.
  • Model
    Das Model ist zunächst die Repräsentation von Daten. Hierzu zählt aber auch die Businesslogik, die in einer guten Architektur in verschiedene Komponenten untergliedert und strukturiert sein sollte: zum Beispiel in einer Art Schichtenarchitektur oder durch die Implementierung von Use-Cases durch Interaktoren.
Abbildung 2: MVP Grundstruktur

Abbildung 2: MVP Grundstruktur

Zusammengefasst sind diese Grundzüge in Abbildung 2. Durch diese Strukturierung der Präsentationsschicht ist die View, die aufgrund oben genannter Beschränkungen teilweise nur schwer im Detail zu testen ist, so einfach und dünn, dass die grundlegende Funktion über integrative UI-Tests getestet werden kann. Das Verhalten der UI und die Logik in der Präsentationsschicht können über Unit-Tests getestet werden, da diese im Presenter enthalten sind und alle Abhängigkeiten zum View und Model definiert sind und gemockt werden können. Auf diese Weise können auch Grenzfälle leicht getestet werden.

Ein weiterer Vorteil ist, dass der Presenter das Verhalten der UI spezifiziert aber nicht abhängig ist von der UI. So ist die UI recht einfach auswechselbar durch neuere Komponenten oder andere Ansichten (z.B. Watch oder Tablet anstelle von Smartphone). 

MVP Implementierung in Android

Die folgende Implementierung zeigt an einem einfachen Beispiel, wie leicht es ist durch den MVP-Ansatz die Präsentationslogik von der UI zu trennen und damit testbar zu machen. Die beschriebene App holt die aktuelle, formatierte UTC-Zeit von einem Rest-Backend und zeigt diese an. Um Boilerplate-Code zu vermeiden, haben wir in diesem Beispiel AndroidAnnotations für die Dependency Injection und das View-Binding genutzt. 

View / Activity:

@EActivity
public class TimeActivity extends AppCompatActivity implements TimePresenter.View {

    @Bean
    TimePresenter presenter;

    @ViewById
    TextView timeTxt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_trends);

        presenter.onViewCreated(this);
    }


    @Override
    public void showTime(String dateString) {
        timeTxt.setText(dateString);
    }
}

Presenter:

@EBean
public class TimePresenter {

    @RestService
    TimeClient timeRestClient;

    private View view;

    public void onViewCreated(View view) {
        this.view = view;
        getTime();
    }

    @Background
    void getTime() {
        TimeModel currentUTCTime = timeRestClient.getCurrentUTCTime();
        displayTime(currentUTCTime);
    }

    @UiThread
    void displayTime(TimeModel currentUTCTime) {
        view.showTime(currentUTCTime.getDateString());
    }

    public interface View {
        void showTime(String dateString);
    }

}

Der Presenter beinhalten keine direkte Verbindung zu UI-Klassen. Das vom Presenter vorgegebene View-Interface wird von der Activity implementiert. Auf diese Weise sind die Interaktionen zwischen Presenter und View klar definiert und lassen sich in einem Unit-Test für alle möglichen Konstellationen prüfen.

Zusammenfassung und Ausblick

 Durch den Einsatz von Model-View-Presenter (MVP) lässt sich die typische starke Verzahnung und Kopplung in der Präsentationsschicht aufbrechen. Hierdurch lässt sich diese Schicht testbar und klar strukturiert gestalten. Es lassen sich Prizipien wie Separation-of-Concerns und Dependency Injection leichter anwenden und so leichter eine saubere Architektur für die Anwendung entwickeln, wie sie z.B. hier von Uncle Bob beschrieben wurde.