Inhaltsverzeichnis
Einleitung
Manchmal kann es schon praktisch sein, wenn man ein Haus baut und die Toilette direkt neben dem Kühlschrank platziert. Die Betonung liegt auf manchmal, denn hin und wieder sind Entscheidungen dieser Art auch mit Nachteilen verbunden – vor allem, wenn es sich um solche handelt, die nur mit großen Kosten und Mühen zu ändern sind. Was für den Hausbau und die Wahl der richtigen Toilettenplatzierung gilt, hat ebenso Gültigkeit für die Wahl der richtigen Architektur bei Apps. Der folgende Beitrag richtet seinen Fokus vorrangig auf die Wahl der Architektur in Kotlin Multiplatform (KMP), wobei es sich streng genommen nicht um eine Wahl handelt – denn MVI ist die einzige – weil ideale – Architektur. Case closed.
Bei anderen Cross-Plattform-Frameworks wie beispielsweise Flutter werden UI und Geschäftslogik nicht durch das System selbst getrennt, was dazu führen kann, dass sie sich stark vermischen. Bei einfacheren Anwendungen mag das kein Problem sein – wenn es jedoch komplexer wird, kann es schnell undurchsichtig werden. Außerdem bekommt man nicht den sweet sweet native Speed in der UI – ein niemals erreichtes Ziel bei plattformübergreifender Entwicklung … bis jetzt:
Denn die Trennung zwischen UI und Logik ist bei Kotlin Multiplatform erzwungen und ermöglicht es, auszuschließen, dass sich Shared Code und UI vermischen.1 Das bedeutet gleichzeitig aber auch, dass plattformgebundene Konzepte wie beispielsweise ViewModel, LiveData oder ObservableObject nicht anwendbar sind, weil der Shared Code plattformagnostisch und deterministisch sein muss: Gleiche Ereignisse sollen immer gleiche Ergebnisse produzieren.
Und hier kommt die Architektur MVI to the rescue. MVI steht dabei für Model-View-Intent und ermöglicht einen klar strukturierten, unidirektionalen Datenfluss, bei dem die Logik sauber von der UI getrennt wird. Dabei zeigt sich gleichzeitig, wieso MVVM gänzlich ungeeignet für Kotlin Multiplatform ist – da es hier unendliche Schleifen von bidirektionalen Bindings gibt.
Das Fundament von MVI sind die drei Komponenten State, Intent und Reducer. Als Ausgangspunkt beginnen wir beim State: Dabei handelt es sich um den aktuellen Zustand der UI – also alle Informationen, die man braucht, um einen Screen vollständig zu rendern, beispielsweise den Boolean, ob ein bestimmter Toggle gesetzt ist. Anders als bei der MVVM-Architektur, wo der State direkt geändert wird, ist der State bei MVI immutable und kann daher nicht direkt verändert werden. Stattdessen kommt der Intent ins Spiel. Er wird ausgeführt und triggert eine Aktion. Diese Aktion erschafft einen komplett neuen State, der deterministisch ist – das heißt: Bei gleichen Voraussetzungen ist der neue State immer gleich. Diese Aktion nennt man auch Reducer, weil Ereignisse auf einen Zustand reduziert werden. Bei seiner Implementierung verwendet man ausschließlich Funktionen, keine Klassen.
Executor, Store, Effect
Um es noch etwas cleaner zu machen, fügen wir noch Executor, Store und Effect ein. Diese befinden sich alle in der UI-Schicht. Aus Platzgründen gehen wir nicht auf tiefere Layer ein – diese können einfach von Clean Architecture übernommen werden.
Das zentrale Element ist der Store. Wir können ihn nicht ViewModel nennen, weil es den nur bei Android gibt – bei iOS wäre das Äquivalent z. B. ein ObservableObject. Der Name „Store“ kommt daher, dass hier alles enthalten ist, was wir brauchen. Dabei ist der Store selbst nur der Vermittler: Er nimmt den State und stellt diesen der View als Observer zur Verfügung – als StateFlow auf Android, als ObservableObject oder Publisher auf iOS (z. B. mit Combine).

Darüber hinaus implementiert der Store das Handling der Intents, die entweder vom System oder vom User kommen. Man kann Intents nicht nur auf User-Interaktionen verknüpfen, sondern auf alles, was den UI-State irgendwie ändert – also auch Systemereignisse etc. Diese Intents werden als sealed classes implementiert, um sie nachvollziehbar und überschaubar zu halten – und ein deterministisches Verhalten zu gewährleisten.
Dieser Intent wird dann mittels Reducer verarbeitet. Dabei ist zu beachten, dass der Reducer aus puren Funktionen besteht – was bedeutet: bei gleichem Input immer gleicher Output (Idempotenz/Determinismus). Die Aufgabe des Reducers ist es, aus dem alten State unter Hinzuziehung des Intent einen neuen State zu erschaffen.
Was fehlt noch? Richtig – Executor und Effect:
Es kann der Fall sein, dass wir etwas tun wollen, ohne den State der UI zu verändern. Wenn wir beispielsweise einen Toast (bzw. das iOS-Äquivalent) anzeigen wollen, ändert sich nichts am State – aber trotzdem passiert etwas. Diese Nebeneffekte, auch bekannt unter dem Namen „Label“ oder „Effect“, werden ebenfalls vom Executor bearbeitet, ändern aber – wie gesagt – den State nicht. Sie können als SharedFlow (Android) oder als Publisher (iOS) verwendet werden.
Die Aufgabe des Executors ist es, Intents zu verarbeiten, also alles, was nicht rein zustandsverändernd ist, sondern z. B. API-Aufrufe, Navigation, Logging etc.
Zusammengefasst:
Store, State, Intent, Reducer, Executor, Label
Diese sechs Komponenten sind in der UI-Schicht verortet. Es ist etwas mehr Boilerplate-Code – aber auf diese Weise wird der Code sauber gekapselt und die Logik ist wiederverwendbar und auf maximale Weise trennbar.
Für ein sehr einfaches Beispiel -> check out https://github.com/05dbec67c6/MVI-Example

Vorteile für iOS-/Swift-Entwickler
Eine solche Trennung gibt mehr Raum für die Business-Logik und drückt das Plattformfremde an den äußersten Rand. Swift-Entwickler:innen können sich bei der Entwicklung den Shared State über ObservableObject oder Combine nutzbar machen. Im Code wird die komplexe Logik reduziert. Alle Tools, die iOS-Entwickler:innen nutzen, sind weiterhin verwendbar – vor dem Hintergrund einer einheitlichen Backend-Logik. Der Nachteil ist allerdings, dass sie, wenn sie im Projekt bleiben wollen, sich mit Kotlin auseinandersetzen müssen, da die Codebase anders auf das Team aufgeteilt werden müsste. Auf der anderen Seite müssen sich Android-Entwickler:innen auf Swift einstellen.
Paradigmenwechsel in Crossplatformentwicklung
Cross-Plattform-Entwicklung dieser Art läutet nichts Geringeres als einen Paradigmenwechsel ein: Ausdifferenzierung statt Frontend/Backend – die Frontend-Teams werden noch einmal geteilt:
- In ein Team mit dem Schwerpunkt auf UI-Entwicklung, das sich oberflächlich mit allen beteiligten Sprachen auskennt, genug, um die Business-Logik anzubinden.
- Und daneben Kotlin-Experten, die sich auf das Business-Logik-Metier fokussieren.
MVI unterstützt diese Rollenverteilung optimal.
Für Unternehmen bringt das wesentliche Vorteile: weg von Plattform-Silos – wo zwei oder mehr Teams nebeneinander an der App entwickeln – hin zu funktionsübergreifenden Rollen.
Außerdem werden Wiederverwendung, Ownership und Skalierung erhöht.
Was heißt das für Unternehmen?
Vielleicht geht die App-Entwicklung langfristig dahin, dass jede/r Android-Entwickler:in auf Kotlin Multiplatform umstellt. Den offiziellen Support von KMP für Android2 dürfte Google weiter ausbauen. Für zukünftige Kunden in Unternehmen wäre dann gegen Aufpreis ein Hinzuschalten weiterer Plattformen (iOS, Backend, Desktop) möglich.
Fazit
Wir können festhalten, dass MVI ein strategischer Architekturansatz ist – und es sich nicht um ein bloßes Pattern handelt. Es ist unverzichtbar bei ernsthafter Nutzung von Kotlin Multiplatform, da es zentrale Qualitätsanforderungen an ein Projekt sicherstellt:
- Trennung der Plattformen
- Testbarkeit
- Skalierbarkeit
Und wer weiß – irgendwann gibt es keine Android- oder iOS-Entwickler:innen mehr, sondern nur noch Kotlin-Multiplatform-Entwickler:innen.
Und die Frage lautet nicht mer: Android oder iOS – oder Desktop – sondern: UI-Layer oder Business-Logik?
Es ist also aktuell ein guter Zeitpunkt, um über professionelle Unterstützung im Bereich Kotlin Mutliplatform nachzudenken. Ob Beratung, Architektur oder Umsetzung – moderne Cross-Platform-Lösungen profitieren von einem klaren technischen Fokus. Mit umfassender Projekterfahrung und einem spezialisiertem Team begleitet adesso Unternehmen dabei, Kotlin Multiplatform gezielt einzusetzen.



