Besonderheiten der Android-Programmierung im Jahr 2017 – ein praktischer Exkurs

Wer bisher ein Teufelslied über Fragmentierung im Mobilbereich sang, erntete von diensterfahrenen Entwicklern ein müdes Lächeln. Warum sich das nun ändert und was Android-Entwickler sonst so vorantreiben sollte.

In Pocket speichern vorlesen Druckansicht
Besonderheiten der Android-Programmierung im Jahr 2017 – ein praktischer Exkurs
Lesezeit: 17 Min.
Von
  • Tam Hanna
Inhaltsverzeichnis

Schon früh gab es verschieden große Mobilgeräte, die angesichts der Handgrößen ihrer User unterschiedliche Displayauflösungen mitbrachten. Während man zu Beginn die Logik zur Größenanpassung von Hand realisieren musste, erledigt Android das dank Layouts und ähnlicher Aspekte automatisch. Auch wenn Handera und Sony den einen oder anderen Eigenweg gingen, Clie, TRGpro und Palm m505 waren doch alle irgendwie Handcomputer. Zudem boten alle identische APIs.

Spätestens seitdem Amazon in den Mobilmarkt eingestiegen ist, ändert sich das gravierend: Ein Fire-OS-Gerät unterscheidet sich in vielen Bereichen stark vom normalem Android. Waren die APIs für SD-Kartenzugriff und/oder virtuelle Graffitiflächen unter Palm OS "Randgruppenvorstellungen", unterscheiden sich heutige Stores massiv. Wer einmal eine komplette IAP-Implementierung (In-App Purchase) oder einen Kartenrenderer ausgetauscht hat, weiß, was gemeint ist.

Besonders kritisch ist in dem Zusammenhang die Vielzahl von Store-Varianten: Wer für jeden kleinen Vertriebskanal von Hand eine neue Applikation kompilieren muss, kann vom "Long Tail"-Effekt nur wenig profitieren und wird bald gewisse Channels nicht mehr bedienen.

Auch wenn das Ignorieren von Amazon schon vor einiger Zeit keine vernünftige Vorgehensweise mehr war, wirklich kritisch wird die Fragmentierung durch das Aufkommen von Android TV, Android Wear und Android Things. Waren Tablet und Telefon in der Bedienintention irgendwie verwandt, expandiert Android-Code nun in eine komplett andere Welt. In der Anfangszeit der Mobile-Industrie warnte Palm seine Entwicklerschaft permanent davor, GUI-Designs und Bedienkonzepte eins zu eins vom Desktop zu übernehmen.

Im Fall von Smart TVs ist das noch kritischer – eine österreichische Bank lieferte vor einigen Jahren mit einem Kinect-basierten Aktien-Analysewerkzeug ein klassisches Antipattern. Aus technischer Sicht war die Idee wunderbar: Mit Kinect und 50 Zoll-Fernseher lassen sich Kurse nun mal großartig darstellen. Das praktische Problem war, dass Passanten so Informationen über das Depot des gerade mit dem System interagierenden Individuums sammeln konnten.

Für Entwickler stellt sich an der Stelle eine besondere Herausforderung: Die schon aus Kostengründen gewünschte Wiederverwendung von Code setzt eine bisher nie dagewesene Menge an Mitdenken voraus. Das Reduzieren des Funktionsumfangs macht auch an anderer Stelle Sinn. So würde die Smartwatch-Version nicht davon profitieren, wenn sie die Chartansicht der Uhrenversion mit 50 verschiedenen Indikatoren "zumüllt" – Abbildung 1 gilt auch mehr als zehn Jahre nach ihrem Erscheinen.

Feature-Liste versus User Experience: Weniger kann mehr sein (Abb. 1)

(Bild: PalmSource)

Kurz gefasst: War Gejaule über Fragmentierung bisher ein Problem mangelnden technischen Verständnisses, haben Entwickler nun wirklich einen Grund zur Sorge. Das aus kommerzieller Sicht sinnvolle Recyclen von Code setzt feingranulare Einstellungsmöglichkeiten voraus. Wer sie von Hand implementieren muss, hat mit der Abbildung aller Möglichkeiten einige hundert Stunden zu tun.

Ohne technische Unterstützung ist das ein Prozess, der in der Praxis eher früher als später aufgegeben wird. Bisher war die Automatisierung dieser Aufgabe großen Unternehmen vorbehalten – das Buildsystem Gradle wurde entwickelt, um dieses Problem zu vermeiden.

Der Umstieg von Eclipse auf Android Studio vor einigen Jahren sorgte in der Entwicklerschaft für Unruhe: Gradle im Herzen der Entwicklungsumgebung ist und bleibt eine eigenwillige Angelegenheit, die GUI-verwöhnte Programmierer an Makefiles und andere Bösartigkeiten erinnert. Wer dem System etwas Zeit spendiert, stellt aber bald fest, dass die Angst unbegründet ist. Gradle basiert auf einem als Flavor bezeichneten Konzept, das in Abbildung 2 grafisch beschrieben ist.

Ein Flavor pro Store ist ein guter Anfang (Abb. 2).

Applikationen liegen als eine Kombination von Geschmacksrichtungen vor, die verschiedene Funktionsumfänge aufweisen und bei der Kompilierung aus unterschiedlichen Paketgruppen zusammengestellt werden. Zudem gibt es normalerweise zumindest eine Debug- und eine Release-Variante – deswegen kann die Menge der Kompilate im Laufe der Zeit stark anwachsen.

Zum Überwinden von Startschwierigkeiten möchte der Autor an der Stelle durch ein komplettes Beispiel führen, das einige Besonderheiten des Gradle-Buildsystems demonstriert. Begonnen sei mit der Generierung eines gewöhnlichen Projektskeletts auf Basis einer nicht allzu veralteten Version von Android. Entwickler öffnen im nächsten Schritt die zum Modul app gehörende build.gradle-Datei. Die relevanten Teile sieht man im folgenden Beispiel:

android {
compileSdkVersion 24
. . .
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}

Auffällig ist, dass schon an der Stelle mehrere Kompilate vorliegen – die von Haus aus angelegte Version der Datei legt nämlich sowohl ein Release- als auch ein Debug-Kompilat fest. Für die Aufgabe hier – also die Nutzung des vollwertigen Flavor-Subsystems – ist stattdessen ein weiteres Attribut erforderlich, dass sich wie im folgenden Beispiel präsentiert:

android {
. . .
productFlavors {
forAmazon {
applicationId "com.tamoggemon.flavortest.amazon"
versionName "1.0-amz"
}
forPlay {
applicationId "com.tamoggemon.flavortest.play"
versionName "1.0-ply"
}
}
}

Das gezeigte Snippet legt zwei Flavors namens forAmazon und forPlay an. Beide bekommen je einen eigenen Versionsnamen und eine eigene Applikations-ID zugewiesen: Das Zuweisen einer eigenen ID wäre streng genommen gar nicht notwendig, ermöglicht aber ein interessantes Experiment mit dem Manifest-Merger.

Nach dem Speichern der neuen Dateiversion blendet Android Studio automatisch ein gelbes Banner ein, das zum erneuten Synchronisieren des Gesamtprojekts auffordert. Um das Buildsystem zur Analyse des Projekts zu animieren und dabei die notwendigen neuen Ordner und Dateien automatisiert anzulegen, muss man den Banner anklicken. Nach der erneuten Synchronisation klicken Entwickler auf Build | Select Build Variant, um das in der Abbildung gezeigte Fenster auf den Bildschirm zu holen. Das Feld Build Variant erlaubt dabei die Auswahl der Konfigurationskombination der Flavor-Kompilate, die beim Anklicken von Play oder Debug erstellt werden soll.

Dieses Fenster hilft Entwicklern bei der Handhabung mehrerer Flavors (Abb. 3).

Mit Android vertraute Entwickler wundern sich an der Stelle mitunter über folgende Passage in der Manifestdatei:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tamoggemon.flavortest">

Die hier festgelegte Package-ID ist unter Gradle für die Kompilierung selbst relevant. Sie beschreibt nur den Namespace, in dem die diversen Ressourcendateien abgelegt werden. Durch das Konstantsetzen dieses Werts ist sichergestellt, dass alle Code-Behind-Klassen ihre Zielelemente auch bei Änderungen des Gesamtpaketnamens finden können.

Nach dem Anlegen des Labels ist es an der Zeit, das Programm zu testen. Dazu wird im ersten Schritt ein String in einer Klasse eingebunden, der das Programm zur Laufzeit über die zu seiner Erstellung verwendete Kompilationskonfiguration informiert. Das lässt sich auch anders erreichen – die hier genutzte Vorgehensweise ist allerdings didaktisch günstig.

Der erste Schritt zum Ziel ist dabei das Anlegen sogenannter sourceSets. Es handelt sich dabei um die "kleinste" Einheit, mit der sich die konditionelle Kompilierung in Android Studio aktivieren lässt. sourceSets sitzen normalerweise in fixen Verzeichnissen, deren Namen über einen vergleichsweise einfachen Algorithmus aus der Build-Variante abgeleitet werden kann. Entwickler ersparen sich diese Arbeit allerdings, da Android Studio bei korrekter Konfiguration bereitwillig über die jeweiligen Verzeichnisse Auskunft gibt.

Android-Studio bietet Entwicklern über das auf der rechten Seite eingeblendete Tab Zugriff auf eine Vielzahl häufig ausgeführter Operationen des zugrunde liegenden Buildsystems. Das Öffnen der Rubrik und das Auswählen des in Abbildung 4 gezeigten Tasks sourceSets aktivieren den sourceSet-Analysator.

In diesem unscheinbaren Fenster verbergen sich einige hilfreiche Werkzeuge (Abb. 4)

Danach muss man den Task doppelt anklicken, um Android Studio zur Ausführung zu animieren. Die Resultate finden sich in der Regel in der Konsolenausgabe, die sich über den Button auf der unteren rechten Bildschirmkante auf den Monitor holen lässt (s. Abb. 5).

Die Gradle-Konsole ist nicht immer am Bildschirm (Abb. 5).

Angesichts der immensen Länge der Ausgabe ist es empfehlenswert, sie in ein eigenes Fenster zu kopieren. Aus Platzgründen werden nur die relevantesten Teile gezeigt, was zu folgender Ausgabe führt:

app:sourceSets

Nach der Ausgabe des Namens des abgearbeiteten Tasks folgt eine mehr oder weniger alphabetisch sortierte Liste aller möglichen Konfigurationen. Nicht wundern, dass es einige Dutzend Möglichkeiten gibt. Zu bedenken ist auch, dass neben der universellen Hauptkonfiguration für jedes Flavor eine Basiskonfiguration zur Verfügung steht, die jeweils wiederum in eine Debugger- und eine Release-Konfiguration aufgesplittet wird.

Die eigentliche Kompilierumgebung entsteht durch ein Abarbeiten von unten nach oben des Flavor-Graphen: Beispielsweise könnte die Debug-Version eines Flavor Ressourcen überschreiben, die im Basis-Flavor angelegt sind. Es ist dabei allerdings darauf zu achten, dass Klassen als Ganzes nur schwer überschreibbar sind – sie liegen in der Praxis immer im feingranularsten Teil des Graphen. Der eigentliche Aufbau der Ausgabe der einzelnen Flavor-Informationen entspricht im Großen und Ganzen immer demselben Schema, weshalb der folgende Code nur eine Version zeigt:

forPlay
-------
Compile configuration: forPlayCompile
build.gradle name: android.sourceSets.forPlay
Java sources: [app/src/forPlay/java]
Manifest file: app/src/forPlay/AndroidManifest.xml
Android resources: [app/src/forPlay/res]
Assets: [app/src/forPlay/assets]
AIDL sources: [app/src/forPlay/aidl]
RenderScript sources: [app/src/forPlay/rs]
JNI sources: [app/src/forPlay/jni]
JNI libraries: [app/src/forPlay/jniLibs]
Java-style resources: [app/src/forPlay/resources]

Gradle emittiert eine Auflistung der vom jeweiligen Flavor verwendeten Verzeichnisse: Wer forPlay beispielsweise mit einem RenderScript-File ausstatten möchte, sollte das File im Verzeichnis app/src/forPlay/rs platzieren.

Als erste Aufgabe schließt der Autor hier die weiter oben begonnene Arbeit an den Android-Projekt-IDs ab. Hierzu ist ein Konfigurationswechsel erforderlich: Entwickler versetzen das Projektstruktur-Fenster in den Projektmodus, um den in Abbildung 6 gezeigten src-Ordner auf den Bildschirm zu holen.

Dieses Verzeichnis dient als Ziel für weitere Ausführungen (Abb. 6).

Nach Klick mit der rechten Maustaste auf src wählt man die Option New | Other | Android Manifest File. Der daraufhin erscheinende Dateigenerator bietet von sich aus eine Combobox an, in dem Entwickler das zu verwendende sourceSet auswählen dürfen. Nach der Auswahl von forAmazon müssen Entwickler auf Finish klicken, um das Erstellen des Manifests abzuschließen. Um eine weitere Manifestdatei für die Konfiguration forPlay anzulegen, verfährt man analog. Vollzieht Android Studio die Änderungen nicht sofort, braucht die IDE manchmal einen Neustart, bevor sie wieder auf den aktuellen Stand ist. Nach getaner Arbeit präsentieren sich die beiden Manifestdateien nach folgendem Schema:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tamoggemon.flavortest.amazon">
<application>
</application>
</manifest>

Android Studio besteht von Haus aus darauf, jede neue Manifestdatei mit der Package-ID des jeweiligen Flavor auszustatten. Das ist hier nicht sinnvoll, weil die Nutzung verschiedener IDs beim Finden der Ressourcen für Ärger sorgt. Die Behebung erfolgt durch Entfernen dieses Attributs:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application></application>
</manifest>

Nun geht es an an anderer Stelle weiter. Entwickler klicken im leeren Mutterordner eines der beiden Flavors rechts an und wählen im daraufhin erscheinenden Kontextmenü die Option New | Folder | Java Folder. Android Studio reagiert darauf mit dem Öffnen des Verzeichnisgenerators. Nach dem Anklicken von Finish finden Entwickler einen neuen hellblauen Ordner im Paket, der zum Aufnehmen von Klassen bereit ist. (Manche Versionen von Android Studio markieren nur jene Verzeichnisse blau, die in dergerade aktuellen Kompilationskonfiguration auch wirklich kompiliert werden.) Darauf verfährt man nach demselben Schema mit dem Projekt forAmazon, um die beiden Ordner anzulegen. Diese lassen sich sodann wie gewohnt rechts anklicken, um die eigentlichen Klassen über den gewohnten Generator zu generieren. Als Erstes der Code des StringHolder, der in das forAmazon-Flavor des kleinen Beispielprojekts wandert:

package com.tamoggemon.flavortest;
public class StringHolder {
static public String TAG = "Amazon!";
}

Der Generator für neue Java-Dateien erzeugt die Files von Haus aus im Default-Paket. Wer die Package-Deklaration von Hand einfügt, provoziert den Editor von Android Studio zur Anzeige eines Fehlers. Er dient als Shortcut zum Aktivieren der Codevervollständigung, was das Verschieben der Klassendatei in den korrekten Ordner erleichtert. Aus Gründen der didaktischen Komplettheit zeigt der Autor den – nicht wesentlich komplexeren – Code des StringHolder für Google Play:

package com.tamoggemon.flavortest;
public class StringHolder {
static public String TAG = "Google Play!";
}

Zum fertigen Testharnisch brauchen Entwickler an der Stelle nur noch ein Programm, das eine TextView mit dem im StringHolder befindlichen Wert bevölkert. Die Beispielvariante sieht wie im folgenden Beispiel aus:

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView t=(TextView)findViewById(R.id.textView);
t.setText(StringHolder.TAG);
}
}

An sich wäre man hier zur Kompilation bereit: Doch leider stellt Android Studio ein kleines Hindernis in den Weg. Die Festlegung des Tasks sourceSets sorgt dafür, dass sowohl der Debug- als auch der Release-Button statt der gewünschten Kompilierung den jeweiligen Job anstoßen. Zur Kompilierung des Programms müssen Entwickler auf das Drop-down-Menü neben den beiden Buttons klicken und die Option app wählen – danach ist klar, ob alles problemlos funktioniert.

Als letzte Aufgabe sei sich nochmals der Manifestdatei zugewendet, um ein manuelles Merging zu realisieren. Als Übungsziel will der Autor nun den im Programmstarter angezeigten Namen verändern, den das Android-Label-Attribut beeinflusst. Im Moment findet sich folgende Sequenz in der Haupt-Manifestdatei:

<?xml version="1.0" encoding="utf-8"?>
<manifest . . .>
<application
...
android: abel="@string/app_name"

Eine naive Vorgehensweise würde sich darauf beschränken, in den beiden plattformspezifischen Manifestdateien jeweils ein neues Label-Attribut anzulegen. Wer das tut, wird im Rahmen der Kompilierung mit dem in Abbildung 7 gezeigten Fehler konfrontiert:

Überschreitungen müssen – zur Vermeidung von Syntaxfehlern – von Hand angewiesen werden (Abb. 7).

Eine funktionierende Version des Codes würde wie im folgenden Beispiel aussehen:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:label="Play-Flavor"
tools:replace="android:label">
</application>
</manifest>

Als ersten Akt laden Entwickler hierbei den Tools-Namespace, der eine Gruppe von Attributen und Werkzeugen enthält – darunter auch jene, die das Beeinflussen der Manifest-Zusammenfassungslogik von Android ermöglichen. Durch das Setzen von tools: replace informieren sie Android Studio sodann darüber, dass der im Application-Tag vorliegende Wert für android:label den Wert des untergeordneten Manifests ersetzen soll.

Neben dem hier gezeigten Überschreiben gibt es einige andere Attribute, die das Löschen oder gezielte Zusammenführen von Manifestattributen ermöglichen. Das Zusammenfügen von Manifestdateien mit verschiedenen Attributen ist eine Wissenschaft für sich: Für Entwickler gibt es eine Übersicht aller möglichen Parameter, mit denen man Manifestverarbeitungslogik von Android Studio dazu anweisen kann, die Attribute automatisch zu überschreiben.

Damit ist auch diese Version des Programms einsatzbereit – im Programmstarter und in der MainActivity erscheinen fortan der Programmname des gerade ausgewählten Flavor. Interessant ist in diesem Zusammenhang noch die MergedManifest-Ansicht des Editors, die einen Schnellüberblick über die Resultate des Merge-Prozesses liefert (s. Abb. 8).

Dank des Merged Manifest Tab haben Entwickler ihre Manifestdatei immer unter Kontrolle (Abb. 8).

Nach all den Verrenkungen mag sich der eine oder die andere fragen, wieso man sich das Umlernen auf Gradle antun sollte. Wer etwas mehr Aufwand investiert, kann bei einem neuen Projekt auch auf eine Cross-Plattform-Entwicklungsumgebung setzen und das Problem so aus der Welt schaffen. Diese Idee ist nicht unbedingt abwegig: 2015 berichteten die Marktforscher von VisionMobile darüber, dass mittlerweile 30 Prozent der befragten Entwickler auf Cross-Plattform-Werkzeuge setzen.

Der für Android-Programmierer erfreuliche Trend von Android als "Universal Runtime" wurde mittlerweile gestoppt: Die Erosion von RIMs nativer Entwicklerschaft durch die Android Runtime dürfte bei Microsoft und anderen ein Umdenken ausgelöst haben. Zudem ist angesichts der sinkenden Rolle von Windows Phone eigentlich nur iOS als "Zweitziel" interessant.

Mehr Infos

OpenJDK?

Bei Googles Umstieg auf OpenJDK handelt es sich um eine Anpassung von Android, die Oracle im Gerichtsverfahren um die recht- oder unrechtmäßige Nutzung von Java-APIs vermutlich "Wind aus den Segeln" nehmen sollte. Für Entwickler ist die Änderung insofern relevant, als die ursprüngliche Version von Androids Java-Runtime auf der seit 2011 nicht mehr unterstützten Apache-Harmony-Laufzeitumgebung basierte. Der Umstieg auf OpenJDK ermöglicht Google so das Bereitstellen von Features von Java 8, der Entwicklern mit syntaktischen Feinheiten unter die Arme greift

Sowohl bei webbasierten Systemen (Schema NativeScript) als auch bei semi-nativen Angeboten wie Qt gelten die altbekannten Trade-offs: Neben geringerer Performance und einer statistisch größeren Binärdatei müssen Entwickler damit rechnen, dass sich die Applikation nicht eng ins Betriebssystem integrieren kann. Wer nicht gerade an einer Systemapplikation arbeitet, mag das verschmerzen können: Haarig wird es spätestens dann, wenn sich der GUI-Stack des Cross-Plattform-Frameworks nicht zu 100 Prozent an den Vorgaben des Betriebssystems orientiert. Aus der Logik folgt, dass das nicht überall gleichermaßen problematisch ist: Ein 2D-Spiel oder ein Aktienverwaltungsprogramm mit komplett eigenem GUI-Stack dürfte unter den Einschränkungen weniger leiden als ein Produkt mit enger Systemintegration.

Auch wenn Android der unangefochtene "King of the Hill" ist: Entwickler von Applikationen für die Plattform haben keine Zeit, sich auf den Lorbeeren auszuruhen. Wer seine Applikation und seine Marketingstrategie nicht permanent an die neuen Gegebenheiten des Markts anpasst, erleidet bald Schiffbruch. Angesichts der sehr niedrigen Einstiegsschwelle ist es nur eine Frage der Zeit, bis sich ein Konkurrent findet.

Tam Hanna
befasst sich seit dem Jahr 2004 mit Handcomputern und Elektronik. Derzeit liegt sein Fokus auf interdisziplinären Anwendungen von Informationstechnologie.
(ane)