Kotlin Tutorial

Kotlin: Ein Tutorial für Einsteiger, Teil 2

Alexander Hanschke

Die statisch typisierte Sprache Kotlin erfreut sich zunehmender Beliebtheit. Kotlin läuft sowohl auf der JVM als auch in JavaScript-Umgebungen. Durch die Kompatibilität zu Java 6 findet Kotlin zudem im Android-Umfeld Anklang, da die Sprache prägnanteren Code ermöglicht und zusätzlich die funktionale Programmierung unterstützt. In diesem mehrteiligen Tutorial betrachten wir die wichtigsten Sprachfeatures anhand einer Beispielanwendung.

JVM-Sprache Kotlin

Im ersten Teil dieses Tutorials haben wir uns ausführlich mit Funktionen, Klassen und Interfaces befasst. Dabei haben wir die unterschiedlichen Zugriffsmodifikatoren diskutiert und den ersten Anwendungsfall für Objekte gesehen. In diesem Teil bauen wir unsere Anwendung weiter aus, indem wir uns um den Austausch von Daten via HTTP sowie das Mappen von JSON nach Kotlin – und zurück –kümmern. Anschließend kann über das API-Projekt auf die in Pocket gespeicherten Artikel zugegriffen werden. Der Code kann von GitHub bezogen werden.

Unser API Modul enthält bereits die ersten Interfaces für die Operationen, die wir auf den Pocket-Artikeln ausführen können. Nun wird es Zeit, die Implementierungen dafür bereitzustellen. Um HTTP Requests gegen das API abzusetzen, verwenden wir die Bibliothek OkHttp. Als Austauschformat verwenden wir JSON. Um nicht manuell das Mapping von und nach JSON durchführen zu müssen, verwenden wir außerdem die Bibliothek Jackson, für die es ein separates Modul gibt, das die Zusammenarbeit mit Kotlin erleichtert.

Beide Bibliotheken fügen wir dem API-Modul als Abhängigkeiten hinzu.

pocket-api/build.gradle
...
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "com.squareup.okhttp3:okhttp:3.4.1"
compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.8.3"
}

Als Erstes wollen wir den Anwendungsfall umsetzen, die bestehenden Artikel abzurufen. Dafür haben wir bereits das RetrieveTemplate angelegt, das aktuell noch die Defaultimplementierung nutzt und eine leere Liste zurückgibt. Für die Modellierung der HTTP Requests legen wir eine neue Datei Requests.kt im Package de.techdev.pocket an. Das Pocket API erwartet mit jedem Request Consumer Key und Access Token. Beide müssen als Teil des Request Bodies übergeben werden.

Heads-up

Der Nachteil an dieser Lösung (im Vergleich zur Verwendung von Request Headern) besteht darin, dass es nicht ohne Weiteres möglich ist, beide Parameter an einer zentralen Stelle zu setzen. Da jeder Request beide Parameter benötigt, müssten diese theoretisch für alle Operationen verfügbar sein. Prinzipiell lassen sich solche Anforderungen mittels eines Interceptors lösen, wie ihn auch OkHttp anbietet. Wir entscheiden uns dennoch gegen diese Lösung, da der Interceptor sonst den Body erneut in ein Objekt und wieder in JSON serialisieren müsste und das verhältnismäßig viel Overhead für unser Szenario ist.

Zuerst modellieren wir eine Superklasse für alle Requests, wie folgt.

pocket-api/src/main/kotlin/de/techdev/pocket/Requests.kt
import com.fasterxml.jackson.annotation.JsonProperty

internal open class PocketRequest {
@JsonProperty("access_token") lateinit var access: String
@JsonProperty("consumer_key") lateinit var consumer: String
}

Wie zuvor, markieren wir die Klasse als internal, sodass sie außerhalb des API-Moduls nicht aufgerufen werden kann. Da in Kotlin alle Klassen per Default public und final sind, müssen wir zusätzlich das Schlüsselwort open verwenden, um die Klasse explizit für die Vererbung zu öffnen.

Ein PocketRequest besitzt die zwei genannten Variablen vom Typ String. Da die Initialisierung erst später erfolgt, muss zum Einen der Typ explizit angegeben werden, da der Compiler diesen nicht automatisch bestimmen kann. Zum Anderen verwenden wir lateinit, um dem Compiler mitzuteilen, dass die zulässigen Werte nicht null sein dürfen, die Initialisierung aber dennoch erst später erfolgt.

Ohne diesen Mechanismus wäre das Arbeiten mit Dependency Injection Frameworks kaum möglich, da die meisten Referenzen als potenziell null behandelt werden müssten. Da die Werte erst später gesetzt werden, kann auch nur var verwendet werden. Greift man dennoch auf eine Variable zu, bevor diese initialisiert wurde, so löst Kotlin eine Exception aus, die aussagekräftiger als eine NullPointerException ist.

Innerhalb der gleichen Datei legen wir nun den eigentlichen Request zum Abrufen der Artikel an. Diesen nennen wir Retrieve und lassen ihn von der Klasse PocketRequest erben.

pocket-api/src/main/kotlin/de/techdev/pocket/Requests.kt
internal class Retrieve(val state: String, val sort: String, @JsonProperty("detailType") val details: String) : PocketRequest()

In der Deklaration der Klasse Retrieve definieren wir alle benötigten Informationen als Konstruktorparameter. Wir können die Annotation @JsonProperty ebenfalls im Konstruktor verwenden. Um von der Klasse PocketRequest zu erben, wird der Doppelpunkt verwendet, wie auch bei der Implementierung von Interfaces. Neben der Superklasse muss innerhalb der Deklaration auch der Konstruktor der Superklasse aufgerufen werden. In dem Beispiel ist das der Defaultkonstruktor, der keine Parameter erwartet.

Als Ergebnis dieses Requests erwarten wir eine Liste aller Artikel, die unserer Suche entsprechen. Daher legen wir analog eine Datei Responses.kt an, welche die Typen und deren Mapping enthalten.

pocket-api/src/main/kotlin/de/techdev/pocket/Responses.kt
import com.fasterxml.jackson.annotation.JsonProperty
import de.techdev.pocket.api.Item

internal class RetrieveResponse(@JsonProperty("list") val items: Map<Int, Item>)

Das API liefert die Artikel gruppiert nach deren ID. Daher verwenden wir eine Map. Zuletzt müssen wir unsere Klasse Item noch anpassen, sodass diese ebenfalls die Jackson-Annotationen verwendet.

pocket-api/src/main/kotlin/de/techdev/pocket/api/Item.kt
import com.fasterxml.jackson.annotation.JsonProperty

data class Item(
@JsonProperty("item_id") val id: Long,
@JsonProperty("time_added") val added: Long,
@JsonProperty("resolved_title") val title: String)

Ohne Angabe der Annotationen versucht Jackson die Properties anhand deren Namen zu mappen. Da die Parameternamen aber von den im JSON verwendeten Namen abweichen, müssen wir das Mapping explizit angeben. Als nächstes kümmern wir uns um das Erstellen des OkHttp-Clients, um die HTTP Requests abzusetzen und um die Erstellung des Jackson Mappers, der sich um das Mapping von und nach JSON kümmert. Um nicht alle Komponenten im PocketTemplate anlegen zu müssen und um das Testen zu vereinfachen, legen wir ein Objekt an, das sich um das Erstellen kümmert.

pocket-api/src/main/kotlin/de/techdev/pocket/Components.kt
internal object Components {

fun client(): OkHttpClient {
return OkHttpClient()
}

fun mapper(): ObjectMapper {
return jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}

}

Im vorherigen Artikel haben wir innerhalb des Pocket Interfaces bereits ein Companion-Objekt verwendet. Nun kommt mit Components ein zweites Objekt dazu. Der obige Code deklariert die Klasse Components und erzeugt gleichzeitig die einzige Instanz. Damit ist Components ein Singleton. Aus diesem Grund können Objekte auch keinen Konstruktor besitzen, da die Instanz nicht selbst erzeugt werden kann.

Innerhalb des Objekts definieren wir zwei Member-Funktionen, die für die Erzeugung des HTTP Clients bzw. des Object Mappers zuständig sind. Der Aufruf der Funktionen erinnert an statische Methoden in Java und sieht wie folgt aus.

Components.client() bzw. Components.mapper()

Der ObjectMapper wird direkt so konfiguriert, dass er unbekannte Properties ignoriert. Dies ist nötig, da wir nur drei der verfügbaren Properties mappen wollen. Da die anderen uns nicht interessieren, müssen wir das dem Mapper explizit mitteilen. Als Austauschformat wollen wir JSON nutzen, das müssen wir dem Pocket API durch Setzen der Header entsprechend mitteilen.

X-Accept: application/json
Content-Type: application/json; charset=UTF-8

Wie zuvor angedeutet, lässt sich diese Anforderung ideal mittels eines Interceptors lösen. Dazu implementieren wir ein entsprechendes Interface aus der OkHttp-Bibliothek.

pocket-api/src/main/kotlin/de/techdev/pocket/PocketInterceptor.kt
internal object PocketInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().newBuilder()

builder.header("X-Accept", "application/json")
builder.header("Content-Type", "application/json; charset=UTF-8")

return chain.proceed(builder.build())
}

}

Wir implementieren die Funktion intercept, in der wir Zugriff auf die Chain bekommen. Diese beinhaltet alle registrierten Interceptoren, sodass diese in einer Reihe aufgerufen werden können. Durch die Chain bekommen wir Zugriff auf den Request, dem wir die beiden Header hinzufügen können. Anschließend wird der modifizierte Request weitergegeben.

Auch der PocketInterceptor ist ein (Singleton-) Objekt und wird in der Erzeugung des OkHttp Clients wie folgt verwendet.

pocket-api/src/main/kotlin/de/techdev/pocket/Components.kt
fun client(): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(PocketInterceptor).build()
}

Bevor wir nun endlich den ersten Request absetzen können, fehlt noch ein weiterer Typ. Zuvor haben wir den PocketRequest definiert. Dieser benötigt Zugriff auf Consumer Key und Access Token. Bis jetzt gibt es aber noch keine Stelle in unserem Code, an der die Werte gesetzt werden. Dies ändern wir nun, und erstellen eine weitere Klasse Transport. Diese Klasse ist zuständig für das Setzen von Key und Token, für das Absetzen von Requests und deren Validierung, sowie das Mappen der Ergebnisse. Damit abstrahiert sie die Transportebene komplett.

Instanzen dieser Klasse werden anschließend an die Implementierungen RetrieveTemplate und ModifyTemplate gegeben.

pocket-api/src/main/kotlin/de/techdev/pocket/Transport.kt
internal class Transport(private val client: OkHttpClient, private val mapper: ObjectMapper, private val consumer: String, private val access: String) {

private val json: MediaType = MediaType.parse("application/json; charset=utf-8")

inline fun post(payload: PocketRequest, endpoint: String): T {

payload.access = access
payload.consumer = consumer

val body = RequestBody.create(json, mapper.writeValueAsString(payload))
val request = Request.Builder().url(endpoint).post(body).build()
val response = client.newCall(request).execute()

return mapper.readValue(response.body().string(), object : TypeReference() {})
}

}

In dieser Klasse passiert so Einiges, daher gehen wir sie Zeile für Zeile durch. Wie gehabt handelt es sich um eine interne Klasse, die im primären Konstruktor eine Referenz auf den HTTP Client, den Object Mapper sowie den Consumer Key und Access Token erwartet. Alle Properties sind mit private und val gekennzeichnet, es kann also nur innerhalb der Klasse lesend darauf zugegriffen werden. Als nächstes definieren wir in der Klasse eine weitere Variable namens json, welche den verwendeten Medientypen beschreibt.

Plattformtypen

Die statische Methode MediaType.parse(String) aus der OkHttp-Bibliothek parst den übergebenen String und gibt eine Instanz vom Typ MediaType zurück. Da wir die Property direkt initialisieren, könnten wir den Rückgabetyp an sich weglassen. Wir geben ihn aber explizit an, da es sich bei dem Rückgabetypen um einen sogenannten Plattformtypen handelt.

Beim Aufruf von Java-Code kann Kotlin nicht mit Gewissheit ausmachen, ob der Rückgabetyp potenziell null sein kann oder nicht. Wäre der Compiler konservativ und würde immer von nullable Typen ausgehen, wäre in unserem Fall der Rückgabetype MediaType? und wir müssten uns um das entsprechende Handling kümmern. Auf der anderen Seite kann Kotlin auch nicht davon ausgehen, dass der Typ niemals null ist. Daher haben wir die Wahl, wie wir diesen Typ handhaben wollen und verwenden hier MediaType. Plattformtypen sind mit einem Ausrufezeichen gekennzeichnet, der Rückgabetyp ist also intern MediaType!.

Type-Reifikation

Als nächstes implementieren wir die einzige Funktion post. Das Pocket-API akzeptiert ausschließlich POST Requests, daher implementieren wir die Funktion für diesen Zweck.

Als Parameter erwartet die Funktion zum Einen den zu sendenden Request und zum Anderen den Endpunkt gegen den dieser abgesetzt werden soll. Aufrufe der Funktion sollen später wie folgt aussehen.

val request: Retrieve = ...
val result: RetrieveResponse = transport.post(request, "https://getpocket.com/v3/get")

Wir geben also den Request und den Endpoint an und erwarten als Ergebnis eine Instanz vom Typ RetrieveResponse. Um das zu bekommen, geben wir in der Funktion <reified T: Any> an, was bedeutet, dass der generische Typ T vom Compiler reifiziert, also automatisch ermittelt wird. Das erkennt der Compiler entweder an dem expliziten Rückgabetypen (val result: RetrieveResponse) oder an der Verwendung eines generischen Parameters val result = transport.post (retrieve, https://getpocket.com/v3/get). Dies ist dann auch ebenfalls der Rückgabetyp der Funktion. Zusätzlich müssen wir das Schlüsselwort inline angeben, was dafür sorgt, dass wir innerhalb der Funktion auf Instanzen von T ohne Weiteres zugreifen können.

In den folgenden Zeilen setzen wir Consumer Key und Access Token. Das geschieht damit nur einmalig an dieser Stelle und beide Schlüssel müssen nicht durch die Anwendung durchgereicht werden. Anschließend erzeugen wir den Request Body basierend auf dem Request und wandeln ihn in JSON um. Danach wird der eigentliche Request erzeugt und gegen das API abgesetzt. Das Ergebnis liegt wieder in JSON vor und wird direkt an den Object Mapper für das Parsen weitergegeben. Hier sehen wir nun auch den dritten und letzten Anwendungsfall für Objekte: Object Expressions. Diese sind vergleichbar mit anonymen inneren Klassen in Java. Wir erzeugen in unserem Fall eine anonyme Instanz von TypeReference und parametrisieren diese mit dem ermittelten Rückgabetypen – RetrieveResponse in unserem Fall.

Instanzen der Klasse Transport lassen wir nun ebenfalls zentral erstellen und fügen sie unseren Components hinzu.

pocket-api/src/main/kotlin/de/techdev/pocket/Components.kt
fun transport(consumer: String, access: String): Transport {
return Transport(client(), mapper(), consumer, access)
}

Um Consumer Key und Access Token ebenfalls verfügbar zu machen, fügen wir beide Parameter sowohl unserer connect-Funktion, als auch dem PocketTemplate hinzu.

companion object {
fun connect(consumer: String, access: String): Pocket = PocketTemplate(consumer, access)
}

internal class PocketTemplate(consumer: String, access: String) : Pocket {
init {
val transport = Components.transport(consumer, access)

retrieveOperations = RetrieveTemplate(transport)
modifyOperations = ModifyTemplate(transport)
}
}

Das PocketTemplate ist anschließend für die Erzeugung der einzelnen Operationen zuständig. Diese bekommen Zugriff auf die Transport-Instanz, um die HTTP-Aufrufe durchzuführen.

internal class RetrieveTemplate(private val transport: Transport) : RetrieveOperations

internal class ModifyTemplate(private val transport: Transport) : ModifyOperations

Nachdem wir alles vorbereitet haben, können wir nun die Aufrufe gegen das Pocket-API absetzen. Dazu müssen wir lediglich unseren Request erstellen und zusammen mit dem entsprechenden Endpoint abschicken. Der API Endpoint für das Abrufen der Artikel lautet https://getpocket.com/v3/get.

pocket-api/src/main/kotlin/de/techdev/pocket/RetrieveTemplate.kt
internal class RetrieveTemplate(private val transport: Transport) : RetrieveOperations {

override fun items(state: State, sort: Sort, details: Details): Collection {
val retrieve = Retrieve(state.value, sort.value, details.value)

val result: RetrieveResponse = transport.post(retrieve, "https://getpocket.com/v3/get")
return result.items.values
}
}

Da das Senden und Empfangen sowie das Mappen der Objekte in der Klasse Transport gekapselt ist, fällt der Code recht schmal aus. Wir können jetzt testen, ob wir unsere Artikel abrufen können. Diese Aufgabe überlassen wir der Klasse Janitor, die bis hierhin noch recht wenig zu tun hatte.

src/main/kotlin/de/techdev/janitor/Janitor.kt
fun main(args: Array): Unit {
val items = Pocket.connect("consumer", "access").retrieveOperations().items()

items.forEach(::println)
}

Für Consumer Key und Access Token verwenden wir vorerst Dummy Werte. Mit diesen Werten erzeugen wir eine Instanz des Pocket Interfaces und rufen alle verfügbaren Artikel ab.

Anschließend iterieren wir über alle Artikel und geben diese auf der Konsole aus. Dies funktioniert analog zu Java, mittels forEach erfolgt die Iteration und wir können eine Referenz auf eine Methode angeben, die für jeden Artikel ausgeführt wird. Führen wir diesen Code aus, so erzeugt er eine Exception. Da Consumer Key und Access Token noch nicht den tatsächlichen Werten entsprechend, antwortet das Pocket-API mit einem Fehler.

Dieser kann nicht auf die erwartete Collection von Artikeln gemappt werden, was der Grund des Fehlers ist. Daher sollten wir die Antworten auf unsere HTTP-Aufrufe validieren. Das Pocket-API dokumentiert die möglichen Fehler hier.

Zuerst definieren wir unsere eigene Klasse um Exceptions zu modellieren: PocketException welche eine RuntimeException erweitert.

pocket-api/src/main/kotlin/de/techdev/pocket/api/PocketException.kt
class PocketException(message: String) : RuntimeException(message)

Die Exception erwartet im Konstruktor eine Nachricht, die an die Superklasse weitergegeben wird. Darüber hinaus ist die Exception ebenfalls Teil des API Packages, das explizit für die Nutzung durch Clients vorgesehen ist. Um die Antworten des Pocket APIs zu validieren, können wir den folgenden Abschnitt nach dem Aufruf hinzufügen:

pocket-api/src/main/kotlin/de/techdev/pocket/Transport.kt
..
if (!response.isSuccessful) {
val error = response.header("X-Error")

throw PocketException("$error [status ${response.code()}]")
}

Führen wir den Code erneut aus, sollte die Validierung erfolgreich sein und der folgenden Fehler dargestellt werden.

Exception in thread "main" de.techdev.pocket.api.PocketException: Pocket server error. [status 500]

Erweiterungsfunktionen

Die bestehende Lösung tut was sie soll, müsste nur in unserem aktuellen Design in jedem Template dupliziert werden, da wir auch im ModifyTemplate die Aufrufe validieren wollen.

Um Codedupliaktion dieser Form zu vermeiden, findet man häufig statische Utility-Methoden in Java. Eine Alternative in Kotlin stellen die Erweiterungsfunktionen dar. Diese erlauben es, Funktionalität zu bestehenden Klassen nachträglich hinzuzufügen, ohne die Klasse zu verändern. Das ist möglich, da die entsprechenden Funktionen statisch aufgelöst werden. Die Aufrufe sehen aber so aus, als wären sie Teil der entsprechenden Klasse. Das wollen wir nutzen, um die Validierung von Aufrufen direkt in der Klasse okhttp3.Response durchzuführen. Daher erstellen wir eine neue Datei Extensions.kt und definieren die Erweiterungsfunktion wie folgt.

pocket-api/src/main/kotlin/de/techdev/pocket/Extensions.kt
internal fun Response.validate() {
if (isSuccessful.not()) {
throw PocketException("${header("X-Error")} [status ${code()}]")
}
}

Die Syntax folgt der schon bekannten Definition von Funktionen. Der einzige Unterschied ist, dass der zu erweiternde Typ (okhttp3.Response) durch einen Punkt getrennt vom Namen der Funktion angegeben wird. Dieser Typ wird auch als Receiver-Typ bezeichnet. Innerhalb der Erweiterungsfunktion kann auf die Instanz der Klasse mittels this direkt zugegriffen werden. Wie auch in Java ist dies aber optional und kann weggelassen werden.

Wir nutzen gleich noch eine Erweiterung, die Kotlin mitbringt. Auf Boolean-Referenzen können wir die Funktion not aufrufen, die den Wert negiert. Dies ist lesbarer als das Ausrufezeichen und kommt daher hier zum Einsatz. Nun können wir unsere vorherige Validierung, durch folgenden Aufruf ersetzen.

pocket-api/src/main/kotlin/de/techdev/pocket/Transport.kt
val response = client.newCall(request).execute()

response.validate()

Nun können Consumer Key und Access Token durch die korrekten Werte ausgetauscht werden. Janitor sollte bereits die verfügbaren Artikel auf der Konsole ausgeben.

Fazit

Im zweiten Teil dieser Kotlin-Einführung haben wir neben dem bereits bekannten Companion Object zusätzlich Objekte als Singleton und Object Expressions kennengelernt. Mittels OkHttp und Jackson haben wir die Transportschicht definiert und in diesem Zusammenhang Type Reifikation und Plattform Typen kennengelernt. Mittels Erweiterungsfunktionen haben wir eine bestehende Klasse um Funktionalität erweitert, um die Validierung der HTTP Aufrufe durchzuführen. Im nächsten Teil werden wir die Funktionalität für das Archivieren und Löschen von Artikeln hinzufügen. Darüber hinaus kümmern wir uns um das Erstellen von Tests sowie der Dokumentation.

Verwandte Themen:

Geschrieben von
Alexander Hanschke
Alexander Hanschke
Alexander Hanschke arbeitet seit 2013 bei der techdev Solutions GmbH in Karlsruhe, wo er Kunden bei der erfolgreichen Umsetzung von IT-Projekten unterstützt.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: