myposter Android App

Einleitung

Damit ein Kunde mit der myposter Android-App seine Bilder bestellen kann, muss die App viele verschiedene Funktionen bereitstellen. Unter anderem werden Produkte konfiguriert und Bilder bearbeitet, der Warenkorb wird angelegt und verwaltet, Bilder werden auf die Server hochgeladen und Anbindungen an mehrere Zahlungsanbieter werden bereitgestellt. Außerdem werden ständig neue Funktionen und Produkte entwickelt, sodass sich der Umfang der App beständig vergrößert.

Damit die App bei dieser Vielzahl von Funktionen auch in Zukunft zuverlässig funktioniert und der Code übersichtlich und möglichst leicht verständlich bleibt, ist es notwendig, den Code sinnvoll zu strukturieren. Da Android als Plattform kaum Vorgaben dazu macht, wie der Code einer App aufgebaut werden soll, gibt es hier ganz unterschiedliche Vorgehensweisen. Deshalb sind die verschiedenen Ansätze für die Architektur von Apps ein populäres Thema in der Android-Community. Von besonderem Interesse ist dabei die Trennung von Code, der zur Darstellung der App dient und dem Code der steuert, was dargestellt werden soll. Auf diesem Gebiet wird daher viel experimentiert und auch moderne Lösungen, die ihren Ursprung auf anderen Plattformen, wie beispielsweise der Webentwicklung, haben, werden zunehmend für Android adaptiert.

In diesem Sinne soll dieser Artikel einen Überblick über den Aufbau der myposter Android-App geben und erklären, welche Tools und Design-Patterns verwendet werden.

Architektur

Zur Kommunikation mit dem Backend wird eine REST-API benutzt. Diese API liefert alle Daten zu Produkten, Preisen und Texten in der App, ermöglicht die Verwaltung des Warenkorbs und die Abwicklung von Bestellungen. Die API ist daher für das Funktionieren der App essentiell und wird von fast jeder Komponente benutzt.

Der Datenaustausch zwischen App und API ist in einer mehrstufigen Architektur organisiert. Die Aufrufe der bereitgestellten Methoden werden mit Hilfe von Retrofit, einem HTTP-Client, ausgeführt. Retrofit erlaubt es, REST-Schnittstellen als Java-Interfaces zu modellieren. Methoden eines Interfaces werden mit Annotations versehen, um die Art der Methode, Parameter und einen Request-Body zu definieren. Retrofit erzeugt dann eine Implementierung des Interfaces, welche die HTTP-Kommunikation ausführt. Mittels eines Plugin-Systems können der erzeugten Implementierung Konverter hinzugefügt werden, welche die von der API gelieferten JSON-Daten in Java-Objekte umwandeln. Hierfür werden Gson und der entsprechende Retrofit-Konverter verwendet.

Die von Retrofit erzeugte Implementierung wird von einer API-Service-Klasse verwendet, um alle REST-Calls auszuführen. Diese Klasse ist für die Metadaten der API-Kommunikation zuständig und leitet die Payload der Responses lediglich an den DataManager weiter. Hier werden also von der API benötigte HTTP-Header gesetzt, ausgelesen und Fehlercodes der API behandelt. Die Header enthalten beispielsweise einen Hash des aktuell von der App verwendeten Datensets. Ist eine neue Version dieser Daten verfügbar, erkennt die App dies an dem veränderten Hash in einer Response der API und kann daraufhin die neuen Daten von der API abfragen. Wenn die API mit einem Fehlercode antwortet, wird dieser ausgelesen und an die dafür zuständigen Komponenten der App weitergeleitet.

Durch die Verwendung von Retrofit wird die App-API sauber im Code abgebildet und ist sehr leicht erweiter- und benutzbar.

Der bereits erwähnte DataManager befindet sich in der Architektur der App zwischen dem API-Service und den anderen Komponenten. Alle Kommunikation mit der API läuft durch den DataManager und wird hier verwaltet. Die Aufgaben des DataManagers sind aktuell Folgende:

  • Initialisierung der App mit den von der API bereitgestellten Daten, entweder aus dem Cache oder durch einen Request an die API
  • Update dieser Daten, wenn eine neue Version verfügbar ist
  • Verwaltung des Warenkorbs
  • Upload der Bilder, die vom Nutzer in den Warenkorb gelegt wurden
  • Behandlung von API-Fehlern

UI-Komponenten werden nach einer Variante des Model-View-Presenter Pattern umgesetzt. Bei MVP werden Events in der View and den Presenter weitergeleitet, der daraufhin gegebenenfalls das Model, also einen gewissen Zustand, verändert. Diese Veränderungen werden dann an die View zurückgespielt und dem Nutzer angezeigt. Bei MVP existiert eine klare Trennung zwischen der Präsentationslogik, die komplett vom Presenter übernommen wird, und dem bloßen Anzeigen von Daten, welches die View ausführt.

In der klassischen Implementierung von MVP ist die View ein Interface und trennt dadurch den Presenter von der Implementierung. Der Vorteil hiervon ist, dass der Presenter dadurch nicht von der Implementierung abhängig ist und im Falle einer Android-App somit ohne Android-spezifischen Code auskommt. Dies ist vor allem für automatisierte Tests der Presenter sinnvoll, da diese in einer normalen Java-Umgebung, und nicht nur auf einem Android-System, ausgeführt werden können.

Für die App wird mittlerweile eine Variante von MVP genutzt, die den Zustand der UI-Komponente als immutable Value Class (siehe AutoValue) abbildet. Um den Zustand zu verändern, muss also immer ein neues Zustands-Objekt erzeugt werden. Dies geschieht mit einer Reducer-Funktion, die als Eingabe den aktuellen Zustand und das Event, das die Veränderung des Zustands ausgelöst hat, erhält. Reducer-Funktionen sind pure functions. Für die Kommunikation von Presenter zu View wird ein RxJava-BehaviorSubject genutzt (siehe RxJava). Die View subscribed auf dieses Subject und erhält dadurch die vom Presenter emittierten Zustands-Objekte. Wenn ein neuer Zustand verfügbar ist, wird dieser an eine render-Methode übergeben und dargestellt.

Dieses Pattern bewirkt durch die Nutzung von Elementen funktionaler Programmierung eine sehr strukturierte und leicht nachvollziehbare Kommunikation zwischen View und Presenter. Da die Reducer-Funktionen keine Seiteneffekte haben können, ist stets klar, welche Veränderungen am Zustand ein Event bewirkt und wo Fehler zu suchen sind.

 

myposter_adroid_app

 

Verwendete Libraries

 

Dagger 2

Für Dependency Injection nutzt die App Dagger 2. Dagger 2 eignet sich besonders gut für Mobile-Apps, da nur sehr wenig zusätzlicher Aufwand zur Laufzeit anfällt. Dies wird erreicht, indem der Dependency-Graph durch Annotations definiert und der für diesen Graph notwendige Code während des Build-Prozesses erzeugt wird. Das Ziel von Dagger 2 ist es, dass der automatisch erzeugte Code so aussieht, als sei er von Hand geschrieben worden. Dementsprechend ist der erzeugte Code verständlich und vor allem simpel und performant. Die gute Performance auf Android wird erreicht, da der erzeugte Code anders als andere DI-Libraries, vollkommen ohne Reflection auskommt. Reflection ist auf Android sehr langsam und würde im Falle von DI die Startzeit der App verlängern.

Dagger 2 nutzt Modules, die benötigte Objekte erzeugen, und Components, die Modules nutzen, um die erzeugten Objekte abhängigen Objekten bereitzustellen. Die App benutzt eine übergreifende Component, die an den Lebenszyklus der App gebunden ist und Objekte bereitstellt, die von vielen Komponenten der App benötigt werden. Dies sind beispielsweise Instanzen des DataManagers, von Picasso, der Image-Loading Library, oder der Tracking-Klasse. Außerdem verfügt jede nicht triviale Komponente der App über eine eigene Dagger-Component, die Objekte bereitstellt, die nur diese Komponente benötigt und die nur so lange existieren, wie auch die abhängige Komponente existiert. Das häufigste Beispiel hierfür sind View-Implementierungen, die eine Instanz des zugehörigen Presenters benötigen.

RxJava

RxJava ist eine Library mit der Daten aus asynchronen Quellen verarbeitet werden können. Es nutzt das Observer-Pattern, um bei Verfügbarkeit neuer Daten eine Menge von Operationen auf diesen Daten auszulösen und schließlich das Ergebnis an alle Subscriber zu übergeben. Diese Technik ist für Apps, die viele lange laufende Operationen ausführen, nützlich, da sie es sehr einfach macht, solche Operationen auf Worker-Threads auszulagern. Dies ist nötig, um den UI-Thread nicht zu blockieren und so eine flüßig laufende Benutzeroberfläche zu bieten. Für Android ist es außerdem erforderlich, das Ergebnis von dem Worker-Thread in den UI-Thread zu holen, da nur von hier aus UI-Komponenten manipuliert werden können. Auch dies ist in RxJava mit einer Zeile Code möglich. Die zahlreichen zur Verfügung stehenden Operatoren machen es außerdem leicht, die ankommenden Daten so zu transformieren, wie sie gerade benötigt werden.

In Zusammenarbeit mit Retrofit können REST-Calls ein RxJava-Observable zurückgeben. Im API-Service wird mit dem map-Operator das Ergebnis auf die Payload reduziert, nachdem die Metadaten verarbeitet wurden. Der DataManager kann dann weitere Operationen ausführen, bevor das Ergebnis bei dem Presenter ankommt, der die Operation ausgelöst hat. Ein Beispiel hierfür ist das Updaten des Warenkorbs. Der DataManager speichert die Response bevor sie beim CartPresenter ankommt, sodass die Response später für andere Zwecke genutzt werden kann, ohne erneut ein Request an die API zu senden.

Die App muss natürlich häufig Bilder verarbeiten und auch hier ist es sinnvoll, diese Operationen asynchron ablaufen zu lassen, da diese bei hochauflösenden Bildern aus modernen Smartphone-Kameras viel Rechenleistung benötigen. Deshalb werden auch diese Aufgaben mit RxJava gelöst.

Ein weiterer Anwendungsfall ist, wenn Daten aus mehreren asynchronen Quellen kombiniert werden sollen. Hier können mehrere RxJava-Observables erzeugt werden, die dann mit Obersable.concatEager zu einem Observable vereint werden, das alle Daten gleichzeitig anfragt und schließlich in der korrekten Reihenfolge ausgibt.

AutoValue

Value Classes sind Klassen, die dazu da sind, Daten zu kapseln. In Java enthalten diese Klassen für gewöhnlich eine Menge von Feldern, Getter- und eventuell Setter-Methoden und implementieren häufig die Methoden equals, hashCode und toString. Diese Klassen von Hand zu schreiben ist oft unnötiger Aufwand, da der Aufbau immer wieder der Gleiche ist. Daher ist es sinnvoll, diese Aufgabe zu automatisieren. AutoValue ermöglicht es, solche Klassen mit geringem Aufwand zu definieren und erzeugt dann automatisch eine korrekte Implementierung. Zusätzlich sind die von AutoValue erzeugten Klassen immutable, d.h. nachdem eine Instanz einer solchen Klasse erzeugt wurde, können die enthaltenen Daten nicht mehr verändert werden. AutoValue erzwingt außerdem die Verwendung von @Nullable-Annotations für Felder deren Werte null sein können sollen. Wurde ein Feld nicht so markiert und wird trotzdem mit null initialisiert, wird eine Exception geworfen. Dies macht den Umgang mit diesen Objekten sehr zuverlässig, da sie nicht unerwartet verändert werden können und mögliche null-Werte leicht erkannt werden können.

Die App nutzt AutoValue für fast alle Model-Klassen. Neue Model-Klassen können dadurch sehr schnell angelegt werden und der Code ist übersichtlicher als er mit händisch implementierten Value Classes wäre. Zusätzlich werden AutoValue-Parcel und AutoValue-Gson verwendet, um die erzeugten Value Classes, um weitere Funktionen zu ergänzen. AutoValue-Parcel implementiert für die erzeugten Klassen das Parcelable-Interface. Dies ist ein Serialisierungsprotokoll, das von Android verwendet wird, um Objekte zwischen Android-Komponenten auszutauschen. So kann beispielsweise eine Activity eine andere Activity starten und ihr ein Objekt das Parcelable implementiert übergeben. AutoValue-Gson erzeugt automatisch Gson-Type Adapter für die generierten Value Classes. Diese TypeAdapter werden von Gson benötigt, um die Value Classes serialisieren und deserialisieren zu können.