Blog von Obi Madu
Zurück zu allen Artikeln
MobileSystem DesignInfrastructure

Wie Mobile Apps funktionieren

Die drei Wege, eine Mobile-App zu bauen, was bei jedem unter der Haube tatsächlich läuft, und das gemeinsame Architektur-Muster hinter React Native, Flutter und Tauri Mobile.

Wie Mobile Apps funktionieren

Wenn man sich in die Mobile-Entwicklung einarbeitet, wird die Zahl der Frameworks und Ansätze schnell verwirrend. Man hört von nativen Apps, React Native, Flutter, Expo, Capacitor, Cordova, Ionic, Tauri Mobile, und es ist nicht immer klar, was sie voneinander unterscheidet. Machen sie alle dasselbe auf verschiedene Weise, oder sind einige von ihnen im Kern unterschiedliche Architekturen?

Die Antwort ist beides. Es gibt drei Hauptwege, eine Mobile-App zu bauen, und die meisten Frameworks, die man hört, fallen in einen davon. Die Unterschiede liegen darin, was die UI rendert, was die Logik ausführt und wie die beiden miteinander kommunizieren.

Die drei Ansätze

Hier ist die Landschaft:

AnsatzUI wird gerendert mitLogik läuft inBeispiele
NativePlattform-UI-KomponentenPlattform-SpracheSwift/Kotlin-Apps
Web-WrapperWebView (Browser)JavaScriptCapacitor, Cordova, Ionic
Cross-PlatformVariiert (nativ, Canvas oder WebView)JS, Dart oder RustReact Native, Flutter, Tauri

Das Erste, was auffällt, ist, dass "Cross-Platform" nicht ein Ansatz ist. Es ist eine Kategorie, und innerhalb davon ist die UI-Rendering-Strategie für jedes Framework unterschiedlich. Daher kommt die meiste Verwirrung. Leute hören "Cross-Platform" und gehen davon aus, es bedeute eine Sache. Tut es nicht.

Gehen wir jeden durch.

Native Apps

Das ist die Grundlinie. Man schreibt eine iOS-App in Swift oder Objective-C, mit UIKit oder SwiftUI. Man schreibt eine Android-App in Kotlin oder Java, mit Android Views oder Jetpack Compose. Der Code kompiliert zum nativen Binär der Plattform, die UI sind die eigenen UI-Komponenten der Plattform, und die Logik läuft direkt auf der Plattform-Runtime.

iOS:    Swift/SwiftUI  →  kompiliert  →  UIKit/SwiftUI rendert auf den Bildschirm
Android: Kotlin/Compose →  kompiliert  →  Android Views/Compose rendert auf den Bildschirm

Der Vorteil ist voller Zugriff auf alles, was die Plattform bietet. Jede API, jede UI-Komponente, jedes plattformspezifische Feature, jede Performance-Optimierung. Es gibt keine Bridge, keine Übersetzungsschicht, keine Abstraktion. Der Code, den man schreibt, ist der Code, der läuft.

Der Preis ist, dass man die App zweimal schreibt. Eine iOS-App und eine Android-App sind zwei komplett separate Codebasen, in zwei verschiedenen Sprachen, mit zwei verschiedenen UI-Frameworks, zwei verschiedenen Build-Systemen und zwei verschiedenen Deployment-Targets. Wenn man ein Feature auf beiden Plattformen will, baut man es zweimal.

Daher kommen die anderen zwei Ansätze. Der Web-Wrapper und die Cross-Platform-Kategorie existieren beide, weil alles zweimal zu schreiben teuer ist.

Web-Wrapper

Der Web-Wrapper-Ansatz nimmt eine Web-App und packt sie in eine native Hülle. Capacitor (von Ionic) und Cordova sind die Haupt-Frameworks hier.

HTML/CSS/JS  →  WebView  →  native Hülle  →  OS

Die UI ist ein WebView. Das ist eine Browser-Engine, dieselbe, die das System nutzt, wenn man einen Link innerhalb einer App öffnet. Auf iOS ist es WKWebView, auf Android ist es der Android WebView. Das JavaScript läuft in diesem WebView, genauso wie es in einem Browser-Tab läuft. Die native Hülle darum liefert eine Bridge, sodass das JavaScript Plattform-APIs aufrufen, auf die Kamera zugreifen, das Dateisystem lesen, native Benachrichtigungen anzeigen und so weiter kann.

Wenn man die Tauri-Einführung gelesen hat, sollte diese Struktur vertraut aussehen. Tauri auf dem Desktop nutzt dasselbe Muster: Web-UI in einem nativen Fenster, mit einer Bridge zum OS. Der Unterschied ist das Backend. Capacitor und Cordova bridgen zur nativen Sprache der Plattform (Swift oder Kotlin), die pro Plattform geschrieben wird. Tauri bridgt zu Rust, das nativ für jedes Mobile-Target kompiliert wird.

Der Reiz des Web-Wrappers ist offensichtlich. Wenn man bereits eine Web-App hat, kann man sie als Mobile-App ausliefern, ohne die UI neu zu schreiben. Dasselbe HTML, CSS und JavaScript, das in einem Browser läuft, läuft im WebView. Man schreibt ein Frontend, und es funktioniert überall.

Der Kompromiss ist Performance und natives Gefühl. Ein WebView ist ein Browser. Es rendert HTML auf einen Bildschirm, auf dieselbe Weise wie ein Browser. Native UI-Komponenten sind schneller, flüssiger und fühlen sich nach der Plattform an, auf eine Weise, die ein WebView nicht immer erreicht. Scroll-Physik, Gesten-Handling, Übergänge und Input-Verhalten sind in einem WebView subtil anders als in einer nativen UI. Für manche Apps fällt das nicht ins Gewicht. Für andere ist es das ganze Spiel.

Cross-Platform-Frameworks

Hier wird es interessant, denn "Cross-Platform" bedeutet für verschiedene Frameworks unterschiedliche Dinge. Jedes wählt eine andere UI-Rendering-Strategie und einen anderen Bridge-Mechanismus.

React Native

React Native rendert nicht in einen WebView. Die React-Komponenten beschreiben eine native UI, und React Native erzeugt tatsächliche native iOS- und Android-Views. Ein View wird auf iOS zu einem UIView und auf Android zu einem android.view.View. Es gibt keinen Browser, kein HTML, kein DOM.

React-Komponenten  →  JSI-Bridge  →  native UI-Komponenten  →  Bildschirm

Die Logik läuft in JavaScript, auf einer JavaScript-Engine, die in die App eingebettet ist. Die Bridge zwischen JavaScript und nativ heißt JSI (JavaScript Interface). Sie erlaubt JavaScript, direkt in native C++-Objekte aufzurufen, was schneller ist als die ältere Bridge, die alles zu JSON serialisierte.

Wenn man den vollständigen Deep Dive zu React Native und Expo will, habe ich darüber hier geschrieben. Die kurze Version für diesen Post: Die UI ist nativ, die Logik ist JavaScript, und die Bridge ist JSI.

Flutter

Flutter geht einen anderen Weg für die UI. Es nutzt keine nativen UI-Komponenten und keinen WebView. Flutter hat seine eigene Rendering-Engine, Impeller genannt (früher Skia), die direkt auf einen Canvas zeichnet. Jeder Pixel auf dem Bildschirm wird von Flutters eigener Engine gemalt.

Flutter-Widgets  →  Dart-Runtime  →  Impeller rendert auf Canvas  →  Bildschirm

Die Logik läuft in Dart, das zu nativem ARM-Code für die Zielplattform kompiliert. Die Bridge zwischen Dart und Plattform-APIs heißt Platform Channels. Wenn Flutter eine Plattform-API aufrufen muss, etwa die Kamera oder das Dateisystem, sendet es eine Nachricht durch einen Platform Channel an nativen Code, der die Anfrage behandelt.

Der Reiz ist Konsistenz. Weil Flutter die eigene UI zeichnet, sieht und verhält sich eine Flutter-App auf iOS und Android identisch. Es gibt kein "das sieht aus wie eine Android-App auf iOS"-Problem, weil die UI keine Komponenten beider Plattformen nutzt. Der Kompromiss ist, dass eine Flutter-App nicht automatisch das Aussehen und Gefühl der Plattform übernimmt. Will man, dass es sich nativ anfühlt, muss man das selbst bauen.

Tauri Mobile

Tauri Mobile ist das neueste der drei und kombiniert den UI-Ansatz des Web-Wrappers mit einem Rust-Backend statt eines Backend in einer nativen Sprache.

HTML/CSS/JS  →  WebView  →  Tauri IPC  →  Rust  →  OS

Die UI ist ein WebView, genauso wie bei Capacitor und Cordova. Der Unterschied ist das Backend. Anstatt zu Swift- oder Kotlin-Code zu bridgen, der pro Plattform geschrieben wird, bridgt Tauri zu Rust, das nativ für jedes Mobile-Target kompiliert wird. Eine Rust-Codebasis handhabt die native Seite für sowohl iOS als auch Android.

Hier wird das One-Codebase-Muster nützlich. Dieselbe Interface-und-Implementierungs-Strategie, die es einer Tauri-Desktop-App ermöglicht, Code mit einem Web-Frontend zu teilen, gilt auch für Mobile. Die Service-Interfaces bleiben gleich, und man fügt eine Mobile-Implementierung neben der Web- und Desktop-Implementierung hinzu.

Das gemeinsame Muster

Hier ist der Teil, der für mich die ganze Landschaft zusammenbrachte. Alle drei Cross-Platform-Frameworks folgen derselben Architektur:

   UI
    |
  Bridge
    |
  Engine

Die UI ist die Präsentationsschicht. Die Bridge ist der Kommunikationsmechanismus. Die Engine ist die Runtime, die die eigentliche Arbeit erledigt. Die Technologie ändert sich, aber die Form bleibt gleich.

FrameworkUIBridgeEngine
React NativeNative KomponentenJSIJavaScript-Engine
FlutterImpeller-CanvasPlatform ChannelsDart-Runtime
Tauri MobileWebViewIPCRust

Sobald man dieses Muster sieht, hören die Unterschiede zwischen den Frameworks auf, verwirrend zu sein. Sie sind alle Variationen derselben Form. React Native wählt native UI und JSI. Flutter wählt den eigenen Canvas und Platform Channels. Tauri wählt einen WebView und IPC zu Rust. Die Wahl dreht sich darum, welche Kompromisse man für die UI-Schicht akzeptiert und welchen Bridge-Mechanismus man nutzen will, um die Plattform zu erreichen.

Wie man wählt

Die Frage ist nicht, welches Framework das Beste ist. Die Frage ist, welche Kompromisse zur eigenen App passen.

Native (Swift/Kotlin), wenn man vollen Plattform-Zugriff, maximale Performance oder eine UI braucht, die sich auf jeder Plattform komplett nativ anfühlt. Der Preis ist, zwei Codebasen zu pflegen.

React Native, wenn man native UI-Komponenten will, die von JavaScript angetrieben werden, und man bereits ein React-Team hat oder Logik mit einer React-Web-App teilen will. Die UI ist nativ, die Logik ist JavaScript, und man schreibt eine Codebase, die beide Plattformen ansteuert.

Flutter, wenn man pixelgenaue Konsistenz über Plattformen hinweg will und bereit ist, das native Aussehen und Gefühl selbst zu bauen. Die UI ist Flutters eigenes Rendering, die Logik ist Dart, und die App sieht auf iOS und Android standardmäßig identisch aus.

Tauri Mobile, wenn man bereits ein Web-Frontend und ein Rust-Backend hat, oder wenn man eine Tauri-Desktop-App auf Mobile erweitern will, ohne den Stack zu wechseln. Die UI ist ein WebView, das Backend ist Rust, und dieselbe Codebasis kann Desktop und Mobile ansteuern.

Web-Wrapper (Capacitor/Cordova), wenn man bereits eine Web-App hat und sie mit minimalen Änderungen auf Mobile ausliefern will, und man die WebView-Kompromisse bei Performance und nativem Gefühl akzeptieren kann.

Das größere Bild

Die meisten Cross-Platform-Frameworks und die meisten modernen App-Architekturen im Allgemeinen folgen dem UI-, Bridge-, Engine-Muster. Die UI ist, was der Nutzer sieht. Die Bridge ist, wie die UI mit der Engine spricht. Die Engine ist, was die Arbeit erledigt. Ob man eine Mobile-App, eine Desktop-App oder eine Web-App baut, diese Form taucht auf.

Die Technologie ändert sich. Die Architektur nicht. Sobald man die Form sehen kann, kann man jedes neue Framework evaluieren, indem man drei Fragen stellt: Was rendert die UI, was führt die Logik aus, und wie kommunizieren sie. Alles andere ist Detail.