Fullstack Kotlin

One Tool to rule them all?

Alle Aufgaben mit nur einer Programmiersprache erledigen? Geht das?

Fullstack-Entwickler kennen das. Das Backend wird in Java geschrieben und per Maven gebaut. Das Frontend wird in Javascript, HTML und CSS implementiert. Kommen nun auch noch Anwendungsteile wie Desktop-Applikationen oder Apps hinzu, welche nativ kompiliert werden müssen, werden noch weitere Sprachen und Bibliotheken benötigt. Wäre es nicht von Vorteil, all diese Aufgaben mit einer einzigen Programmiersprache erledigen zu können?

Kotlin hat sich dies zur Aufgabe gemacht. Dabei wird Kotlin jedoch nicht einfach nur für die einzelnen Umgebungen kompiliert, sondern stellt auch die Sprachfeatures für die einzelnen Umgebungen zur Verfügung. Egal ob JS, JVM oder Native, in allen Fällen kann der Entwickler wie gewohnt mit dieser einen Sprache und seinen Eigenheiten programmieren. Zu den Spezialitäten gegenüber Java zählen Null Safety, ein vollständig objektorientierter Ansatz, funktionale Programmierung, High Order Functions, DSLs, etc… Javascript profitiert vor allem von der Typsicherheit. Doch was heißt dies im Alltag? Kann ein Projekt vollständig und ohne signifikant erhöhten Mehraufwand einfach so in einer Sprache realisiert werden? In diesem Artikel möchte ich einen kleinen Überblick über das Thema Fullstack mit Kotlin gewähren und auf die Eigenheiten der Implementierung eingehen. Dabei werde ich auch einige Technologien vorstellen, die zusammen im Verbund mit Kotlin sehr gut harmonieren.
Als Demoprojekt kommt hier eine (sehr) rudimentäre Bibliotheksverwaltung zum Einsatz, die speziell als Begleitung zu diesem Artikel entwickelt wurde. Dabei gehen die Features eher in die Breite als in die Tiefe, um die einzelnen Besonderheiten der Implementierung zu beleuchten.

Allgemeines

Kotlin ist auf der JVM bereits kein Novum mehr und schon seit einigen Jahren im Einsatz. Ein Totschlagargument für Kotlin-Anhänger ist, dass Kotlin fast 100% kompatibel mit anderen Java-Klassen ist und somit (fast) schmerzfrei in eine bestehende Java-Applikation integriert werden kann.

Allerdings muss dazu erwähnt werden, dass Kotlin mehr als nur eine Erweiterung für Java ist. Einige Features der Sprache lassen Java ziemlich alt aussehen. In einigen Punkten kann man durchaus behaupten, dass Kotlin Java ein paar Schritte voraus ist. Sie ist über die letzten Jahre hinweg zu einem ernsthaften Konkurrenten gereift und steht Java in Sachen Funktionalität durch nichts nach. Auf der Android-Plattform hat Kotlin Java bereits als primäre Sprache verdrängt.
Doch was genau zeichnet Kotlin aus? Im Folgenden möchte ich eine kleine Übersicht über einige Interessante Features geben:

  • Cross-Compiler

    • JVM
    • Android
    • Javascript / React
    • Native
  • Type-Safe-Builder für Domain-Specific-Languages (DSL)
  • Null Safety
  • Lambdas
  • Extension Functions
  • High Order Functions
  • Eine erweiterte stdlib mit vielen Hilfsfunktionen
  • Funktioniert nahtlos mit allen gängigen Java und JS Programmteilen
  • Smart Casts
  • Default- und Named Arguments
  • Data Classes
  • Objekt-Dekonstruktion

Viele dieser Features werden noch einmal im Detail in einem Grundlagenartikel genauer vorgestellt werden.

Server

Die Kotlin-Implementierung für die JVM ist definitiv kein Hexenwerk und bedarf lediglich einer guten Kenntnis der Sprache selbst. Dabei können auch auf bekannte Mechanismen (OOP etc...) und Frameworks aus der Javawelt zurückgegriffen werden. Dazu zählen u.a. Spring Boot, Vert.X und viele andere Serverlösungen. Die Kotlin-Entwicklung im Backend ist also kein großer Kunstgriff. Es gibt bereits zahlreiche Anwendungsfälle, in denen Kotlin zum Einsatz kommt.

Neben der eigentlichen Anwendung kann auch das Buildscript (Gradle) in Kotlin geschrieben werden. Dazu muss lediglich die entsprechende DSL verwendet werden.
Der Server der Beispielimplementierung dieses Projekts basiert auf dem Webserver von Vert.X und einer MongoDB als Datenbank. Beides zusammen bietet eine sehr leichtgewichtige, reaktive Lösung. Dabei wird weder ein Spring-Kontext noch ein Applikationsserver benötigt. Dies bietet sich gerade bei kleineren Webanwendungen an, in denen Spring und Co. einfach einen Overhead darstellen würden.

Client

Etwas spannender gestaltet es sich im Frontend. Der für dieses Projekt entwickelte Webclient nutzt eine von Kotlin zur Verfügung gestellte HTML-DSL. Dadurch kann HTML-Code in Kotlin geschrieben werden, ohne die Syntax wechseln zu müssen.

<p class=“my-class“>Hello World</p>
p ("my-class") { +"Hello World" }

Dank dieser Technik ist es möglich, Backend-seitig HTML-Seiten statisch oder dynamisch zu generieren und auszuliefern oder den geschriebenen Code direkt in Javascript zu kompilieren, um ihn beispielsweise mit React zu verwenden. React ist eine Javascript-Bibliothek zur Erstellung von Single-Page-Applikationen und ermöglicht es, ein virtuelles DOM (Document-Object-Model, also die Repräsentation der HTML-Seite als Objekt) zu manipulieren und zu bearbeiten. Das eigentliche DOM der Webseite wird erst dann verändert, wenn eine React-Komponente darauf gerendert wird. Mit dieser Technik können auch komplette Seiten innerhalb der Anwendung geladen werden, ohne dass diese vom Server geholt werden müssen. Dafür verwendet React einen Router, welcher die Zuordnung einer Adresse zu einer React-Komponente vornimmt. React ermöglicht über die Komponenten eine Modularisierung der Anwendung. Während der klassische Aufbau einer Seite stets eine Trennung zwischen HTML, CSS und Javascript vorsieht und keine semantische, modulare Zusammenfassung möglich ist, besteht in React die Möglichkeit, geschlossenene Module zu programmieren, welche alle nötigen Informationen, Attribute, Funktionen und Stilbeschreibungen beinhalten. Gerade bei komplexen Projekten ist dies ein Vorteil, da gezielt modular entwickelt werden kann. Der Vorteil von React gegenüber Frameworks wie Angular und Vue.jsliegt darin, dass es eben kein Framework im klassischen Sinne, sondern eine reine Javascript-Bibliothek ist. Um React zu verwenden, erbt eine Javascript-Klasse lediglich von der Klasse React.Component. Dadurch hat diese Klasse bereits Zugriff auf den Lifecycle von React, welcher es ermöglicht, Code bei bestimmten Events auszuführen. Wird ein Element auf den DOM gerendert, wird die Methode render() aufgerufen, in dessen Return der HTML/XML-Code der Komponente an das DOM übergeben wird. Diese Methode kann jedoch beliebig ergänzt und mit Logik gefüllt werden, um ein dynamisches Rendering zu ermöglichen. Neben dem Render-Event stellt React noch weitere, Lifecycle-orientierte Events zur Verfügung. Der Zustand der Komponente wird über einen Komponenten-internen State gesteuert. Der Zustand des States kann per setState() verändert werden, was ein Rerendering der Komponente veranlasst. React-Komponenten werden normalerweise in JSX geschrieben, was eine Mischung aus Javascript und einer XML-Notation ist. Dies ermöglicht, XML- bzw. HTML-Elemente innerhalb von Javascript-Funktionen und Variablen zu verwenden, was die Arbeit mit React sehr intuitiv macht. Der relevante Code kann dadurch nämlich in einer einzigen Javascript-Klasse gebündelt werden. Die DSL von Kotlin geht da sogar noch einen Schritt weiter, denn sie erlaubt zusätzlich die Verwendung der sprachspezifischen Eigenschaften wie Typensicherheit, Null-Sicherheit, etc… Damit dies im Browser funktioniert, müssen die in KotlinJS geschriebenen Module (analog zu JSX) kompiliert und in Javascript übersetzt werden. Dies geschieht über den KotlinJS-Compiler. Für die Übersetzung nach Javascript kommt Webpack zum Einsatz, was alle Projektdaten in jeweils einem statischen Bundle pro Asset (Javascript, CSS, etc.) zusammenführt.

Da KotlinJS auf Javascript basiert, ist die Einbettung von React also sehr naheliegend. Durch die Verschmelzung dieser beiden Technologien kann komplett auf andere Sprachen wie Javascript, Typescript, JSX oder HTML (in Teilen sogar auch CSS) verzichtet werden. Damit das Ganze in der Praxis funktioniert, müssen lediglich ein paar Dinge beachtet werden. Als Ausgangspunkt dient ein HTML-Dokument, in welchem lediglich ein root-Element angelegt wird. Alle weiteren Inhalte werden dann in dieses Element von React hineingerendert.

<html lang="en">
  <head>
      <!-- Script imports, stylesheets, meta, etc.. -->
  </head>
  <body>
    <div id="root">
    </div>
  </body>
</html>

Das Rendern der Komponenten auf dieses Element übernimmt der ReactDom.Renderer. Dieser wird idealerweise in index.kt implementiert:

package index

/**
 * Die Main Methode wird von der Webseite aufgerufen.
 */
fun main(args: Array<String>) {
    // Einbetten der CSS-Dateien.
    requireAll(require.context("src", true, js("/\\.css$/")))

    // render "holt" sich das <div id="root" /> Element und fügt die Klasse AppComponent über Function
    // app() (siehe Unten) als Child hinzu
    render(document.getElementById("root")) {
        app()
    }
}

Von hier aus kann ein Hashrouter verwendet werden, um verschiedene Komponenten innerhalb der React-Struktur auf das DOM zu rendern. Im Folgenden wurde die Komponente AppComponent entsprechend diesem Ansatz implementiert.

/**
 * React-Properties zur Weitergabe von Informationen von ausserhalb
 */
interface IdProps : RProps {
    var id: Int
}

/**
 * Die App Komponente. Hier wird u.a. das Routing, sowie der allgmeine Aufbau der Seite vorgenommen
 */
class AppComponent : RComponent<RProps, RState>() {
    override fun RBuilder.render() {
        // HashRouter -> myseite.de/#/search
        hashRouter {
            // or "browserRouter"
            switch {
                route("/search", AddBooksToLibraryApp::class, exact = true)
                route("/bookshelf", BookshelfApp::class, exact = true)
                redirect(from = "/", to = "/search")
            }
        }
    }
}

/**
 * Mapped die Klasse AppComponent auf die Methode app(). Hier können weitere Initialisierungsaufgaben wie das befüllen der
 * Properties erledigt werden.
 */
fun RBuilder.app() = child(AppComponent::class) {
    // Properties setzen mit attrs.items = {Properties}
    // Die properties können über einen Funktionsparameter mit übergeben werden,
    // -> app(props: AppProperties) 
    // oder erst in dieser Methode implementiert werden 
    
    // attrs.items = items
}

Im Prinzip werden alle weiteren Komponenten analog zu dieser Vorgehensweise implementiert, in diesem Fall die BookshelfApp und die AddBooksToLibraryApp.
Ein wichtiger Aspekt von React ist der State. Dieser definiert den Zustand der Anwendung zur Laufzeit. Das folgende Beispiel zeigt die Verwendung dieses States exemplarisch anhand der BookshelfApp:

/**
 * Dieses Interface dient der Komponente später als State
 * und beinhaltet in diesem Fall eine MutableList<Item>
 * Dies birgt den Vorteil, dass der State Type-Safe ist (anders als in plain JS)
 */
interface AppState : RState {
    var books: MutableList<Item>
}


/**
 * Dient zur Anzeige aller bereits eingetragenen Bücher
 */
class BookshelfApp : RComponent<RProps, AppState>() {
    override fun AppState.init() {
        books = mutableListOf()
    }
   // Wird beim Laden der Komponente aufgerufen
    override fun componentWillMount() {
        // Methode zum laden der Inhalte.
        var books = fetchDataFromServer()
        setState {
         // SetState ist eine sogenannte Scope Function. Hier ist "this" vom Typ AppState
            this.books = books 
        }
    }

    override fun RBuilder.render() {
    // Die bereits vorhandenen Einträge aus dem State holen
	val stateVal = state.books

        stateVal.forEach { item ->
            var itemvalues = "ID: ${item.id} | Title: ${item.volumeInfo?.title}"
		    // Setzen der itemvalues in den DOM
		    p {
			   +itemvalues
		    }
	    }
    }
}

Die Syntax des Clients wirkt zwar auf den ersten Blick komplizierter als HTML/JS, ist jedoch, sobald man sich daran gewöhnt hat, sehr mächtig. Darüber hinaus wird Kotlin-React von Jetbrains, den Entwicklern von Kotlin und IntelliJ offiziell unterstützt. Die Einrichtung von Kotlin-React geschieht dank des npm-Tools create-react-kotlin-app quasi von alleine. Neben der Möglichkeit einen Webclient zu erstellen, bietet Kotlin zudem die Möglichkeit direkt für Android zu programmieren, oder den Code nativ (Linux, Windows, RaspberryPi, etc…) zu kompilieren. Eine detaillierte Auseinandersetzung mit diesen Themen würde den Rahmen dieses Blog Posts aber sprengen.

Fazit

Kotlin selbst ist eine sehr pragmatische Programmiersprache und daher ein fantastischer Wegbegleiter für jeden Entwickler. Die Möglichkeit durch (fast) alle Software-Architektur-Schichten hindurch zu programmieren stellt sich durchaus als machbar dar. Darüber hinaus sind die Features von Kotlin auf allen Plattformen sinnvoll einsetzbar und erleichtern z.T. den Programmieralltag ungemein. An dieser Stelle sei jedoch auch einmal gewarnt. Kotlin selbst hat natürlich nicht nur Vorteile. So gibt es einige Sprachfeatures, welche der alteingesessene Javaentwickler im ersten Moment als unnötig kompliziert oder gar als unbrauchbar einstufen würde (Näheres dazu im noch folgenden Grundlagenartikel). Außerdem ist gerade der Ansatz Kotlin-React noch sehr frisch und wird noch stetig weiterentwickelt. Folglich kann es passieren, dass es noch die einen oder anderen Änderungen geben wird. Im Fehlerfall oder bei Problemen in der Implementierung, muss daher teilweise recht intensiv recherchiert werden, um auf eine Lösung zu kommen. Sollte diese Technologie jedoch kontinuierlich weiterentwickelt werden, könnte Kotlin den Begriff Fullstack komplett neu definieren!

Das Begleitprojekt zu diesem Artikel findet sich hier: Virtual-Library@Gitlab


Hinweis: Da dieser Artikel bereits Anfang 2019 geschrieben wurde, ist das dazugehörige Beispiel veraltet. Der Build wurde vom verwendeten Kotlin2JS-Gradle-Plugin auf die neuen Plugins (kotlin-frontend, multiplatform) umgestellt. Die in diesem Artikel vorgestellten Features und Implementierungsbeispiele sind jedoch ohne weiteres gültig. Die neue Multiplattform-Fullstack-Architektur wird in einem separaten Artikel vorgestellt werden.

 

Jetzt teilen:

Kommentare

Einen Kommentar schreiben

Bitte addieren Sie 9 und 2.

Wir verarbeiten Ihre personenbezogenen Daten, soweit es für die Bereitstellung des Kommentars sowie zur Sicherstellung der Integrität unserer informationstechnischen Systeme erforderlich ist. Sie sind zur Bereitstellung dieser Daten nicht verpflichtet, eine Nutzung der Kommentarfunktion ist ohne die Bereitstellung jedoch nicht möglich. Weitere Hinweise zum Datenschutz finden Sie in der Datenschutzerklärung.