Moderne Android-Entwicklung mit Kotlin
Seite 2: Sicherheit, Properties & UI-Zugriff
Sicherheit im Blick
Im Typsystem von Kotlin besitzt null gegenüber dem von Java einen Ehrenplatz: Kotlin-Typen wissen, ob sie Nullwerte enthalten können. Compiler und IDE fordern stringent die Berücksichtigung des Schnittstellenvertrags ein. So garantiert der Typ TextView stets einen Wert, während ViewGroup? auch Nullwerte haben kann und damit sowohl im Kontrollfluss als auch bei der Derefenzierung speziell zu behandeln ist:
var toolbar: ViewGroup? = null
var textView: TextView = TextView(baseContext)
Ungeprüfte Zugriffe oder Zuweisungen mit null-fähigen Typen wie in den folgenden Beispielen sind folglich nicht erlaubt und erzeugen Compiler-Fehler. An Schnittstellen zu Java erstellt Kotlin zudem transparent entsprechende Prüfungen beziehungsweise gibt sein Wissen über @NotNull-Annotationen weiter.
// Alle Statements erzeugen Compiler-Fehler
toolbar.measuredHeight
textView = toolbar.getChildAt(0) as TextView
Die Behandlung von Nullwerten versüßt Kotlin zudem mit vielen Abkürzungen. Unter Java wären für die eingeforderten Kontrollen folgender Code notwendig:
TextView child = null;
if (toolbar != null && toolbar.getChildAt(0) instanceof TextView) {
child = (TextView) toolbar.getChildAt(0);
}
textView = child != null ? child : new TextView(getBaseContext());
if (textView instanceof EditText) {
((EditText) textView).selectAll();
}
Der Elvis-Operator (?:) und der Safe-Call-Operator (?.) sind aus anderen Programmiersprachen bekannte Kurzschreibweisen. Eine Besonderheit von Kotlin ist jedoch, dass es vorangestellte Typprüfungen über die sogenannte Smart Cast berücksichtigt. Im Beispiel versteht der Compiler, dass innerhalb des if-Blocks textView vom Typ EditView sein muss und bewahrt den Entwickler vor einer erneuten Nullwert-Prüfung oder einer expliziten Typumwandlung. Trotz der zusätzlich eingeforderten Sicherheit liest sich die Variante mit Kotlin am Ende deutlich eleganter und prägnanter als das Original.
val child = toolbar?.getChildAt(0) as? TextView
textView = child ?: TextView(baseContext)
if (textView is EditText) {
textView.selectAll()
}
Properties
Aufmerksamen Lesern wird nicht entgangen sein, dass der Aufruf getBaseContext() in Kotlin zu einem einfachen baseContext geworden ist. Klassen in Kotlin können Properties besitzen, die implizit Getter-Methoden und gegebenenfalls zusätzliche Setter enthalten. Beide lassen sich auch überladen. Wie bei Variablen sind veränderbare Properties durch das Schlüsselwort var, nur lesbare Properties hingegen über val deklariert. Beim Zugriff auf Java-Code lässt Kotlin passende Getter/Setter-Paare als Properties erscheinen, was die eingangs erwähnte Kurzschreibweise erklärt.
Zugriff auf UI-Elemente
Im Kontext von Android bringen Kotlins Properties einige weitere interessante Eigenschaften mit. Einen wahren Produktivitäts-Boost bieten etwa die synthetischen Properties der Kotlin Android Extensions. Entwicklern dürfte eine der größten Quellen für Boilerplate-Code unter Android bestens vertraut sein: Das stetig wiederkehrende Muster für den Zugriff auf die Views in den Activities oder Fragments über die findViewById()-Methode.
EditText editTitle = (EditText) v.findViewById(R.id.edit_title);
editTitle.setText(mItem.getTitle());
CheckBox enabledBox = (CheckBox) v.findViewById(R.id.enable_box);
enabledBox.setChecked(true);
Button createButton = (Button) v.findViewById(R.id.create_entry);
createButton.setOnClickListener(new OnClickListener() {
@Override public void onClick(View button) {
createElement();
}
});
Durch die Butter-Knife-Bibliothek gibt es auch für Java Ansätze, um sich Teile dieser Tipparbeit zu sparen. Kotlin geht noch einen Schritt weiter, und macht das Arbeiten mit UI-Elementen unter Android fast vollständig transparent. Die von Haus aus mitgelieferten Kotlin Android Extensions erzeugen zur Compile-Zeit automatisch für alle UI Widgets eines Layouts korrekt typisierte Properties in einem synthetischen Paket. Damit reduziert sich der Zugriff auf GUI-Elemente auf ein import-Statement und ihre direkte Verwendung als Objekte über ihre IDs.
import kotlinx.android.synthetic.main.activity_hello.*
// ...
edit_title.setText(mItem.title)
enable_box.isChecked = true
create_entry.setOnClickListener { createElement() }
Um in den Genuss dieses Sahnehäubchens zu kommen, genügt es, die zusätzliche Direktive apply plugin: 'kotlin-android-extensions' in app/build.gradle aufzunehmen. Danach lässt sich zu jeder Layoutdatei ein gleichnamiges, virtuelles Paket importieren.
Im Beispiel stellt das Paket kotlinx.android.synthetic.main.activity_hello den Zugriff auf alle in der Datei layout/activity_hello.xml definierten UI-Elemente bereit. Da die virtuellen Properties bereits den richtigen Typ aufweisen, entfällt mit den sonst notwendigen Typecasts eine weitere lästige Fehlerquelle.
Verzögertes Initialisieren und Delegieren
Oftmals sind Werte unter Android beim Erstellen der Klasse noch unbekannt und beispielsweise erst beim onCreate()-Callback verfügbar. Die Deklaration sogenannter lateinit-Properties erlaubt es, Properties außerhalb des Konstruktors zu initialisieren. Somit muss das Programm nicht auf Nullwerte zurückgreifen, was eine volle Nullwertbehandlung bei jedem Zugriff erforderlich machen würden.
Auch die leistungsfähige Option, den Zugriff auf Properties über by an Dritte zu delegieren, bietet weitere Ansätze. Im Beispiel wird etwa die Initialisierung des Properties imm als Lambda-Ausdruck an lazy() und damit an den ersten Lesezugriff delegiert.
val lateinit id: String
val imm: InputMethodManager by lazy ( {
getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager } )
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
id = getString(R.string.app_name)
imm.showInputMethodPicker()
}
Bei allen Vorteilen – einen kleinen potenziellen Pferdefuß für Android haben die Kotlin-Properties jedoch: Da für öffentliche Attribute zusätzliche Getter- und Setter-Methoden zu generieren sind, stößt man gegebenenfalls schneller an das 64k-Methodenlimit der Android-DEX-Dateien. Bei Bedarf können Entwickler das jedoch mit lateinit oder @JvmField vermeiden.