zurück zum Artikel

Multiplattform-Entwicklung für Android und Web? Kotlin macht‘s möglich!

Robin Holzwarth

(Bild: Andrey Suslov/Shutterstock.com)

Kotlin Multiplatform kann mehr als nur Mobile. Seit Kotlin 1.9.20 gilt es neben den Versionen für Android, iOS und Desktop für das Web als stabil.

Kotlin Multiplatform (KMP) ist JetBrains' neues Steckenpferd und soll etablierten Frameworks wie Flutter oder React Native den Rang ablaufen. Googles Entscheidung, Kotlin zur offiziellen Programmiersprache für die Android-Entwicklung zu machen, bietet dafür beste Voraussetzungen. Denn Android-Entwicklerinnen und -Entwickler, die schon mit Kotlin arbeiten, können ihre Skills nutzen, um weitere Plattformen abzudecken. Neben Android-, iOS- und Desktop-Plattformen ermöglicht KMP es auch, für das Web zu entwickeln.

Dabei bietet KMP in vielen Bereichen sehr viel Flexibilität. So auch bei der Entscheidung, Frontend-Code mit Compose Multiplatform zwischen allen Zielplattformen zu teilen oder die jeweiligen Frontends nativ zu entwickeln.

Im Web zeigt der Flutter-Ansatz, den gesamten Code zu teilen, signifikante Schwächen. Webseiten sehen oft ungewöhnlich aus und verhalten sich manchmal nicht wie gewünscht, beispielsweise beim Scrollen. Zudem leiden die Sichtbarkeit und Auffindbarkeit der Webseite stark darunter. Sogar Flutter stellt deshalb die eigene Dokumentation [1] nicht als Flutter-generierte Webseite, sondern als HTML-Seite zur Verfügung.

KMP lässt im Unterschied dazu den Developern mehr Freiheit. Das Frontend lässt sich bei Bedarf separat entwickeln. Diese Anpassungsfähigkeit hilft, die Herausforderungen einer einheitlichen Benutzeroberfläche – wie eine weniger personalisierte Nutzererfahrung – zu überwinden, während gleichzeitig Vorteile der plattformübergreifenden Entwicklung, wie Kosteneinsparungen, erhalten bleiben.

JetBrains' Entwicklungsumgebungen IntelliJ IDEA und Android Studio bieten einige Möglichkeiten, KMP-Projekte mithilfe eines Wizards zu generieren. Erfahrungsgemäß müssen Entwicklerinnen und Entwickler die Projekte oft selbst konfigurieren. Beispielsweise braucht es für die beiden Zielplattformen Android und Web drei Module (Abbildung 1): common, webApp und androidApp. Gradle-Build-Skripte verknüpfen die drei Module miteinander. Auf die Modulstruktur geht der Artikel im weiteren Verlauf ein, vorerst liegt der Fokus auf den Gradle-Skripten.

Modulstruktur einer KMP-Anwendung für Android und Web (Abb. 1).

Für die beiden Zielplattformen braucht es insgesamt vier Gradle-Build-Skripte.

Die build.gradle.kts-Datei in Kombination mit der settings.gradle.kts-Datei ist für den Build-Prozess des Projekts verantwortlich. Die beiden enthalten die Definitionen der globalen Bibliotheken, Abhängigkeiten und Plug-ins, die im gesamten Projekt zum Einsatz kommen. Zusätzlich findet hier auch die Versionsverwaltung statt – etwa die der Kotlin-Version. Die settings.gradle.kts-Datei listet alle Module auf, die der Build-Prozess berücksichtigen soll. Zusätzlich enthalten das Common-Modul sowie die Plattform-Module jeweils ein Gradle-Skript.

Das Gradle-Skript im Common-Modul konzentriert sich auf die plattformübergreifenden Aspekte der Anwendung. Es verwaltet die Bibliotheken und Abhängigkeiten, die im gesamten Projekt Verwendung finden – unabhängig von der spezifischen Plattform. Das Skript stellt sicher, dass die Kernfunktionalitäten und -logiken plattformunabhängig bleiben.

Jedes Plattform-Modul, wie das Android-Modul, hat sein eigenes build.gradle.kts-Skript. Es ist für das plattformspezifische Build-Setup zuständig und enthält Konfigurationen, die ausschließlich für die jeweilige Plattform relevant ist. Hier finden sich Definitionen zu plattformspezifischen Tasks, Dependencies und Plug-ins. Das Android-Modul enthält spezielle Einstellungen für das Android-Gradle-Plug-in und die SDK-Version.

Um eine Verbindung zwischen den Plattform-Modulen und dem Common-Modul herzustellen, ist in den Gradle-Skripten der Plattform-Module eine Abhängigkeit definiert: Implementation(project(„:common“)).

Das Hinzufügen dieser Zeile ermöglicht es, innerhalb der Plattform-Module auf den geteilten Code im Common-Modul zuzugreifen.

Die Fallstricke im Rahmen des Projektaufbaus liegen besonders bei inkompatiblen Versionen und Plug-ins, die sich gegenseitig und unerwünscht beeinflussen. Denn jedes Gradle-Skript führt Aufgaben aus, die mitunter undurchsichtig sein können, gerade dann, wenn Plug-ins im Spiel sind. Das kann dazu führen, dass der Build fehlschlägt und die Fehlersuche beginnt. Insbesondere wenn Entwicklerinnen und Entwickler die Gradle-Skripte nicht richtig verstehen, kann diese Fehlersuche kraftraubend sein. Der Copy-and-Paste-Ansatz scheint anfangs vielleicht attraktiv für Gradle-Neulinge, kostet aber auf Dauer viel mehr Nerven.

Das Herzstück jeder KMP-Anwendung ist das Common-Modul. Es beherbergt den Code, der plattformübergreifend verwendet wird – beispielsweise Datenmodelle, Netzwerklogik, ViewModels und Use Cases (Abbildung 2).

Beispielhaftes Architekturdiagramm einer KMP-Anwendung für Android und Web (Abb. 2)

Innerhalb des Common-Moduls befinden sich Unterordner. commonMain enthält die Definition plattformunabhängigem Codes. Um plattformspezifische Anpassungen vorzunehmen, ohne die gemeinsame Logik zu beeinträchtigen, sind zwei weitere Unterordner vorgesehen: jsMain für das Web und androidMain für Android.

Diese Struktur ermöglicht den Einsatz der expect/actual-Funktionalität (Abbildung 3). Das bedeutet, dass Entwicklerinnen und Entwickler in commonMain generelle Erwartungen (expects) bezüglich des Verhaltens setzen, und in den spezifischen Ordnern (jsMain und androidMain) wird dann die tatsächliche (actual) Umsetzung dieser Erwartungen vorgenommen. Man kann sich das vorstellen wie ein Interface, das sich implementieren lässt.

Expect-/actual-Mechanismus zur Nutzung des lokalen Speichers (Abb. 3)

Ein praxisnahes Beispiel ist das Speichern von Daten. Während Key-Value-Paare im Web oft in localStorage des Browsers zu finden sind, kommen dafür in Android die SharedPreferences zum Einsatz. Kein Grund, die gesamte Logik zum Ablegen dieser Daten separat zu implementieren. commonMain enthält die Definition einer expect-Klasse oder -Funktion und der Compiler erwartet die entsprechende actual-Implementierung in androidMain und jsMain.

Beim Ausführen der Anwendung wird dann zur Laufzeit ermittelt, welches Verhalten für die jeweilige Plattform gelten soll.

Zusätzlich zum Common-Modul gibt es die plattformspezifischen Module androidApp und webApp. Darin implementiert man alles, was tatsächlich nur für die jeweilige Plattform nötig ist, wie das Frontend und die Navigationslogik.

Code zu teilen, verringert den Wartungsaufwand und reduziert Entwicklungskosten. Diese Hoffnung erfüllt sich spätestens dann nicht, wenn das Multiplattform-Framework spezielle plattformspezifische APIs nicht unterstützt. KMP bringt mit seinem flexiblen Ansatz frischen Wind in die Betrachtung. Entwicklerinnen und Entwickler können projektspezifisch entscheiden, welche Elemente des Codes sie teilen möchten. Das ist besonders praktisch, wenn sich die Zielplattformen so gravierend unterscheiden wie Android und Web.

Das wachsende Ökosystem und die begeisterte Community um KMP ermöglichen es, dass sich trotz der großen Unterschiede viele Komponenten und Funktionen teilen lassen. Denn die meisten plattformspezifischen Probleme lassen sich mit geeigneten Bibliotheken lösen.

Beim plattformübergreifenden Implementieren der Netzwerklogik kann JetBrains' quelloffene Kotlin-Bibliothek Ktor helfen. Eine plattformunabhängige lokale Datenbank zu implementieren, stellt dank SQLDelight keine Hürde dar. Dependency Injection für Android und Web? Kodein (Kotlin Dependency Injection) macht’s möglich! Und selbst die Android-Entwicklern vertrauten ViewModels lassen sich dank kmp-viewmodel plattformübergreifend nutzen [2].

Für all die, die jetzt die Nase rümpfen, bei dem Gedanken Model-View-ViewModel (MVVM) im Web zu nutzen, gibt es natürlich auch andere Möglichkeiten, wie Model-View-Intent (MVI) [3].

Sich zu sehr auf verschiedene, mitunter kleine Bibliotheken zu verlassen, birgt natürlich auch Risiken, wie Wartungsunsicherheit, Kompatibilitätsprobleme und potenzielle Sicherheitslücken. Hier bedarf es sinnvollen Abwägens, wann eine Bibliothek und wann lieber das expect/actual-Konzept zum Einsatz kommen sollte. Außerdem ist es ratsam, die Zukunftsfähigkeit der genutzten Bibliotheken im Blick zu behalten. Sonst kommt es im Projektverlauf möglicherweise zu bösen Überraschungen, wenn man eine weitere Plattform ergänzen möchte.

JetBrains legt den Fokus aktuell unter anderem auf Compose Multiplatform und den mobilen Bereich. Das wird besonders klar, wenn man sich abseits von Compose Multiplatform nach Möglichkeiten umschaut, Webseiten mit Kotlin zu bauen. Dabei gibt es mit Compose HTML eine interessante Alternative. Die Bibliothek überträgt den reaktiven und komponentenbasierten Compose-Ansatz auf die Webentwicklung. Compose HTML wird zwar im GitHub-Repository [4] von Compose Multiplatform als Option angegeben, um für das Web zu entwickeln, doch die Beschreibung der Bibliothek ist recht gut versteckt [5].

Dennoch setzen auf Compose HTML verschiedene Projekte auf. So zum Beispiel KVision, fritz2, Doodle und Kobweb. Kobweb fährt dabei einen sehr interessanten Ansatz: Jetpack Compose für das Web, ohne die Einschränkungen, die Compose Multiplatform mit sich bringt.

Kobweb definiert seine eigene API, die der von Jetpack Compose aber teilweise sehr ähnlich ist. Das ist besonders vorteilhaft für die Zielplattformen Android und Web. Denn es erstellt zwei separate Frontends, wobei sich viele Teile des Codes aber wiederverwenden lassen oder nur geringfügiger Anpassungen bedürfen.

Die Abbildungen 4 und 5 zeigen, wie ähnlich die APIs aussehen können.

Beispiel Composable für Android in Jetpack Compose (Abb. 4)

Beispiel Composable für das Web in Kobweb (Abb. 5)

Die bekannten Layout-Elemente Row und Column lassen sich in Kobweb nahezu identisch wie in Jetpack Compose verwenden. Die Reihenfolge der Modifier ist im Gegensatz zu Jetpack Compose allerdings irrelevant. Das liegt daran, dass Kobweb aus den gesetzten Modifiern im Hintergrund HTML-Elemente erzeugt und diese mittels CSS entsprechend anpasst.

Ein weiterer Unterschied besteht darin, wie das Framework Texte, Buttons und andere Seitenelemente erzeugt. Dafür greift es direkt auf die entsprechenden Compose-HTML-Elemente zu. Daraus entstehen im Hintergrund HTML-Elemente – native Webentwicklung mit Kotlin. Aus dem gesamten Konstrukt generiert Kobweb den Quellcode der Webseite und hilft darüber hinaus bei der Navigation, ermöglicht Live Reloading und unterstützt Markdown.

Der entscheidende Vorteil gegenüber Compose Multiplatform betrifft das Arbeiten mit nativen Webtechnologien. Skripte lassen sich einbinden – wer möchte, kann mit Bootstrap designen. Die Sichtbarkeit der Seite wird dadurch nicht beeinträchtigt.

Eine Kobweb-App als Modul in ein Multiplatform-Projekt einzubinden, ist aktuell noch etwas umständlich. Folgt man den Hinweisen auf GitHub [6], verläuft die Implementierung aber meist reibungslos.

Der größte Nachteil von Kobweb liegt wohl darin, dass das Framework vergleichsweise jung und abhängig von einigen wenigen Entwicklerinnen und Entwicklern ist. Mit Kobweb erstellte Seiten tendieren außerdem dazu, recht groß zu werden. Bei umfangreichen Web-Apps kann das die Performance beeinträchtigen.

Die Android-Entwicklung in einem KMP-Projekt unterscheidet sich nur marginal von der gewohnten Android-Entwicklung. In der build.gradle.kts des Android-Moduls findet sich die Definition, die vorgibt, dass der Code vom Common-Modul abhängig ist. Im Anschluss lassen sich die im Common-Modul definierten Klassen und Funktionen auch im Android-Modul verwenden. Der Rest ist dann gewöhnliche Android-Entwicklung mit Kotlin und daher im Rahmen dieses Artikels weniger interessant.

Ein großer Vorteil von KMP liegt in der Magie, die in dem Begriff Gradual Adoption steckt, auf Deutsch: schrittweise Einführung. Denn KMP ermöglicht es, die bestehende Codebasis Schritt für Schritt multiplattform-fähig zu machen.

Das funktioniert besonders gut, wenn als Grundlage eine Kotlin-Android-App existiert. Dann lassen sich nach und nach Teile des Codes aus dem Android-Modul extrahieren und ins Common-Modul verschieben. Natürlich ist dabei darauf zu achten, keine Android-spezifische Logik zu verwenden, oder diese, wenn nötig mithilfe der expect/actual-Funktionalität einzubinden. Das Großartige daran: Es braucht keinen Big-Bang-Ansatz. Dank der Interoperabilität von KMP lässt sich ein Bereich nach dem anderen multiplattform-fähig machen.

Aktuell legt JetBrains unter anderem den Fokus auf das Komplettpaket Compose Multiplatform. Doch gerade für das Web bringt der Ansatz einer geteilten Benutzeroberfläche gravierende Einschränkungen mit sich. Benutzeroberflächen wirken oft unnatürlich, sie beeinträchtigen die Auffindbarkeit der Webseite und Developer können nicht auf bestehende Software im Webumfeld zugreifen.

Doch KMPs Flexibilität schafft Abhilfe. Entwicklerinnen und Entwickler können bei Bedarf native Webtechnologien verwenden. Die von JetBrains entwickelte Schnittstelle Compose HTML ermöglicht es, HTML/CSS-Webseiten mittels Kotlin zu entwickeln. Auf dieser Schnittstelle setzen verschiedene Frameworks auf, die die Webentwicklung mit Kotlin erleichtern. Eines der Frameworks, Kobweb, unterscheidet sich in der Handhabung kaum von Jetpack Compose. Das hat den Vorteil, dass diejenigen, die mit Android arbeiten, ohne lange Einarbeitungszeiten auch für das Web entwickeln können.

Besonders für Projekte, in denen schon eine Kotlin-Android-App existiert, ist eine Webseite auf Basis von Kotlin attraktiv. Vorhandene Android-Apps lassen sich schrittweise zu KMP-Anwendungen umbauen, und große Teile der Logik können ohne erneutes Implementieren der Web-App zugänglich gemacht werden. Das spart Entwicklungs- und Wartungskosten.

Allerdings sollten auch die Einschränkungen nicht unter den Tisch fallen. Die vorhandenen Kotlin-Frameworks für das Web sind oft kleine, von wenigen Entwicklern getragene Projekte. JetBrains pflegt die Schnittstelle zwar weiterhin, versteckt die Beschreibung von Compose HTML aber recht gut. Es bleibt abzuwarten, ob sich ein Ansatz der Webentwicklung in KMP durchsetzt.

Robin Holzwarth
ist Experte für Cross- und Multiplattform-Entwicklung bei CGI in Frankfurt. Er setzt sich intensiv mit neuen Technologien und Frameworks auseinander und testet deren Reifegrad sowie deren Einsatzfähigkeit im geschäftlichen Kontext.

(mdo [7])


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

Links in diesem Artikel:
[1] https://docs.flutter.dev/platform-integration/web/faq#search-engine-optimization-seo
[2] https://github.com/hoc081098/kmp-viewmodel
[3] https://github.com/arkivanov/MVIKotlin
[4] https://github.com/JetBrains/compose-multiplatform#compose-html
[5] https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials/HTML
[6] https://github.com/varabyte/kobweb#adding-kobweb-to-an-existing-project
[7] mailto:mdo@ix.de