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
Nächster Fall, gleiches Symptom: Simulator sauber, echtes iPhone tot nach sechs Sekunden. Diesmal stand im Backtrace tatsächlich 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/>Zwei Zeilen XML zwischen „läuft" und „startet gar nicht".
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. Was sie verbindet:
Die schnelle Prüfung ist nicht die echte Prüfung. Simulator ist nicht Gerät. Metadata ist nicht Plattform-Compile. Ein grüner Task ist nicht ein grüner Build. Jede dieser Abkürzungen ist bequem — und jede hat uns Stunden gekostet, in denen wir an der falschen Stelle gesucht haben.
Kotlin Multiplatform ist die Mühe wert; die Ersparnis über zwei Plattformen ist real. Aber wer damit ausliefert, braucht das echte Gerät im Prüfpfad — nicht am Ende, sondern von Anfang an.