zurück zum Artikel

Android-Entwicklung mit Groovy

Alexander Klein

Android-Entwicklung muss nicht immer gleichbedeutend mit Java-Programmierung sein. Spätestens seit Android Gradle als offizielles Build-System nutzt, rückt Groovy zunehmend ins Blickfeld der App-Entwickler.

Android-Entwicklung mit Groovy

Android-Entwicklung muss nicht immer gleichbedeutend mit Java-Programmierung sein. Spätestens seit Android Gradle als offizielles Build-System nutzt, rückt Groovy zunehmend ins Blickfeld der App-Entwickler.

Nachdem Apple Swift als alternative Sprache für iOS-Apps eingeführt hat, denkt so mancher darüber nach, ob es für Android etwas Ähnliches gibt. Zumal Entwicklern hier nur Java 6 beziehungsweise 7 ab Android Release 5.0 (Lollipop) zur Verfügung stehen. Bei Betrachtung der Swift-Syntax lassen sich schnell Ähnlichkeiten zu Groovy entdecken. Ein intensiver Blick auf die Sprache könnte also lohnen.

Im August 2014 verkündete die New York Times [1], die Sprache Groovy zur Entwicklung ihrer neuen Android-App einzusetzen. Die Programmierer suchten Wege, App-Code wiederverwendbarer und leichter an neue Features und APIs anpassbar zu machen. Mit Blick auf die mit Java 8 eingeführten Lambdas und der damit einhergehenden Möglichkeit funktionaler Entwicklungsparadigmen sowie der ausdrucksstarken Syntax fiel die Wahl auf Groovy.

Die Sprache ist objektorientiert wie Java, allerdings ermöglicht sie Mehrfachvererbung in Form von Traits [2]. Gleichzeitig ist Groovy eine funktionale Programmiersprache, sodass sich Closures [3] und Methodenreferenzen nutzen lassen. Im Gegensatz zu Java ist sie zudem dynamisch mit Features wie optionaler Typisierung [4], Coercion [5] (erzwungene Typumwandlung) und Duck Typing [6].

In Groovy geschriebene Klassen entsprechen auf Bytecode-Ebene Java-Klassen, und die Verwendung der jeweils anderen ist transparent und ohne Konvertierung möglich. Deshalb ist man nicht gezwungen, die gesamte App in Groovy zu schreiben – bestehender Code und Bibiotheken lassen sich wie gewohnt verwenden.

Groovy bringt viele nützliche Features mit. Die für die Android-App-Entwicklung wichtigsten sollen im Folgenden vorgstellt werden.

Groovy ermöglicht den Einsatz aller Features aus Project Coin [7] – auch bei Verwendung von Java 6. Hierunter fallen zum Beispiel switch-Statements mit Strings, binäre Literale oder Multi-Catch-Blöcke bei Exceptions. Was Java 8 angeht, so unterstützt Groovy derzeit keine versionsspezifischen Syntaxerweiterungen, allerdings ist die Syntax von Lambdas und Closures so ähnlich, dass Entwickler sie im Falle eines Umstiegs leicht oder gar automatisiert konvertieren können. Zudem lassen sich Closures beim Aufruf von Methoden automatisch in Lambdas umwandeln und können auf dieselbe Weise mit Java 8 verwendet werden.

Closures sind in erster Linie Codeblöcke und werden wie sie mit geschweiften Klammern ({}) notiert. Sie entsprechen zudem klassenlosen Methoden wie Lambdas, lassen sich Variablen zuweisen und können Argumente erhalten. Letztere werden anders als bei Java-8-Lambdas innerhalb der Klammern notiert und durch einen Pfeil (->) getrennt. Auch in Closure-Signaturen sind Datentypen optional.

def printName = { String vorname, nachname ->
System.out.println("$vorname $nachname")
}

Wenn eine Closure keine Argumente definiert, lässt sie sich mit oder ohne Argument aufrufen. Das Default-Argument bekommt den Namen it.

def printIt = {
println(it) // System.out kann weggelassen werden weil
//java.Object in Groovy die Methode `.println`
//implementiert.
}
printIt('Alexander') // Ausgabe: Alexander
printIt() // Ausgabe: null

Closures in Groovy sind Objekte vom Typ groovy.lang.Closure und haben Methoden, – etwa um sie aufzurufen:

printName.call('Alexander', 'Klein')  

Sie lassen sich aber auch wie Lambdas nur mit Klammern nutzen:

printName('Alexander', 'Klein')

Argumente können Default-Werte erhalten, die zum Einsatz kommen, falls das Argument beim Aufruf weggelassen wird.

Closure person = { name, wohnort = 'Unbekannt' ->
println "$name: $wohnort"
}
person('Alexander') // Ausgabe: Alexander Klein: Unbekannt

Wenn das erste Argument eine Map ist, lassen sich Argumente benennen, die im Anschluss als Key-Value-Paare in der Map landen.

Closure person = { Map params, name ->
println "$name: $params"
}
person('Alexander', wohnort: 'Stuttgart') // Ausgabe: Alexander:
// [wohnort:Stuttgart]

Wenn das letzte Argument eine Closure ist, können Entwickler sie beim Aufruf außerhalb der Argumentliste notieren.

Closure forEach(List list, Closure action) {
for(def element: list) {
action(element)
}
}
forEach([1,2,3]) { println it } // [1,2] ist ein Literal für eine List

Default-Werte, benannte Argumente und Closure-Parameter gelten in Groovy auch für normale Methoden.

Groovy erweitert Java-Klassen um weitere Methoden. So wird zum Beispiel java.lang.Object wie oben erwähnt mit der Methode .println ergänzt, die intern System.out.println aufruft. Derartige Erweiterungen sind Teile des Groovy Development Kit (GDK) und auf der Webseite der Sprache dokumentiert [8].

So wird etwa mit

new File('/my/file').text 

der Inhalt einer Datei als String zurückgegeben. Besonders erwähnenswert sind die Erweiterungen für die Collections-API. Ein paar Beispiele:

assert [1,2,3].join(', ') == "1, 2, 3"
assert [1,2,3].reverse() == [3,2,1]
assert [3,1,2].sort { a,b -> a <=> b } == [1,2,3]
assert [1,2,3].findAll { it % 2 } == [1,3]
assert [1,2,3].collect { it * 2 } == [2,4,6]
[1,2,3].each { println it } // Ausgabe 123
assert [1,2] + [3,4] == [1,2,3,4]
assert [1,1,2,3] - 1 == [2,3]
[a:1, b:2, c:3].each {key, value -> print "$key=$value "}
// Ausgabe: a=1 b=2 c=3
// [a:1] ist ein Literal für eine Map

Mit Extension Modules [9] lassen sich vergleichsweise einfach eigene Erweiterungen erstellen.

JsonBuilder und MarkupBuilder sind hilfreich beim Erstellen von JSON- beziehungsweise XML-Code. Ein Beispiel zur XML-Erzeugung:

def xml = new groovy.xml.MarkupBuilder()
xml.persons {
person(id:1) {
firstname("John")
lastname("Smith")
}
person(id:2) {
firstname("Duke")
lastname("Ellington")
}
}

<persons>
<person id='1'>
<firstname>John</firstname>
<lastname>Smith</lastname>
</person>
<person id='2'>
<firstname>Duke</firstname>
<lastname>Ellington</lastname>
</person>
</persons>
</persons>

JsonBuilder arbeitet entsprechend, ihm kann man allerdings auch eine Datenstruktur zur Serialisierung übergeben.

Die Tools JsonSlurper und XmlSlurper lesen JSON-beziehungsweise XML-Code in ein DOM-Modell ein. Letzteres lässt sich einfach traversieren:

def xml = """\
def xml = """\
<persons>
<person id='1'>
<firstname>John</firstname>
<lastname>Smith</lastname>
</person>
<person id='2'>
<firstname>Duke</firstname>
<lastname>Ellington</lastname>
</person>
</persons>"""
def data = new XmlSlurper().parseText(xml)
println data.person.find{ it.@id == 2 }.lastname // Ausgabe: Ellington
println data.person.collect {
"Person[${it.@id}]: $it.firstname $it.lastname"
}
// Ausgabe: [Person[1]: John Smith, Person[2]: Duke Ellington]

Der Groovy-Compiler bietet die Möglichkeit, in den Kompilierungslauf einzugreifen und den Abstract Syntax Tree (Objektstruktur des Quellcodes) zu analysieren und zu verändern. Dies reduziert Boilerplate-Code und löst Querschnittsprobleme wie Autorisierung, Tracing oder Ähnliches. In Java hilft in dem Fall oft nur aspektorientierte Programmierung [10] (AOP).

Über Annotationen im Quellcode aktiviert man die AST-Transformationen. So erzeugt beispielsweise @Immutable an einer Klasse den gesamten nötigen Code, um sie "Immutable" zu setzen. Ein Beispiel in Java:

public final class ToBeImmutable {
private final String variable;
public ToBeImmutable(String variable) {
this.variable = variable;
}
public String getVariable() {
return variable;
}
@Override
public String toString() {
return "ToBeImmutable(variable:" + variable + ")";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((variable == null) ? 0 :
variable.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ToBeImmutable other = (ToBeImmutable) obj;
if (variable == null) {
if (other.variable != null)
return false;
} else if (!variable.equals(other.variable))
return false;
return true;
}
}

Dasselbe ist in Groovy deutlich schlanker:

import groovy.transform.Immutable
@Immutable final class ToBeImmutable {
String variable
}

getVariable() und setVariable() werden vom Groovy Compiler hinzugefügt (siehe Groovy Beans [11]). Weitere Beispiele für AST-Transformationen lassen sich in der folgenden Tabelle nachlesen.

Annotation Beschreibung
@Singleton macht die Klasse zu einem Singleton
@Bindable erweitert die Klasse um PropertyChangeSupport und wirft Events nach der Änderung
@Vetoable erweitert die Klasse um PropertyChangeSupport und wirft Events vor der Änderung
@ToString erzeugt eine .toString()-Methode
@EqualsAndHashCode erzeugt eine .equals()- und eine .hashCode()-Methode
@TupleConstructor erzeugt je einen Konstruktor für alle Kombinationen an Properties der Klasse
@Canonical Kombination von @ToString, @EqualsAndHashCode und @TupleConstructor
@InheritConstructors erzeugt einen Konstruktor für alle Konstruktoren der Superklasse
@Delegate erzeugt Delegate-Methoden für alle Methoden des als Delegate angegeben Felds
@Lazy erzeugt Code für die späte Initialisierung eines Feldes
@Builder erzeugt Methoden für alle Properties nach dem Java-Builder Pattern
@Log, @Log4J, @Log4J2 erzeugt eine Variable log mit einem Logger-Object (java.utils, Log4j oder Log4J2)

Außerdem lässt sich über die Annotationen @TypeChecked und @CompileStatic für Klassen und Methoden steuern, ob und wo der Quellcode beim Kompilieren statisch zu prüfen oder statisch zu kompilieren ist. Dabei verliert man zwar statische Hilfsmechanismen (zum Beispiel Json- und MarkupBuilder), dafür ist der erzeugte Bytecode allerdings kleiner und genau so schnell wie von Java kompilierter Code.

Auf Android-Geräten ist Größe und Performance sehr wichtig. Deshalb ist es angeraten, alles statisch zu kompilieren und nur die Methoden davon auszunehmen, die dynamische Funktionen benötigen.

Bei Mehrfachvererbung kann das Diamond-Problem [12] enstehen, das im Folgenden kurz beschrieben ist. Klasse A implementiert eine Methode getName(). Die Klassen B und C werden jeweils von A abgeleitet und überschreiben jeweils die Methode getName(). Wenn nun die Klasse D von B und C abgeleitet wird und getName() nicht überschreibt – welche Implementierung erbt dann D?

Groovy löst das Problem mit Traits, die Code wie Klassen enthalten können, aber wie Interfaces mit implements notiert werden. Im Zweifel hat der letzte Trait die höchste Präzedenz und gewinnt.

Es ist zwar nicht empfohlen, public-Variablen in Traits zu benutzen, aber dennoch möglich. Sie werden dann allerdings nach dem Muster <Klassenname>__<Variablenname> umbenannt.

Dynamische Mechanismen haben einen gewissen Overhead und verringern dadurch die Leistungsfähigkeit unter Android merkbar. Manche dynamischen Features erfordern gar die Kompilierung von Quellcode zur Laufzeit.

Das ist möglich, aber durch das konvertierte Bytecodeformat (DEX) unter Android ist die Klasse erst in normalen Java-Bytecode zu kompilieren, dann in eine .jar-Datei zu speichern und im Anschluss mit einem speziellen Class Loader wieder einzulesen.

Der Vorgang dauert relativ lang. Danach ist die Ausführung allerdings wieder so schnell wie für jede andere Klasse. Zu den Groovy-Features, die zu vermeiden sind, gehören Builder (außer sie sind für CompileStatic vorgesehen), GroovyShell, GroovyClassLoader, GroovyTemplateEngine und die dynamische Erweiterung von Klassen (Metaprogramming).

Android-Apps lassen sich mit Groovy ab Version 2.4.0 entwickeln. Dazu sind zudem ein aktuelles Android SDK und Gradle als Buildsystem nötig. Der vorliegende Artikel geht von Android Studio als Entwicklungsumgebung aus.

Das Projekt wird, wie in Android Studio üblich, über den Wizard angelegt. Anschließend ist noch die Datei build.gradle anzupassen. Zuerst muss man dafür das Groovy-Android-Plug-in für Gradle in den Classpath des Build-Skripts einbinden. Dafür ist in der Datei build.gradle des Projekts die folgende Zeile in den dependencies-Block einzufügen:

classpath 'org.codehaus.groovy:gradle-groovy-android-plugin:0.3.5'

Nun muss man es noch in der build.gradle des Moduls verwenden:

apply plugin: 'groovyx.grooid.groovy-android'

Zu guter Letzt ist nun Groovy als Abhängigkeit hinzuzufügen. Dafür sollten Entwickler im build.gradle des Moduls im Block dependencies die folgende Zeile einfügen:

compile 'org.codehaus.groovy:groovy:2.4.0:grooid'

Komplett könnte das so aussehen:

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'org.codehaus.groovy:gradle-groovy-android-
plugin:0.3.5'
}
}
allprojects {
repositories {
jcenter()
}
}

build.gradle (Module)

apply plugin: 'com.android.application'
apply plugin: 'groovyx.grooid.groovy-android'
android {
compileSdkVersion 21
buildToolsVersion "19.1.0"
defaultConfig {
applicationId "de.codecentric.android.helloworld"
minSdkVersion 15
targetSdkVersion 21
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:21.0.3'
compile 'org.codehaus.groovy:groovy:2.4.0:grooid'
}

Jetzt kann losgelegt werden. Groovy-Dateien sucht die Software im Quellpfad src/main/groovy. Dorthin lässt sich die Activity nun verschieben und von .java in .groovy umbenennen, damit man die Groovy-Syntax einsetzen kann.

Zwar hat das Groovy-Jar eine Größe von 4,5 Megabyte, doch ist ein generiertes Android Package (APK) deutlich kleiner – bei der Gr8conf-Konferenz-App beispielsweise nur noch circa zwei Megabyte. Durch das Verwenden von ProGuard lässt sie sich darüber hinaus noch einmal verringern. Durch den Einsatz untenstehender Regeln kann man sie auf circa ein Megabyte reduzieren.

-dontobfuscate
-keep class org.codehaus.groovy.vmplugin.**
-keep class org.codehaus.groovy.runtime.dgm*
-keepclassmembers class org.codehaus.groovy.runtime.dgm* {
*;
}
-keepclassmembers class ** implements
org.codehaus.groovy.runtime.GeneratedClosure {
*;
}
-dontwarn org.codehaus.groovy.**
-dontwarn groovy**

Als Beispiel für eine Verbesserung des Codes durch Groovy soll der Aufrufs eines asynchronen Tasks dienen. Zunächst der entsprechende Code in Java:

AsyncTask task = new AsyncTask<String, Integer, Long>() {
protected Long doInBackground(String... params) {
// do something
}
}
task.execute("a", "b");

Ohne weitere Arbeit lässt sich das in Groovy wie folgt notieren:

AsyncTask task = { String... params->
// do something
}
task.execute('a', 'b')

Bei der Zuweisung einer Closure zu einer Variablen vom Typ eines Interfaces mit einer Methode oder einer abstrakten Klasse mit einer abstrakten Methode wird die Closure als Implementierung der abstrakten Methode verwendet.

Noch schöner wäre:

Async.execute('a', 'b') { args ->
// do something
}

Um das zu erreichen, müssen Entwickler eine Klasse Async erstellen.

Wenn das letzte Argument vom Typ Closure ist, gilt, dass es nach der Argumentliste notiert werden kann.

class Async {
static def execute(Object... args) {
if(!(args.last() instanceof Closure))
throw new UnsupportedOperationException('Last argument has to
be a Closure')
AsyncTask task = args.last() // Typumwandlung des letzten
// Arguments
args = args - args.last() // Closure aus der Liste der
// Argumente entfernen
return task.execute(*args) // Liste als Argumente verwenden
}
}

AsyncTask besitzt allerdings noch Callback-Methoden, um beispielsweise auf das Ende oder den Abbruch der Ausführung zu reagieren. Das ist hier noch nicht berücksichtigt, lässt sich aber vergleichsweise einfach ergänzen.

Folgender Aufruf soll möglich sein:

Async.execute('a', 'b',
onCancelled: {
// do on cancel
},
onPostExecute: {
// do after execution
}
) {
// do something
}

Die Implementierung sieht nun so aus:

class Async {
static def execute(Object... args) {
// benannte Argumente landen in einer Map<String, Object> als
// erster Parameter
def map = args.first()
def cls = args.last()
if(!(cls instanceof Closure))
throw new UnsupportedOperationException('Last argument has to
be a Closure')
AsyncTask task
if(map instanceof Map) {
args = args - map // aus der Liste der Argumente
// entfernen
map.doInBackground = cls // doInBackground ist die Haupt-
// Methode eines AsyncTasks
task = map as AsyncTask // Typumwandlung der Map in einen
// AsyncTask
} else
task = cls
args = args - cls
return task.execute(*args)
}
}

Bei der Typumwandlung einer Map in ein Interface oder in eine Klasse werden die Map-Einträge mit den Namen der Methoden als deren Implementierung verwendet. Wenn man statt Async.execute('a', 'b') zum Beispiel nur async('a', 'b') schreiben möchte, ist auch das einfach mit einem Extension-Modul [13]möglich.

Groovy bietet an sich schon viele Möglichkeiten für die Programmierung von Android-Anwendungen. Die Community erweitert sie durch zusätzliche Projekte. SwissKnife [14] etwa bietet View Injection und Threading auf Basis von Annotationen. GrooidTools [15] stellt einen Builder für Android-Widgets für imperative UIs ohne XML zur Verfügung (im Anfangsstadium).

Groovy ist eine für viele Zwecke interessante und mächtige Sprache. Sie kann dabei helfen, die Android-Entwicklung wieder "groovy" zu machen und den Code knapp, leserlich und damit besser wartbar zu halten – ohne dass Entwickler Kompromisse bezüglich Typsicherheit und Performance eingehen müssen.

Alexander (Sascha) Klein
ist Principal Consultant bei der codecentric AG. Er ist seit fast 20 Jahren im Java-Umfeld als Entwickler, Architekt, Coach und Dozent tätig. Seine Interessengebiete sind UI-Entwicklung, Ergonomie, DSLs und Produktivität in der Softwareentwicklung.
(jul [16])


URL dieses Artikels:
https://www.heise.de/-2559439

Links in diesem Artikel:
[1] http://open.blogs.nytimes.com/2014/08/18/getting-groovy-with-reactive-android
[2] http://de.wikipedia.org/wiki/Trait_%28Programmierung%29
[3] http://de.wikipedia.org/wiki/Closure
[4] http://de.wikipedia.org/wiki/Optionale_Typisierung
[5] http://de.wikipedia.org/wiki/Polymorphie_%28Programmierung%29#.C3.9Cberladen_und_Coercion
[6] http://de.wikipedia.org/wiki/Duck-Typing
[7] http://openjdk.java.net/projects/coin
[8] http://groovy.codehaus.org/groovy-jdk
[9] http://groovy.codehaus.org/Creating+an+extension+module
[10] http://de.wikipedia.org/wiki/Aspektorientierte_Programmierung
[11] http://groovy.codehaus.org/Groovy+Beans
[12] http://de.wikipedia.org/wiki/Diamond-Problem
[13] http://groovy.codehaus.org/Creating+an+extension+module
[14] http://github.com/Arasthel/SwissKnife
[15] http://github.com/karfunkel/grooid-tools
[16] mailto:jul@heise.de