Kotlin Multiplatform hält, was es verspricht: Eine Codebasis, zwei Plattformen. Unsere App Anirova besteht aus rund 215.000 Zeilen Kotlin und läuft daraus nativ auf iOS und Android.
Der Preis dafür sind Fehlerklassen, die es sonst nicht gibt. Die unangenehmsten haben alle dasselbe Muster: Der Simulator ist grün, das echte Gerät stirbt.Fünf davon, mit dem, was wir gebraucht haben, um sie zu finden.
1. Das Crash-Log lügt — im Backtrace
Eine App startete auf dem iPhone und war nach etwa sechs Sekunden weg. SIGABRT, also eine Kotlin-Exception — dachten wir. Stundenlang haben wir im App-Code gesucht.
Der Grund stand nicht im Backtrace, sondern im termination-Feld des Crash-Reports:
"namespace": "DYLD",
"indicator": "Library missing",
"reasons": ["Library not loaded: @rpath/App.debug.dylib",
"terminated at launch"]Kein App-Fehler, sondern ein Ladeproblem: Das Framework fehlte. Das Erkennungsmerkmal ist eindeutig, wenn man es einmal kennt — der Crash-Thread enthält nur dyld-Frames, keine einzige Zeile eigenen Codes, dazu „terminated at launch".
Bei einem iOS-Crash zuerst das termination-Feld lesen, nicht den Backtrace. Es sagt, ob überhaupt eigener Code lief.2. Ein fehlender Plist-Eintrag reicht
Wieder lief es im Simulator problemlos, und wieder war das iPhone nach sechs Sekunden weg. Diesmal stand im Backtrace immerhin Kotlin, aber kein eigener Code:PlistSanityCheck.
Compose Multiplatform prüft beim Start die Info.plist und wirft eine Exception, wenn ein bestimmter Schlüssel fehlt. Der Simulator überspringt diese Prüfung. Das Gerät nicht.
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>Fehlen diese zwei Zeilen XML, startet die App auf dem Gerät gar nicht erst.
3. Ein harmloser Parameter, der die App killt
Zum Speichern von Einstellungen bietet sich unter iOS NSUserDefaults an. Die Bibliothek erlaubt einen suiteName-Parameter — der sieht aus wie ein beliebiger Name für den eigenen Datenbereich:
// Crasht die App sofort beim Start
NSUserDefaultsSettings(NSUserDefaults(suiteName = "meine-app"))
// Richtig
NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults)suiteName ist ausschließlich für echte App-Groups gedacht. Mit einem beliebigen String liefert iOS null zurück, und Kotlin/Native wirft — unbehandelt, direkt beim Start.
Auf Android fällt das nicht auf. Im Simulator oft auch nicht. Nur auf dem Gerät.
4. „Grün" heißt nicht, dass es baut
Ein Prüflauf meldete Exit 0. Alles in Ordnung, dachten wir. Der Plattform-Build scheiterte acht Minuten später:
compileKotlinMetadataprüft nur den gemeinsamen Metadata-Layer.compileKotlinIosArm64und Co. sind strenger — sie fangen, was Metadata durchlässt.
Im konkreten Fall war es ein vollqualifiziert geschriebenes Icon ohne passenden Import. Metadata: grün. Plattform-Compile: unresolved reference.
Ein echtes „Baut die App?"-Gate ist immer das Plattform-Target, das zum Deploy-Ziel passt — nie Metadata.
Nachtrag aus derselben Sitzung, weil es genauso teuer war: Wer einen Build als Hintergrundprozess mit | grep filtert, bekommt den Exit-Code desgrep zurück, nicht den des Builds. Der Task meldet „completed", obwohl der Build gescheitert ist. Bei Prüf-Gates immer den echten Build-Status abfragen.
5. Ein Status, auf den man nicht warten darf
Beim Deployment aufs Gerät zeigt xcrun devicectl list devices ein per WLAN gekoppeltes iPhone häufig als unavailable — obwohl es erreichbar ist. Eine Schleife, die auf „available" wartet, bevor sie installiert, läuft endlos.
# Nicht auf den Status warten — direkt installieren
xcrun devicectl device install app --device <UDID> <App> --timeout 60Der Listen-Status ist eine Momentaufnahme des Developer-Tunnels, kein Erreichbarkeits-Indikator. devicectl baut den Tunnel beim Installieren selbst auf. Erfolg erkennt man an App installed: in der Ausgabe — nicht am vorherigen Status.
Das Muster hinter allen fünf
Vier der fünf Fälle sind reine Geräte-Probleme: Simulator grün, Gerät tot. Der fünfte ist ein grünes Gate, das nichts geprüft hat. Und in allen fünf hat mich dieselbe Abkürzung reingelegt — ich hab der schnellen Prüfung geglaubt statt der echten. Der Simulator verhält sich anders als das Gerät, ein Metadata-Compile anders als der Plattform-Compile, und ein grüner Task sagt nichts über den fertigen Build. Bequem sind diese Abkürzungen alle. Teuer waren sie auch: jede hat mich Stunden an der falschen Stelle suchen lassen.
Kotlin Multiplatform ist die Mühe trotzdem wert, die Ersparnis über zwei Plattformen ist real. Nur gehört das echte Gerät von Anfang an in den Prüfpfad, nicht erst am Ende. Sonst findet man die Hälfte dieser Fehler am Tag vor dem Release.