Choosing a mobile technology stack is one of the most consequential architectural decisions. Each approach has concrete strengths and weaknesses that show up in specific scenarios.
Comparison Matrix
| Dimension | Native (Kotlin/Swift) | Flutter | React Native |
|---|---|---|---|
| Performance | Highest | Very high | High (JSI bridge) |
| UI fidelity | Perfect — platform controls | Custom engine (Skia/Impeller) | Mostly platform controls |
| Code sharing | None (two codebases) | ~70–80% | ~70–80% |
| OTA updates | No | No (Dart VM only) | Yes (CodePush) |
| Ecosystem | Most mature | Growing fast | Large (JS ecosystem) |
| Hire | Hardest (two teams) | Moderate | Easiest (JS devs) |
| Bundle size | Smallest | ~5–10MB overhead | ~3–8MB overhead |
| Camera/Bluetooth | Best | Needs plugins | Needs bridges |
| Animations | Best | Excellent | Good (can stutter) |
When Native Wins
- Games, AR, video editing — GPU-intensive features that need metal/Vulkan directly
- Deep OS integration — Widgets, accessibility services, VPN, NFC payment
- Maximum performance — Latency-sensitive features like real-time audio
- Large existing teams — Splitting into Flutter/RN has a transition cost
- Platform UX parity matters deeply — Material 3 evolves faster than Flutter wraps it
// Native: Custom CameraX pipeline that Flutter can't easily match
val cameraProvider = ProcessCameraProvider.getInstance(this).await()
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build()
imageAnalysis.setAnalyzer(cameraExecutor, ::processFrame)
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis)
When Flutter Wins
- New product with limited mobile budget — one team ships both platforms
- Custom design system — Flutter renders its own pixels; your design doesn't depend on platform conventions
- Games with custom UI — Flutter's rendering model suits highly interactive, animated UIs
- Internal tools — consistency matters more than platform-native feel
// Flutter: Pixel-perfect custom UI that looks identical on iOS and Android
class ArticleCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.purple]),
borderRadius: BorderRadius.circular(16),
),
child: /* custom layout */,
);
}
}
When React Native Wins
- Existing JavaScript/TypeScript team — no new language to learn
- OTA updates are important — CodePush deploys JS bundle updates without App Store review
- Web-to-mobile port — much code and patterns carry over from React web
- Rapid prototyping — fast hot reload; huge npm ecosystem
// RN: OTA update for a business logic fix, deployed in hours not days
import CodePush from 'react-native-code-push';
const App = CodePush({ checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME })(AppComponent);
KMM as a Middle Path
KMM is not a cross-platform UI framework — it shares business logic while keeping UI native. This gives you:
- Native UI fidelity on both platforms
- Shared repository, use case, and model code
- No "lowest common denominator" UI restrictions
Use KMM when you have separate Android/iOS teams but want to eliminate duplicate logic layers.
Key Takeaways
| Scenario | Choose |
|---|---|
| Deep platform features (AR, BLE, payments) | Native |
| One team, custom design system | Flutter |
| Existing JS team, fast iteration | React Native |
| Shared logic, native UI | KMM |
| Startup with limited budget | Flutter or RN |
| Consumer app where feel matters | Native |