Ein modernes Kotlin Multiplatform Projekt mit Ktor, Arrow-Kt, Kotest & Clean Architecture
In diesem Blogbeitrag wird ein Best-Practice-Projekt vorgestellt, das mit Compose und Kotlin Multiplatform umgesetzt wurde. Das Ziel ist es, eine App zu entwickeln, die Superhelden-Daten aus der Marvel API lädt und anzeigt. Dabei kommen verschiedene moderne Kotlin-Technologien wie Ktor, Compose Multiplatform, Arrow-Kt, Kotest und Turbine zum Einsatz. Zudem findet ein Vergleich der beiden Architekturmuster MVVM und MVI statt. Der Fokus liegt dabei auf einer vollständigen Kotlin-basierten Lösung, um ein echtes multiplattformfähiges und modernes Architekturbeispiel zu schaffen.
Inhaltsverzeichnis
Projektüberblick
Das Projekt besteht aus vier Schichten: API-Client, Repository, ViewModel und UI. Die Marvel API liefert Superhelden-Daten, die mithilfe von Ktor geladen, mit Arrow-Kt funktional behandelt, im ViewModel über StateFlow verwaltet und in einer Compose UI dargestellt werden. Die Tests basieren auf Kotest und Turbine für eine moderne und asynchrone Teststrategie.
Ktor – Die API-Anbindung
Ktor ist ein moderner HTTP-Client, der sich hervorragend in Kotlin-Projekte integriert. In diesem Projekt wird Ktor für die Kommunikation mit der Marvel API verwendet. Ktor erlaubt die Konfiguration von Standard-Parametern, Logging, JSON-Serialisierung und Fehlerbehandlung auf elegante Weise.

Ktor ist vollständig Kotlin-basiert und bietet First-Class-Support für Multiplatform, was es ideal für Compose Multiplatform macht. Durch die gute Erweiterbarkeit und modulare Struktur kann es flexibel an verschiedene Anwendungsfälle angepasst werden.
Arrow-Kt – Funktionale Fehlerbehandlung
Fehlerbehandlung ist ein kritischer Teil jeder App. Arrow-Kt bringt funktionale Programmierkonzepte nach Kotlin. In diesem Projekt wird die Raise-API und der either-Block von Arrow verwendet, um Fehler klar und typisiert zu behandeln.

Diese Herangehensweise sorgt für eine saubere Trennung von Logik und Fehlerhandling und vermeidet verschachtelte Try-Catch-Blöcke. Sie trägt dazu bei, die Fehlerbehandlung explizit und nachvollziehbar zu gestalten, was sich positiv auf die Wartbarkeit auswirkt.
Repository-Schicht
Das Repository kapselt die Datenlogik. Diese kommuniziert mit der API und liefert ein Flow von Superhelden. Dadurch bleibt die Datenquelle austauschbar und testbar.
Die Daten werden paginiert geladen. Sobald alle Daten geladen sind, wird der Flow abgeschlossen. Die Verwendung von Flow erlaubt eine reaktive Verarbeitung der Daten und eine klare Trennung zwischen Datenquelle und UI.
ViewModel mit StateFlow
Das ViewModel verwaltet den UI-Zustand mithilfe von StateFlow. Die Funktion fetchCharacters() startet einen asynchronen Ladevorgang und aktualisiert den State entsprechend:

Dies ermöglicht eine saubere Trennung von Logik und UI, erleichtert das Testen und sorgt für konsistentes State-Handling.
Die UI mit Compose Multiplatform
Mit Compose Multiplatform wird die Benutzeroberfläche deklarativ aufgebaut. Der aktuelle state wird beobachtet und reagiert auf Änderungen. Die Darstellung erfolgt über LazyColumn, Image, Text und weitere Compose-Komponenten.
Durch die plattformübergreifende Funktionalität von Compose kann dieselbe UI auf Android, Desktop und auch auf iOS (mittlerweile stable) genutzt werden – ein großer Vorteil für Teams, die eine konsistente Nutzererfahrung über Plattformen hinweg schaffen wollen.
Tests mit Kotest und Turbine
Kotest ist eine expressive Testbibliothek für Kotlin, während Turbine speziell für das Testen von Flows gedacht ist. Mit beiden Tools lassen sich StateFlow-basierte ViewModels präzise und asynchron testen.
Ein Beispieltest mit Turbine:

Diese Kombination erlaubt eine vollständige Abdeckung und Kontrolle asynchroner Zustände. Turbine erleichtert das Testen von Flow-Emissionen durch eine intuitive API, während Kotest durch seine DSL eine bessere Lesbarkeit der Tests ermöglicht.
Warum diese Bibliotheken?
Für ein modernes Compose Multiplatform-Projekt ist es essenziell, plattformunabhängige und idiomatische Tools zu verwenden:
Ktor: Ein schlanker, vollständig in Kotlin geschriebener HTTP-Client mit Multiplatform-Support. Ideal für API-Kommunikation auf allen Plattformen.
Arrow-Kt: Bringt funktionale Programmierparadigmen nach Kotlin, erlaubt elegantes Error-Handling mit typsicheren Exceptions.
Kotest & Turbine: Mächtige, moderne Testwerkzeuge mit perfekter Integration für Coroutines und Flows. Sie bieten ein hohes Maß an Kontrolle und Präzision bei der Testentwicklung.
Compose Multiplatform: Ermöglicht UI-Entwicklung über Plattformen hinweg mit nur einer Codebasis.
Alle gewählten Technologien stammen aus dem Kotlin-Ökosystem und bieten damit bestmögliche Integration und Wartbarkeit in einem Compose Multiplatform-Projekt.
MVVM vs. MVI – Ein Architekturvergleich
Dieses Projekt wurde initial mit dem Model-View-ViewModel (MVVM)-Architekturmuster umgesetzt. Eine Alternative dazu ist das Model-View-Intent (MVI)-Muster, das auf einem einheitlichen, unidirektionalen Datenfluss basiert. Beide Muster bieten Vor- und Nachteile:
MVVM – Aktuelle Implementierung
MVVM verwendet ein ViewModel, um UI-Status über StateFlow zu verwalten. Die UI beobachtet diesen Status und reagiert darauf. Änderungen fließen bidirektional. Dieses Muster ist leichtgewichtig und intuitiv für viele Entwicklerteams.
MVI – Alternative Umsetzung
MVI nutzt einen zentralen State, der nur durch definierte Intents verändert wird. Jede Nutzerinteraktion erzeugt einen Intent, der dann im ViewModel verarbeitet wird. Die UI selbst ist rein reaktiv. Das Muster basiert auf einem unidirektionalen Datenfluss und verwendet häufig ein „Reducer“-Konzept zur State-Veränderung.
Vergleich
| Kriterium | MVVM | MVI |
| Datenfluss | Bidirektional | Unidirektional |
| Testbarkeit | Bei komplexen Zuständen fehleranfälliger | Deterministischer State |
| Komplexität | Geringe Einstiegshürde | Höhere Komplexität, mehr Boilerplate Code |
|
Fehleranfälligkeit |
Eventuell inkonsistente Zustände | Klare Trennung durch reducer-Funktion |
| State Historie | Nicht automatisch rückverfolgbar | Historie lässt sich leichter rekonstruieren |
Wann welches Muster?
MVI ist besonders hilfreich bei größeren Projekten mit komplexem UI-Zustand, da der deterministische Datenfluss das Debuggen erleichtert und die Logik besser strukturiert. Besonders bei hohem Anspruch an Nachvollziehbarkeit und Testbarkeit empfiehlt sich MVI.
MVVM eignet sich besonders für kleinere bis mittelgroße Anwendungen, bei denen die Anzahl an Zuständen und Übergängen überschaubar bleibt. Es bietet eine gute Lesbarkeit und ist einfacher umzusetzen.
Fazit
Dieses Projekt zeigt die Stärken von Kotlin und Compose Multiplatform bei der Entwicklung moderner Apps. Die Kombination aus Ktor, Arrow-Kt, StateFlow, Compose Multiplatform und Kotest ermöglicht eine robuste und skalierbare Architektur.
Besonders hervorzuheben ist die klare Trennung der Schichten, die funktionale Fehlerbehandlung mit Arrow-Kt und die vollständige Testabdeckung durch Kotest & Turbine. In Verbindung mit MVVM (oder optional MVI) ergibt sich ein modernes, produktionsreifes Fundament für Kotlin-basierte Anwendungen über alle Plattformen hinweg.
Das ganze Projekt ist hier zu finden: https://github.com/fabian-rump/cmpsuperheroes
Die MVI-Lösung dazu befindet sich in einem separaten Branch: https://github.com/fabian-rump/cmpsuperheroes/tree/feature/mvi-architektur
Quellen
https://www.jetbrains.com/compose-multiplatform/
https://kotest.io/
https://github.com/cashapp/turbine
https://arrow-kt.io/
https://ktor.io/
