20 KiB
Keluna
Kotlin Embedded Local UI for Native Applications
Keluna ist die Idee für ein leichtgewichtiges Framework zur Entwicklung nativer Desktop-Anwendungen für Linux, macOS und Windows mit Kotlin/Native.
Das Ziel ist eine Entwicklererfahrung ähnlich moderner Kotlin-UI-Frameworks, ohne dafür eine JVM, eine eigene Rendering-Engine oder einen mitgelieferten Browser zu benötigen.
Keluna kombiniert dafür drei grundlegende Konzepte:
- Kotlin/Native für Anwendungscode und Betriebssystemintegration
- ein Vaadin-Flow-artiges Komponentenmodell für die Beschreibung und Verwaltung der UI
- die vorhandene System-WebView des jeweiligen Betriebssystems als Rendering-Engine
Dadurch sollen kleine, schnell startende Desktop-Anwendungen entstehen, deren Anwendungscode nahezu vollständig in Kotlin geschrieben werden kann.
Motivation
Für plattformübergreifende Desktop-Anwendungen existieren heute mehrere etablierte Ansätze.
Electron
Electron bietet eine sehr komfortable Web-Entwicklungsumgebung, liefert jedoch Chromium und Node.js mit jeder Anwendung aus.
Selbst kleine Anwendungen erreichen dadurch schnell Größen von deutlich über 100 MB und benötigen vergleichsweise viel Arbeitsspeicher.
Compose Multiplatform
Compose Multiplatform ermöglicht eine hervorragende Kotlin-Entwicklung, basiert auf dem Desktop jedoch auf der JVM und einer eigenen Rendering-Infrastruktur.
Dadurch entstehen ebenfalls relativ große Distributionen und zusätzliche Startkosten durch JVM und UI-Runtime.
Tauri
Tauri vermeidet den eingebetteten Browser und verwendet stattdessen die WebView des Betriebssystems.
Dadurch können Anwendungen sehr klein bleiben.
Die typische Architektur besteht jedoch aus zwei Welten:
- TypeScript oder JavaScript für das Frontend
- Rust für den nativen Teil
Qt
Qt ermöglicht kleine und schnelle Desktop-Anwendungen, verwendet traditionell jedoch C++.
Rust-Bindings verbessern diese Situation, dennoch bleibt Qt ein großes, eigenständiges UI-Ökosystem.
Grundidee
Keluna soll einen anderen Weg gehen:
Die Anwendung ist eine Kotlin/Native-Anwendung.
Die System-WebView ist lediglich ihre Rendering-Engine.
Die Anwendung selbst lebt vollständig im nativen Kotlin-Prozess.
┌──────────────────────────────────────────────┐
│ Kotlin/Native App │
│ │
│ Domain Model │
│ Application State │
│ Services │
│ UI Component Tree │
│ Event Handler │
│ OS Integration │
└─────────────────────┬────────────────────────┘
│
UI Synchronisation
│
kleines IPC-Protokoll
│
┌─────────────────────▼────────────────────────┐
│ JavaScript Runtime │
│ │
│ DOM Updates │
│ Event Forwarding │
│ Browser Interaction │
└─────────────────────┬────────────────────────┘
│
DOM
│
┌─────────────────────▼────────────────────────┐
│ System WebView │
│ │
│ Windows WebView2 │
│ macOS WKWebView │
│ Linux WebKitGTK │
└──────────────────────────────────────────────┘
Keluna bringt damit keine eigene Browser-Engine mit.
Ebenso benötigt Keluna:
- keine JVM
- kein Chromium-Bundle
- kein Node.js
- keine eigene GPU-Rendering-Engine
Inspirationsquellen
Keluna kombiniert Ideen aus mehreren existierenden Frameworks, ohne eines davon direkt nachzubauen.
Kotlin und Compose
Von Kotlin und Compose übernimmt Keluna vor allem die gewünschte Entwicklerergonomie:
- typsichere APIs
- Kotlin DSLs
- Properties und State
- deklarative UI-Beschreibung
- gute Integration mit Coroutines
- Gradle als Build-System
Ein Keluna-Programm könnte beispielsweise konzeptionell so aussehen:
window("Keluna Demo") {
verticalLayout {
textField("Repository")
button("Open") {
openRepository()
}
}
}
Die genaue API ist dabei noch offen.
Wichtig ist das Prinzip:
Der Anwendungsentwickler soll möglichst ausschließlich Kotlin sehen.
Vaadin Flow
Von Vaadin Flow übernimmt Keluna die Idee eines serverseitigen Komponentenbaums.
Bei Vaadin existieren Komponenten als Java-Objekte auf dem Server. Änderungen an diesen Objekten werden mit dem DOM im Browser synchronisiert.
Keluna verwendet ein ähnliches Modell, jedoch ohne Netzwerk.
Bei einer Desktop-Anwendung befinden sich beide Seiten lokal:
Vaadin Flow
Java Server
│
│ HTTP / WebSocket
▼
Browser
Keluna reduziert dieses Modell auf:
Kotlin/Native
│
│ lokale WebView Bridge
▼
System WebView
Dadurch entfallen große Teile der Komplexität von Vaadin:
- HTTP
- WebSocket
- Sessions
- Reconnect
- Authentication zwischen Client und Server
- Netzwerk-Retry
- Cluster-Synchronisation
Übrig bleibt im Wesentlichen:
Component Tree
│
▼
DOM Patch
│
▼
WebView
WebView
│
▼
Event
│
▼
Kotlin Handler
Tauri
Von Tauri übernimmt Keluna vor allem das Prinzip:
Nutze die Browser-Engine, die das Betriebssystem ohnehin bereitstellt.
Keluna verwendet deshalb unterschiedliche WebView-Implementierungen:
| Plattform | WebView |
|---|---|
| Windows | WebView2 |
| macOS | WKWebView |
| Linux | WebKitGTK |
Die WebView ist dabei kein eigenständiger Anwendungsteil.
Sie ist lediglich der Renderer für:
- HTML
- CSS
- Text
- Layout
- Form Controls
- SVG
- Animationen
Architektur
Keluna könnte zunächst aus sechs grundlegenden Bereichen bestehen.
keluna-core
keluna-ui
keluna-runtime
keluna-web
keluna-platform
keluna-components
keluna-core
Enthält grundlegende Framework-Abstraktionen.
Beispielsweise:
Component
Element
Property
State
Event
StateTree
Dieses Modul sollte weitgehend unabhängig von den konkreten Plattformen sein.
keluna-ui
Enthält die Kotlin-API zur Beschreibung der Benutzeroberfläche.
Beispielsweise:
Window
Layout
Button
TextField
Checkbox
Select
Table
Dialog
Tabs
Menu
Darauf baut die Kotlin-DSL auf.
keluna-runtime
Verwaltet den Komponentenbaum und den Anwendungszustand.
Aufgaben sind unter anderem:
- Component Lifecycle
- State Management
- Event Dispatching
- Erkennung von Änderungen
- Erzeugung von DOM-Patches
- Synchronisation mit der WebView
keluna-web
Enthält die kleine JavaScript-Runtime innerhalb der WebView.
Diese Runtime sollte bewusst minimal bleiben.
Ihre Aufgaben:
- DOM-Elemente erzeugen
- DOM-Properties ändern
- Elemente entfernen
- Events registrieren
- relevante Browser-Events an Kotlin weiterleiten
Beispielsweise könnte Kotlin intern folgende Instruktion erzeugen:
{
"op": "set",
"node": 17,
"property": "textContent",
"value": "Open"
}
Oder:
{
"op": "append",
"parent": 3,
"node": 17
}
Ein Klick könnte in Gegenrichtung übertragen werden:
{
"node": 17,
"event": "click"
}
Das Protokoll sollte später vermutlich kompakter sein als JSON.
Für einen ersten Prototyp wäre JSON jedoch ideal.
Plattform-Layer
Keluna benötigt für jedes Betriebssystem einen sehr kleinen nativen Adapter.
keluna-platform
│
├── linux
│ ├── GTK
│ └── WebKitGTK
│
├── macos
│ ├── Cocoa
│ └── WKWebView
│
└── windows
├── Win32
└── WebView2
Die gemeinsame API könnte beispielsweise Funktionen bereitstellen wie:
createWindow()
loadResource()
evaluateJavaScript()
receiveMessage()
showFileDialog()
setClipboard()
showNotification()
Die Anwendung selbst soll diese Plattformunterschiede normalerweise nicht sehen.
Native Desktop-Funktionen
Keluna soll nicht nur eine Web-Anwendung in einem Fenster darstellen.
Der Kotlin-Prozess besitzt direkten Zugriff auf das Betriebssystem.
Dadurch können Anwendungen beispielsweise folgende Funktionen nutzen:
- Dateisystemzugriff
- native File Dialogs
- Clipboard
- Drag & Drop
- Notifications
- System Tray
- Menüleiste
- Prozesse starten
- lokale Sockets
- Git
- Credential Stores
- Betriebssysteminformationen
Diese Funktionen können als Kotlin-APIs bereitgestellt werden.
Keluna Application
│
├── UI
│
├── Files
├── Process
├── Clipboard
├── Notifications
├── Dialogs
└── Platform APIs
Verantwortlichkeit zwischen Kotlin und WebView
Nicht jede UI-Interaktion sollte den Kotlin-Prozess erreichen.
Lokale Browserfunktionen sollten möglichst lokal bleiben.
WebView
Die WebView übernimmt beispielsweise:
- Hover
- CSS
- Animationen
- Scrolling
- Text Selection
- Focus-Darstellung
- Cursor
- einfache visuelle Interaktionen
Kotlin
Kotlin übernimmt:
- Application State
- Navigation
- Business Logic
- Commands
- persistente UI-Zustände
- Dateisystem
- Prozesse
- Netzwerk
- Git
- Betriebssystemintegration
Dadurch bleibt die IPC-Schicht klein.
State und Synchronisation
Der zentrale Bestandteil von Keluna wäre ein State Tree ähnlich dem Komponentenmodell von Vaadin Flow.
Beispiel:
Window #1
└── VerticalLayout #2
├── TextField #3
├── Button #4
└── Label #5
Jede Komponente besitzt eine interne ID.
Ändert Kotlin beispielsweise:
Label #5
"Ready"
↓
"Loading..."
muss lediglich folgende Änderung an die WebView übertragen werden:
SET #5 textContent "Loading..."
Es wird nicht die gesamte Benutzeroberfläche neu aufgebaut.
Event-Modell
Events laufen in Gegenrichtung.
Beispielsweise:
WebView
Button #4 clicked
│
▼
IPC
│
▼
Keluna Runtime
│
▼
Kotlin callback
In der Anwendung könnte das beispielsweise so aussehen:
button("Open") {
openProject()
}
Keluna registriert intern lediglich:
#4 → click → callback
HTML und CSS
HTML und CSS sind Implementierungsdetails der Rendering-Schicht.
Der Entwickler muss sie nicht zwingend direkt verwenden.
Für fortgeschrittene Anwendungen sollte Keluna sie jedoch nicht verstecken.
Denkbar wären deshalb mehrere Ebenen:
High Level
Keluna Components
↓
Keluna Element API
↓
HTML / CSS
↓
System WebView
Dadurch könnten Entwickler bei Bedarf eigene Web Components oder CSS einbinden.
Custom Components
Neben Standardkomponenten sollte Keluna eigene Komponenten erlauben.
Beispielsweise:
Graph
CodeEditor
Tree
MarkdownViewer
Terminal
Diagram
Solche Komponenten könnten aus:
- Kotlin
- HTML
- CSS
- etwas JavaScript
bestehen.
Für Spezialfälle könnte auch eine bestehende JavaScript-Komponente eingebettet werden.
Keluna soll JavaScript also nicht verbieten.
Es soll lediglich für normale Anwendungsentwicklung nicht notwendig sein.
Packaging
Ein wichtiges Ziel von Keluna ist eine kleine Distribution.
Eine Anwendung besteht im Wesentlichen aus:
Kotlin/Native executable
+
Keluna runtime
+
HTML/CSS/JS resources
Die Browser-Engine wird nicht mitgeliefert.
Auf den Plattformen wird stattdessen verwendet:
Windows → WebView2
macOS → WKWebView
Linux → WebKitGTK
Unter Linux würden GTK und WebKitGTK typischerweise Paketabhängigkeiten darstellen.
Ein Debian-Paket könnte beispielsweise von diesen Systembibliotheken abhängen, anstatt sie selbst mitzubringen.
Dadurch sollte eine einfache Keluna-Anwendung potenziell im Bereich von wenigen Megabyte bis niedrigen zweistelligen Megabyte bleiben können.
Die tatsächliche Größe müsste durch einen Prototyp überprüft werden.
Startzeit
Keluna benötigt:
- keine JVM-Initialisierung
- keinen Node-Prozess
- keinen Start eines eingebetteten Chromium
- keine eigene Grafik-Runtime
Der native Prozess startet lediglich die vorhandene WebView und lädt lokale Ressourcen.
Damit sollte die Startzeit eher in der Größenordnung kleiner nativer Anwendungen bzw. Tauri-Anwendungen liegen als bei Compose Desktop oder Electron.
Auch dies müsste frühzeitig durch einen Proof of Concept gemessen werden.
Ziele
Keluna soll:
- Desktop-Anwendungen vollständig oder nahezu vollständig in Kotlin ermöglichen
- Kotlin/Native verwenden
- Linux, macOS und Windows unterstützen
- kleine Distributionen erzeugen
- schnell starten
- die vorhandene System-WebView verwenden
- keine eigene Rendering-Engine benötigen
- keinen Webserver starten
- keine JVM voraussetzen
- keine JavaScript-Anwendungsarchitektur erzwingen
- sich natürlich in Gradle und Kotlin Multiplatform integrieren
Nicht-Ziele
Keluna soll zunächst ausdrücklich kein:
- Browser-Webframework
- Mobile-Framework
- eigener Browser
- eigener HTML-Renderer
- eigener GPU-Renderer
- Electron-Ersatz mit eingebettetem Chromium
- vollständiges Vaadin-Flow-Reimplementation
sein.
Ebenso sollte Keluna nicht versuchen, von Anfang an hunderte UI-Komponenten bereitzustellen.
Ein mögliches MVP
Ein erster Proof of Concept benötigt überraschend wenig.
Phase 1 – WebView
Für eine Plattform, beispielsweise Linux:
- Kotlin/Native Executable
- GTK-Fenster
- WebKitGTK-WebView
- lokale
index.htmlladen - JavaScript aus Kotlin aufrufen
- Nachricht von JavaScript an Kotlin senden
Damit wäre die technische Grundlage bewiesen.
Phase 2 – Element Tree
Ein einfacher Kotlin-Elementbaum:
Window
Column
Text
Button
Dieser erzeugt DOM-Strukturen.
Phase 3 – Events
Browser-Events werden an Kotlin übertragen:
click
change
input
keydown
Phase 4 – State Synchronisation
Änderungen im Kotlin-Component-Tree erzeugen nur noch inkrementelle DOM-Patches.
Phase 5 – weitere Plattformen
Implementierung von:
- WKWebView für macOS
- WebView2 für Windows
Phase 6 – Desktop APIs
Erste native Funktionen:
- File Picker
- Clipboard
- Notifications
- Prozessstart
Mögliche Modulstruktur
keluna/
├── keluna-core
├── keluna-ui
├── keluna-runtime
├── keluna-components
├── keluna-web-runtime
├── keluna-platform-linux
├── keluna-platform-macos
├── keluna-platform-windows
└── keluna-gradle-plugin
Eine Anwendung könnte anschließend beispielsweise nur benötigen:
keluna-core
keluna-components
keluna-platform
Gradle-Integration
Langfristig sollte eine Keluna-Anwendung über ein Gradle-Plugin konfiguriert werden können.
Konzeptionell:
plugins {
kotlin("multiplatform")
id("dev.keluna.application")
}
keluna {
applicationName = "My Application"
mainClass = "MainKt"
}
Das Plugin könnte anschließend:
- Kotlin/Native Targets konfigurieren
- Web-Ressourcen bündeln
- native Abhängigkeiten konfigurieren
- Installer erzeugen
- Signing vorbereiten
- Plattformpakete bauen
Entwicklungsmodell
Keluna könnte zwei Ebenen anbieten.
High-Level
Für normale Anwendungen:
Keluna Components
+
Kotlin State
+
Kotlin Event Handler
Der Entwickler benötigt keine Webkenntnisse.
Low-Level
Für Spezialkomponenten:
DOM
CSS
JavaScript
Web Components
Dadurch bleibt das Framework offen.
Eine komplexe Komponente wie ein Code-Editor könnte beispielsweise intern Monaco oder CodeMirror verwenden, während die restliche Anwendung vollständig in Kotlin bleibt.
Designprinzipien
Kotlin first
Die öffentliche API wird für Kotlin entworfen und nicht aus einer anderen Sprache übernommen.
Native application first
Keluna ist kein Webframework, das zufällig Desktop-Fenster erzeugt.
Es ist ein Desktop-Framework, das Web-Technologie zum Rendern verwendet.
System WebView instead of bundled browser
Der größte Baustein vieler Desktop-Webframeworks wird bewusst nicht ausgeliefert.
Thin client
Die JavaScript-Runtime sollte möglichst klein und weitgehend zustandslos bleiben.
Escape hatches
HTML, CSS und JavaScript bleiben erreichbar, wenn eine Anwendung sie benötigt.
Incremental adoption
Eine Anwendung sollte mit wenigen Standardkomponenten beginnen können und später komplexere eigene Komponenten hinzufügen können.
Measure early
Binary-Größe, RAM-Verbrauch und Startzeit sind zentrale Eigenschaften des Projekts und sollten von Anfang an automatisiert gemessen werden.
Risiken
Unterschiedliche WebView-Engines
Windows verwendet Chromium über WebView2.
macOS und Linux verwenden WebKit.
Dadurch können Unterschiede bei:
- CSS
- JavaScript
- Web APIs
- Rendering
auftreten.
Keluna sollte deshalb bewusst nur auf gut unterstützte Webstandards setzen.
Linux-Abhängigkeiten
Linux besitzt keine so klar definierte System-WebView wie macOS.
WebKitGTK und GTK müssen auf dem Zielsystem vorhanden sein.
Distribution und Packaging müssen deshalb früh untersucht werden.
Kotlin/Native Interop
Die Plattformadapter benötigen Interop mit:
- Win32 und COM
- Cocoa/Objective-C
- GTK und WebKitGTK
Dieser Bereich dürfte einer der technisch anspruchsvollsten Teile des Frameworks sein.
Er bleibt jedoch innerhalb des Frameworks verborgen und betrifft nicht den normalen Anwendungsentwickler.
UI-State-Komplexität
Ein nativer Komponentenbaum klingt zunächst einfach.
Komplexität entsteht jedoch bei:
- Textfeldern während der Eingabe
- Selection
- Focus
- Cursorpositionen
- Drag & Drop
- großen Tabellen
- virtuellen Listen
Keluna sollte deshalb nicht versuchen, jeden Browserzustand permanent nach Kotlin zu spiegeln.
Langfristige Perspektive
Keluna könnte langfristig drei Ebenen bilden:
Keluna
│
┌───────────┴───────────┐
│ │
Kotlin Components Custom Components
│ │
└───────────┬───────────┘
│
Component Tree
│
Keluna Runtime
│
WebView
│
Native Platform
Damit entsteht ein ungewöhnliches Desktop-Modell:
Native application logic with web-based rendering, without turning the application itself into a web application.
Zusammenfassung
Keluna kombiniert drei bewährte Ideen:
Kotlin/Native
für:
- Anwendungscode
- Domain-Logik
- State
- Betriebssystemzugriffe
Vaadin-Flow-artiges Komponentenmodell
für:
- Component Tree
- Properties
- Events
- inkrementelle UI-Synchronisation
Tauri-artige System-WebView-Nutzung
für:
- kleines Packaging
- schnelles Startup
- HTML/CSS-Rendering ohne eingebettetes Chromium
Das Ziel ist eine Desktop-Anwendung, die sich für den Entwickler wie eine normale Kotlin-Anwendung anfühlt:
Kotlin
↓
Keluna
↓
System WebView
↓
Native Desktop
Der Browser ist dabei kein Teil der Anwendungsarchitektur.
Er ist lediglich der Renderer.
Keluna – Kotlin Embedded Local UI for Native Applications.