arrow arrow--cut calendar callback check chevron chevron--large cross cross--large download filter kununu linkedin magnifier mail marker media-audio media-blog media-video menu minus Flieger phone play plus quote share youtube

Migration eines Mobile-Projekts auf Kotlin Multiplatform

Chancen und Herausforderungen der Zusammenführung getrennter Codebasen für Android und iOS

Im Rahmen eines sechswöchigen Experiments haben wir im Februar 2025 evaluiert, inwiefern eine bestehende, umfangreiche und modularisierte Android-App auf Kotlin Multiplatform (KMP) migriert werden kann. Die Vorstellung, eine gemeinsame Codebasis zu haben, mit der wir unsere App sowohl für Android- als auch iOS-Geräte weiterentwickeln und pflegen können, ist für uns sehr verlockend. Hybride Ansätze versprechen seit langer Zeit, dass Apps mit weniger Aufwand entwickelt und betrieben werden können und dass die Funktionalität und das Corporate Design konsistent auf beiden Plattformen umgesetzt werden kann.

Foto von Bastian Demuth
Bastian Demuth

Android Developer

Die in diesem Beitrag beleuchtete KMP-Technologie hat gegenüber anderen hybriden Ansätzen den entscheidenden Vorteil, dass die Programmiersprache und viele der nötigen Tools für Android Entwickler*innen unverändert beibehalten werden können, so dass sich diese mit dem Framework sehr schnell anfreunden werden.
Das Ziel des Experiments war es, die konkreten Herausforderungen einer plattformübergreifenden Entwicklung auf Basis des aktuellen Technologiestands zu identifizieren. Dabei haben wir als zusätzliche Plattform “nur” iOS untersucht, der Einsatz von KMP im Web oder zur Erstellung von Desktop Clients ist für uns aktuell nicht relevant.

Im Folgenden berichten wir im Detail über unsere Erkenntnisse, die Transformation unserer Module und die noch bestehenden Limitierungen im Vergleich zu nativen Implementierungen – speziell mit Blick auf iOS.

Multiplattform-Anwendungen

Erwartungen an KMP und reale Vorteile

Die Vorteile einer gemeinsamen Codebasis liegen auf der Hand und haben in der Vergangenheit immer wieder zum Einsatz hybrider Technologien geführt, die in der Praxis allerdings die Erwartungen oft nicht erfüllen konnten.

Diese Vorteile sind

  • Einheitlichkeit im Verhalten und Design
  • Kein Feature Gap zwischen den Plattformen, neue Features können nahezu zeitgleich veröffentlicht werden
  • Enge Zusammen­arbeit der Entwickler*innen und Tester*innen, es entstehen keine “Plattform-Lager”
  • Geringerer Aufwand, da nicht alles “doppelt” entwickelt werden muss
  • Compose Multiplatform (CMP) ermöglicht auch die plattformübergreifende Deklaration von grafischen Oberflächen

Durch den Einsatz von KMP für die Anbindung der App an unsere Server und die Implementierung der Geschäftslogik der App und CMP für die grafischen Oberflächen wird eine übergreifende Entwicklung aller Software-Schichten der App ermöglicht, es müssen damit keine nativen iOS-Views mehr definiert werden. In der Zeit des sechswöchigen Experiments haben wir festgestellt, dass dies schon recht gut funktioniert und wir außerdem als Entwickler*innen stärker zusammengewachsen sind, die “Plattform-Lager” begannen schnell, sich aufzulösen und es wurde viel Wissen in Bezug auf die jeweils andere Plattform transferiert.

Auch der erwartete Effizienzgewinn wurde in einem gewissen Umfang beobachtet. Um wieviel geringer der Aufwand der hybriden Entwicklung tatsächlich in der Praxis ist, hängt allerdings von verschiedenen Faktoren ab. Man darf hierbei nicht vergessen, dass mobile Apps auf der vollen Bandbreite der unterstützten Geräte getestet und oft auch für bestimmte Geräte angepasst oder optimiert werden müssen. Dies ist auch in der hybriden Entwicklung der Fall und kostet eine Menge Zeit. Außerdem unterscheiden sich die Plattformen in einigen wesentlichen Punkten, so dass es auch in der hybriden Entwicklung immer plattformspezifische Anteile geben wird. Dies gilt besonders, wenn es um hardwarenahe Features geht, oder man die unterschiedlichen Konventionen der Plattformen bei grafischen Nutzeroberflächen und Navigation respektieren möchte.

Integration von KMP-Code in iOS-App

Herausforderungen bei der Migration zu KMP

Eine Kurzversion unseres KMP-Readiness Checks gibt es auch in unserem Referenzbereich:

Arbeiten mit zwei Entwicklungsumgebungen

Die offensichtlichste Veränderung in der Arbeitsweise ist zum jetzigen Zeitpunkt, dass man als Entwickler*in zwei Entwicklungsumgebungen (IDEs) verwenden muss, um zügig arbeiten zu können. Während man die Kotlin-Anteile des Codes üblicherweise in Android Studio entwickelt, wird für die iOS-Entwicklung, für das schnelle Deployment der App auf den Apple Endgeräten und für das Debugging Xcode verwendet.

Das impliziert, dass man sich mit beiden IDEs gut auskennen muss, um effizient zu arbeiten. In der Praxis haben wir festgestellt, dass es sowohl bei Android als auch bei iOS-Entwickler*innen Vorbehalte gegenüber der jeweils anderen IDE gibt, die teilweise auch ihre Berechtigung haben. In unserem komplexen Projekt haben wir immer wieder Abstürze von Xcode erlebt, und da wir eine sehr aktuelle Android Studio Version (eine Canary Version) verwendet haben, kam es auch dort immer wieder zu Problemen. Außerdem gibt es zunächst einmal unterschiedliche Tastenbelegungen, und viele weitere Unterschiede beispielsweise beim Bauen und Debuggen der App, aber auch beim Logging und der Navigation im Source Code.

Um den gemeinsam genutzten KMP-Code in unsere bestehende iOS-App einzubinden, haben wir über einen speziellen gradle Task ein XCFramework-Verzeichnis erstellt, das dann wiederum über Xcode in das iOS-Projekt eingebunden werden musste, um die iOS-App gegen die erstellten Schnittstellen zu kompilieren und zu bauen.

Objective-C Schnittstellen

Aktuell werden aus dem Kotlin Code nur Objective-C-Header erstellt – eine Unterstützung für Swift-Schnittstellen ist zwar geplant, aber stand zum Zeitpunkt des Experiments noch nicht zur Verfügung. Diese Tatsache hat unseren iOS-Entwickler*innen nicht gefallen, da sie schon seit geraumer Zeit damit beschäftigt sind, alle Objective-C-Anteile aus dem Source Code der App zu entfernen. Das Zusammenspiel aus Swift und Objective-C ist deutlich weniger reibungslos als das zwischen Kotlin und Java, es ergeben sich einige praktische Probleme:

  • Objective-C unterstützt keine Generics – hierdurch verliert man die eigentlich aus Kotlin und Swift gewohnte Typsicherheit, was zu Crashes zur Laufzeit führen kann, wenn sich die Schnittstelle ändert und der Swift Code nicht angepasst wird.
  • Der Swift Compiler kann nicht beurteilen, ob Werte, die über die Objective-C-Schnittstelle weitergereicht werden, null sein, also fehlen können; auch dies kann zu unvorhergesehenen Laufzeit-Crashes führen.
  • Objective-C unterstützt keine optionalen Parameter. Die in Kotlin definierten Default-Werte für solche Parameter gehen verloren, wodurch immer alle Parameter übergeben werden müssen. Das macht die Schnittstelle unhandlicher.
  • Primitive Kotlin-Datentypen werden in Objective-C auf Referenztypen abgebildet und alle numerischen Parameter werden durch NSNumber repräsentiert, wodurch zusätzlicher Mapping-Aufwand entsteht und die Typsicherheit weiter leidet.


Außerdem konnten die lang laufenden suspend functions, die wir in unserem Kotlin Code verwenden, nicht gut in dem bestehenden Swift Code aufgerufen werden. Hier erzeugt der o.g. gradle Task eine Callback basierte Schnittstelle, mit der man die Vorteile der Verwendung von Coroutines verliert – namentlich die Vermeidung von verschachtelten Callbacks und den gezielten Abbruch der Ausführung, falls dieser erforderlich ist.

Einige der Schnittstellenprobleme konnten durch das sogenannte SKIE Plugin (ein Third-Party-Plugin für gradle) gelöst oder abgemildert werden:

  • Coroutines müssen nicht auf dem Main Thread gestartet werden und können wie normale Swift Async Functions verwendet werden, allerdings können sie vom Aufrufer immer noch nicht abgebrochen werden.
  • Flows lassen sich damit sinnvoll konsumieren bzw. beobachten.
  • Besserer Support für Sealed Classes und Enums
  • Default Parameter werden abgebildet und machen die Funktionsaufrufe schlanker

Diese Verbesserungen sind sehr wertvoll, allerdings ermöglicht das Plugin noch nicht die generelle Erzeugung von Swift-Schnittstellen, sondern erzeugt nur eine Wrapper-Schicht um die generierten Objective-C Header, so dass der Bruch zwischen Swift Code und Objective-C nicht ganz aufgelöst wird. Außerdem werden nicht alle der oben genannten Probleme adressiert und die Weiterentwicklung eines solchen Drittanbieter-Plugins in der Zukunft ist nicht sichergestellt.

Debugging

Debugging von laufendem Kotlin Code auf einem iPhone war in Xcode möglich, hier musste jedoch noch ein zusätzliches Third-Party-Plugin installiert und der Source Code in einem weiteren Schritt eingebunden werden. Außerdem schien der Debugger nicht zuverlässig an allen gesetzten Breakpoints anzuhalten, dies war insbesondere in CMP Code zu beobachten. Unsere Android-Entwickler*innen empfanden es als schwierig, den Inhalt der Variablen zur Laufzeit zu untersuchen, was das Debugging wesentlich erschwerte.

Insgesamt ist die Arbeitsweise mit zwei IDEs deutlich komplexer und fühlt sich weniger flüssig an. Entwickler*innen müssen regelmäßig länger als in einem nativen Projekt warten, bis sie die App nach einer Codeänderung auf einem Endgerät installiert haben. Debugging ist knifflig, insbesondere an den Schnittstellen zwischen Swift und Kotlin unter iOS.

Diese Probleme sind bei JetBrains (den Entwickler*innen hinter Kotlin und der IntelliJ IDE, auf der Android Studio basiert) bekannt und es wird scheinbar intensiv an einer besseren Unterstützung für Swift und iOS-Geräte direkt in Android Studio gearbeitet. Wann diese Verbesserungen genau zu erwarten sind und welchen Umfang sie jeweils haben werden, ist aktuell noch unklar.

Zu Kotlin Multiplatform haben wir noch mehr zu erzählen! Interesse? Dann geht's hier zu einer Podcastfolge und zu einem Blogbeitrag zum Thema:

Fehlende KMP-Unterstützung bei verbreiteten Packages und Bibliotheken

Ein zentrales Ergebnis unseres Experiments war, dass zahlreiche Klassen aus dem Java/Android-Ökosystem oder populären Third-Party-Bibliotheken in einem KMP-Kontext nicht oder nur eingeschränkt verwendbar sind:

  • java.time vs. kotlinx.datetime:
    In unserer App gibt es diverse Stellen, an denen wir mit Kalenderdaten und Uhrzeiten rechnen müssen, wofür wir die bekannten Klassen aus dem java.time Package einsetzen. Diese sind unter KMP nicht verfügbar. Als Ersatz steht hier die kotlinx.datetime Library zur Verfügung, welche die meisten grundlegenden Funktionen von java.time anbietet. Allerdings fehlen einige wichtige Klassen und Funktionen wie beispielsweise ZonedDateTime. Dies führt zu einem erheblichen Refactoring-Aufwand, der nicht nur fehleranfällig sein kann, sondern auch den Migrationsaufwand deutlich erhöht.
  • java.util.concurrent:
    Für viele der Funktionen aus java.util.concurrent existiert aktuell nur ein teilweiser oder gar kein adäquater Ersatz. Dies betrifft vor allem die beliebten Collections, die dort thread-safe implementiert wurden und die Atomic* Klassen (z.B. AtomicReference, AtomicBoolean, AtomicInteger, usw.). Für diese Klassen muss man jeweils passenden Ersatz finden, was wiederum längere Refactorings nach sich ziehen kann.
  • Dependency Injection:
    Dagger/Hilt ist im KMP-Umfeld aktuell nicht nutzbar, da die Library Java Code generiert. Laut Aussage der Dagger Entwickler*innen wird dies gerade durch Umstellung auf XPoet geändert, allerdings gibt es hierfür noch keinen konkreten Zeitplan. Es empfiehlt sich hier, auf Frameworks wie Koin oder Kotlin Inject umzusteigen – was in jedem Fall einen großen Umstellungs- und Anpassungsaufwand bedeutet.
  • Retrofit & Reflection:
    Retrofit, das stark auf Reflection basiert, ist in KMP nicht einsetzbar. Unsere Lösung war der Umstieg auf Ktor. In unserem Projekt verwenden wir Interceptoren in der Okhttp Schicht, um die ausgehenden Requests um bestimmte HTTP-Header anzureichern. Da Ktor out-of-the-box keine Interceptoren unterstützt, wir diesen Code aber gerne für beide Plattformen teilen wollten, mussten wir einen eigenen Interceptor-Mechanismus durch die Entwicklung eines Ktor-Plugins etablieren.
  • android.graphics.Bitmap:
    Im Design System unserer App gibt es Komponenten, bei denen Texte in verschiedener Schriftgröße an der oberen Kante bündig angeordnet werden müssen. Hierfür gibt es im klassischen Android View System und auch in JJetpack Compose keinen direkten Support (das System fügt hier immer ein gewisses Padding hinzu, dessen Größe abhängig von der Schriftgröße ist und das mit der globalen Änderung der Schriftgröße in den Geräteinstellungen nicht-linear skaliert). Um diese Komponenten dennoch umsetzen zu können, haben wir eine eigene Lösung entwickelt, die in einem Compose View unter der Haube den darzustellenden Text in eine Bitmap rendert und das unerwünschte Padding dann oben aus der Bitmap entfernt. Da die Klasse android.graphics.Bitmap in KMP nicht einsetzbar ist, mussten wir unsere Lösung auf die Klasse ImageBitmap umstellen, die in Compose verfügbar ist, was etwas Aufwand bedeutet hat. Wir haben an dieser Stelle auch versucht, ein Interface für die Komponente zu definieren und hierfür eine native Android-Implementierung und eine native iOS-Implementierung zur Verfügung zu stellen. Hierfür wollten wir einen Swift UI View in die bestehende Jetpack Compose View Struktur integrieren, wasmöglich ist, aber leider an der Tatsache gescheitert ist, dass die Größe des Swift UI Views noch vor der Layoutphase bekannt sein muss, was bei Texten im Allgemeinen nicht der Fall ist. Außerdem gibt es bei der Einbindung nativer Swift UI Views aktuell noch diverse Accessibility-Probleme.
  • Testbibliotheken und Mocking:
    Auch im Bereich der automatisierten Tests (unit tests und instrumentierte UI-Tests) traten gravierende Einschränkungen auf:

    Mockito und MockK waren zum Zeitpunkt des Experiments nicht auf anderen durch KMP unterstützten Plattformen außer Android einsetzbar. Unserer Einschätzung nach wird es auch in naher Zukunft nicht möglich sein, diese Libraries ohne größeren Verlust von Funktionen oder deutliche Änderungen der Schnittstellen KMP fähig zu machen. Die folgenden Alternativen wurden explizit für den Einsatz unter KMP entwickelt, bieten aber nur einen stark eingeschränkten Funktionsumfang:

    • MocKMP kann ausschließlich Interfaces mocken.
    • Mockative erfordert im Produktivcode eigene @Mockable-Annotationen.
    • Mokkery hat Probleme mit finalen Properties in Klassen.

    Man kann nun zu der Entscheidung gelangen, plattformspezifische automatisierte Tests zu entwickeln und den Produktivcode darüber abzusichern, aber dies kann zu erheblichen Duplikationen im Testcode führen oder man verliert die Sicherheit, dass der Produktivcode auch tatsächlich auf allen unterstützten Plattformen fehlerfrei läuft. Letzteres ist aus unserer Sicht besonders problematisch, da wir zur Laufzeit auf iOS-Geräten Crashes beobachtet haben, die mit demselben Code unter Android nicht auftraten. Im konkreten Fall haben wir über einen AnnotatedString in einem Compose View eine Textgröße als relative Größe mit der Einheit em definiert. Dies funktionierte in Android problemlos und führte auf iOS zum Laufzeit-Crash ohne jegliche Vorwarnung zur Compile Zeit.

Diese Erkenntnisse zeigen, dass die Umstellung großer Projekte auf KMP oftmals den Einsatz neuerer Libraries erforderlich macht, was wiederum große Aufwände nach sich ziehen und einige Nachteile gegenüber den bisherigen Lösungen mit sich bringen kann. Außerdem kann man sich während der Entwicklung nicht sicher sein, ob der Code auf beiden Plattformen korrekt laufen wird. Hier hilft eine große Testabdeckung durch automatisierte UI-Tests weiter, was aber zusätzlichen Aufwand bedeutet. Hat man in seinem Projekt weder Zeit für solche Tests noch umfangreiche Erfahrungen mit ihren speziellen Herausforderungen, ist das Risiko von Fehlern, die erst zur Laufzeit auffallen, sehr groß.

Transformation klassischer Android-Module zu KMP-Modulen

Ein weiteres zentrales Thema im Experiment war die Umstrukturierung der Module:

  • Ordnerstruktur:
    Die Struktur eines klassischen Android-Moduls weicht signifikant von der eines KMP-fähigen Moduls ab. Das bedeutet, dass ein Refactoring der gesamten Projektstruktur nötig wird, um die neuen Anforderungen zu erfüllen. Diese Art von Refactoring kann zu erheblichen Merge-Konflikten führen, wenn neben der Umstrukturierung parallel weiter an Features für die App gearbeitet wird.
Vergleich von Ordnerstrukturen
  • Flavors und White-Labeling:
    In unserem Projekt werden unterschiedliche App-Varianten, die verschiedenen Design-Systemen folgen und jeweils einen leicht unterschiedlichen Funktionsumfang haben, über Flavors abgebildet. Da es in KMP keine Flavors mehr gibt, erfordert der White-Labeling-Ansatz eine grundlegende Überarbeitung. Neue Theming-Mechanismen müssen entwickelt werden, um die verschiedenen Varianten weiterhin flexibel und wartbar zu gestalten. Die Unterschiede im Funktionsumfang können nicht mehr durch unterschiedliche Implementierungen von Klassen oder Funktionen pro Flavor abgebildet werden. Stattdessen haben wir uns dazu entschieden, diese Unterschiede durch verschiedene Implementierungen von Interfaces zu ermöglichen, die über Dependency Injection für die jeweilige App-Variante definiert werden können.

Unausgereifte Implementierungen und plattformspezifische Herausforderungen

Beim Einsatz von KMP und CMP auf iOS-Geräten gibt es noch Bereiche, die nicht ganz ausgereift scheinen:

  • Unvorhergesehene Laufzeit-Crashes:
    Wie bereits erwähnt, führt die Definition von Schriftgrößen in em in AnnotatedStrings auf iOS zu Abstürzen, obwohl der gleiche Ansatz in Android funktioniert und in Android Studio keinerlei Warnungen oder Hinweise zu sehen sind. Solche Inkonsistenzen erfordern zusätzliche Tests und Anpassungen im plattformübergreifenden Betrieb und sind aus unserer Sicht hochproblematisch.
  • Barrierefreiheit:
    Während Talkback unter Android die UI-Elemente in der erwarteten Reihenfolge vorliest, gibt es bei VoiceOver in iOS teilweise Abweichungen. Dies kann negative Auswirkungen auf die Benutzererfahrung von Menschen mit besonderen Bedürfnissen haben. Diese Probleme waren in der letzten stabilen Compose-Version zum Zeitpunkt des Experiments (1.7.8) noch enthalten, waren dann aber in der neuesten instabilen Version (1.8.0-alpha08) schon behoben. Hieraus schließen wir, dass im Januar 2025 noch am KMP-Support für grundlegende Accessibility-Features in Jetpack Compose gearbeitet wurde.
  • Einbinden nativer Swift-Views:
    Wie bereits kurz erwähnt, ist die Integration einzelner nativer Swift-Views in Compose Multiplatform (CMP) View-Hierarchien zwar technisch möglich, erweist sich jedoch in der Praxis als unpraktikabel. Gründe hierfür sind unter anderem, dass die Größe der Views vor dem eigentlichen Layouting bekannt sein muss und die enthaltenen Texte von VoiceOver nicht korrekt behandelt werden. Durch diese Probleme fällt ein wichtiger Baustein zur Erarbeitung plattformspezifischer Workarounds weg, was in der Praxis bedeutet, dass man Probleme, die man mit CMP-Bordmitteln nicht lösen kann, evtl. unter iOS gar nicht in den Griff bekommt, während für Android noch weitere Möglichkeiten für Workarounds zur Verfügung stehen.
  • Ressourcenmanagement:
    • Ressourcen wie Strings, Drawables und Fonts mussten in unserem Multi-Module-gradle-Projekt während des Build-Prozesses „händisch“ via Custom gradle Task in einen einzelnen Zielordner kopiert werden, um in Xcode gefunden und korrekt in die iOS-App integriert zu werden. Dies birgt die Gefahr, dass Ressourcen versehentlich überschrieben werden können, was erst zur Laufzeit auffallen würde.
    • Außerdem mussten diese Ressourcen zusätzlich zum generierten Kompilat manuell in das iOS-Projekt eingebunden werden.
    • Ein weiteres Problem ist, dass Ressourcen in unserer aktuellen App-Architektur noch in den ViewModels referenziert werden. Dort werden häufig Texte definiert, die aus String-Ressourcen mit dynamischen Parametern zusammengesetzt werden. Da Ressourcen aktuell nicht generell in KMP-Code, sondern nur in CMP-Views verfügbar sind, muss hier ein weiteres Refactoring stattfinden und es wird etwas zusätzliche Präsentations-Logik von dem ViewModel in den View verlagert.

Nachteile gegenüber nativer Swift-Implementierung

Unser Experiment hat zudem gezeigt, dass die plattformübergreifende Lösung gegenüber einer rein nativen iOS-Implementierung einige wesentliche Nachteile mit sich bringt:

  • Linting:
    Beim Einsatz des SKIE-Plugins ist eine erhebliche Anpassung der genutzten SwiftLint-Settings notwendig. Dies kann zu zusätzlichem Konfigurationsaufwand und potenziellen Inkonsistenzen in der Codequalität führen.
  • Type-Mapping:
    Es entsteht ein erheblicher Overhead auf iOS-Seite durch das Bridging von simplen Datentypen, wie beispielsweise der Umwandlung von Int zu NSNumber.
  • Debugging:
    Breakpoints im Kotlin Code sind nur mit einem speziellen Xcode-Kotlin-Plugin möglich. Dieses hält jedoch nicht zuverlässig bei allen Breakpoints an, was zu Schwierigkeiten bei der Fehlersuche führt – insbesondere in Compose Multiplatform Views, wo eine weiterführende Detaileinsicht oft nicht möglich ist.
  • Artefakt-Größe:
    Die resultierende ipa-Datei ist etwa 42% größer als bei einer nativen Swift-Implementierung. Dieser Overhead kann sowohl Speicherplatz als auch die Downloadzeiten der App negativ beeinflussen.
  • Crash-Analyse:
    Die Analyse von Stack Traces gestaltet sich erheblich schwieriger, da oft ein Bruch zwischen verschiedenen IDEs entsteht. Wir haben als Teil des Experimentes absichtlich in unserem Kotlin Code Exceptions geworfen, um zu bewerten, ob man die Ursachen von Laufzeit-Crashes in Kotlin Code unter iOS anhand von Crash-Reports finden kann. Manche Crashes, verursacht etwa durch OutOfMemoryErrors, wurden iOS-seitig vom Tooling nicht zuverlässig erfasst und blieben daher vor den Entwickler*innen verborgen. 

Diese Nachteile in Kombination mit der Notwendigkeit, Kotlin zu lernen und zwei IDEs parallel zu verwenden, führten in ihrer Gesamtheit zu erheblichen Widerständen gegenüber der Technologie bei unseren iOS-Entwickler*innen. Die entstehenden Konfliktpotentiale bei der Umstellung nativer Plattform-Teams auf plattformübergreifend arbeitende Teams sollte man nicht unterschätzen!

Evaluation möglicher Migrationsstrategien

Wir hatten ursprünglich die Vorstellung, dass wir unsere Android- und iOS-Projekte schrittweise zu KMP migrieren und somit schließlich zusammenführen könnten. Genauer gesagt, wollten wir schrittweise einzelne Module der Android App KMP-fähig machen und diese dann in die iOS-App einbinden, um nativen Swift Code abzulösen. Wir mussten jedoch im Laufe des Experiments feststellen, dass diese Vorgehensweise für unser komplexes und über die Zeit gewachsenes Projekt nicht sinnvoll ist. 

Dies ist sowohl den vielen kleinen Unterschieden der beiden nativen Codebasen geschuldet als auch den praktischen Problemen, die sich durch die generierten Objective-C-Header ergeben. Die Aufwände, die bei der Integration eines neuen KMP-Moduls entstehen, sind teilweise sehr hoch und die angesprochenen Schnittstellenprobleme treten dann an vielen Stellen in der App auf. Wir haben auch die Befürchtung, dass Crashes und Bugs, die nahe an den Schnittstellen zwischen Swift und Kotlin Code auftreten, schwer zu analysieren und lösen sein werden.

Wir präferieren eine alternative Vorgehensweise, die darin besteht, den aktuellen Android Code sukzessive KMP-fähig zu machen, ohne den entstehenden Code schon in einer iOS-App zu veröffentlichen. Auch dabei müssten bestimmte hardwarenahe Anteile plattformspezifisch in Kotlin und Swift implementiert werden. Erst wenn der Code vollständig multiplattform-fähig ist, kann daraus eine funktionierende App für iOS generiert werden, die dann über den Apple Store ausgerollt wird und somit die “alte” iOS-App ablöst. Diese Vorgehensweise hat den großen Nachteil, dass die iOS-spezifischen Anteile zwar stetig mitentwickelt werden, aber nicht manuell auf echten Devices getestet werden können, bis die KMP-basierte iOS-App verfügbar ist. Die entscheidenden Vorteile wären der vielfach geringere Aufwand bei der Migration und die Vermeidung einer ablehnenden Grundhaltung gegenüber der Technologie, die mit großer Wahrscheinlichkeit allein durch die vielen Schnittstellenprobleme entstehen würde. 

Fazit

Das sechswöchige Experiment zur Evaluierung von Kotlin Multiplatform anhand einer bestehenden Android-App hat gezeigt, dass die Migration in eine plattformübergreifende Architektur große Vorteile verspricht. Wir haben den Eindruck gewonnen, dass wir einen Großteil des Codes inklusive der grafischen Benutzeroberfläche in beiden Plattformen teilen könnten und nur ein kleiner Rest plattformspezifisch bleiben würde. 

Allerdings ist die Migration auch mit erheblichen Herausforderungen verbunden. Während das Teilen von Geschäftslogik auf mehreren Plattformen zu einheitlicherem Verhalten führen wird, sind wir noch nicht sicher, wie groß die erhoffte Steigerung der Effizienz in der Entwicklung wirklich ausfällt. Die fehlende Unterstützung zahlreicher Android-spezifischer Klassen und beliebter Drittanbieter-Bibliotheken bedeutet auch bei Neuentwicklungen in Zukunft einen erheblichen Mehraufwand. Außerdem muss die entstehende App auch weiterhin auf beiden Plattformen gründlich getestet werden, was den Effizienzgewinn weiter einschränkt.

Der initiale Aufwand bei der Umstellung der Module der App ist größer, als ursprünglich erwartet. Er wird in unserem Fall von den folgenden Aufgaben dominiert:

  • Refactoring-Aufwand bei zeitbezogenen Operationen, Dependency Injection und Rückbau von weiteren Android-spezifischen Lösungen im bestehenden Code
  • Notwendige Umstrukturierungen der Projektarchitektur und Ordnerstrukturen
  • iOS-spezifische Lösungen für Funktionen, die nicht in KMP abgebildet werden können
  • Zusätzliche Herausforderungen beim Testing und Debugging und in der Analyse von Crashes

Android-Entwickler*innen, die an eine Zukunft mit KMP denken, sollten sich bewusst sein, dass trotz der verlockenden Möglichkeit, Code zu teilen, ein umfassendes technisches Verständnis beider Welten (Android und iOS) sowie ein erheblicher Aufwand in der Anpassung an bzw. Optimierung für beide Plattformen nötig sind.

Aktuell ist das Tooling für den Einsatz auf iOS-Geräten noch nicht wirklich ausgereift und wir sind noch nicht sicher, ob die Technologie in Zukunft auf breiter Basis eingesetzt werden wird. Die Firma JetBrains ist jedoch dabei, die Unterstützung von KMP direkt in Android Studio weiter auszubauen, so dass die Entwicklung in Swift, das Deployment auf iOS-Geräten und das Debugging der laufenden iOS-App bald möglich sein sollten. Außerdem sollen in Zukunft Swift Interfaces statt Objective-C Header generiert werden, wodurch weitere Hürden abgebaut werden. Die wachsende Community-Adoption deutet darauf hin, dass KMP das Potenzial hat, sich als führende Cross-Platform-Lösung zu etablieren.

Wir konnten uns aktuell noch nicht dazu durchringen, unser Projekt auf KMP umzustellen. Bei einem kleineren oder neu startenden Projekt würden wir KMP aber ohne zu zögern einsetzen. Wegen der schnellen Weiterentwicklung der Technologie haben wir uns vorgenommen, die Situation in einigen Monaten neu einzuschätzen.

Mit diesem Beitrag hoffen wir, einen praxisnahen Einblick in die Herausforderungen und Chancen der Migration zu Kotlin Multiplatform zu geben und damit anderen Projekten bei der Entscheidungsfindung zu helfen.