Cross-platform mobile development in 2026 comes down to three mature options, and each one shares code at a different layer. Flutter owns the rendering pipeline and draws every pixel itself. React Native maps JavaScript components to real native views. Kotlin Multiplatform (KMP) shares business logic and, optionally, UI through Compose Multiplatform. Your team’s language skills, UI requirements, and existing codebase decide the winner.
Quick Takeaways
- Choose Flutter when you need pixel-identical, animation-heavy UI on mobile, web, and desktop from one codebase.
- Choose React Native when your team already writes TypeScript/React and wants native-looking components plus a huge npm ecosystem.
- Choose Kotlin Multiplatform when you have native Android/iOS apps and want to share logic incrementally without a rewrite.
- All three are production-ready. The riskiest choice is picking one that fights your team’s existing skills.
| Criteria | Flutter | React Native | Kotlin Multiplatform |
|---|---|---|---|
| Language | Dart | TypeScript/JavaScript | Kotlin |
| UI approach | Own renderer (Impeller) | Native views via Fabric | Native UI or Compose Multiplatform |
| Code sharing | UI + logic (~90–100%) | UI + logic (~85–95%) | Logic (~40–70%), UI optional |
| Learning curve | Moderate (new language) | Low for web devs | Moderate to high |
| Incremental adoption | Possible, awkward | Possible, common | Excellent |
| Hiring pool | Medium | Largest | Growing, Kotlin-centric |
How Each Framework Works Under the Hood
Architecture explains almost every trade-off in this comparison. Understand the rendering model first, and the rest follows.
Flutter: Own the Canvas
Flutter ships its own rendering engine. The Impeller renderer precompiles shaders, which removed the shader-compilation jank that plagued earlier versions. Your Widget tree describes the UI, Flutter diffs it into an element tree, and the engine paints it to a GPU surface. The OS never sees individual buttons, only one canvas.
Pixel-level consistency across devices follows from this design. The cost is that platform-native widgets, such as a native date picker or accessibility behaviors, must be re-implemented or bridged.
React Native: Orchestrate Native Views
React Native runs your JavaScript on the Hermes engine. The New Architecture (JSI, Fabric, TurboModules) replaced the old asynchronous JSON bridge with direct, synchronous calls between JavaScript and native code. Your <View> becomes a real UIView or android.view.View.
The result is native look, feel, and accessibility by default. The trade-off is that two runtimes cooperate, so performance tuning means understanding both the JS thread and the UI thread.
Kotlin Multiplatform: Share What You Choose
KMP compiles Kotlin to JVM bytecode for Android and to native binaries (via Kotlin/Native) for iOS. You pick the layer to share. Most teams start with networking, caching, and domain logic. Others adopt Compose Multiplatform, which reached stable iOS support in 2025, to share UI as well.
KMP is a toolkit and not a monolithic framework. You keep full access to platform APIs through the expect/actual mechanism.
Practical Implementation: The Same Feature Three Ways
Each example below renders a list of articles fetched from an API. The code is minimal but follows production conventions.
Flutter Implementation
import 'package:flutter/material.dart';
class Article {
const Article({required this.id, required this.title});
final int id;
final String title;
}
class ArticleList extends StatelessWidget {
const ArticleList({super.key, required this.articles});
final List<Article> articles;
@override
Widget build(BuildContext context) {
// ListView.builder lazily builds only visible rows.
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
final article = articles[index];
return ListTile(
key: ValueKey(article.id),
title: Text(article.title),
);
},
);
}
}
ListView.builder creates children on demand, so memory scales with the visible window rather than the full list of n items. The const constructor lets Flutter skip rebuilds when inputs are unchanged.
React Native Implementation
import React, { memo, useCallback } from 'react';
import { FlatList, Text, View } from 'react-native';
type Article = { id: number; title: string };
const Row = memo(({ title }: { title: string }) => (
<View style={{ padding: 16 }}>
<Text>{title}</Text>
</View>
));
export function ArticleList({ articles }: { articles: Article[] }) {
// Stable references prevent needless re-renders of every row.
const renderItem = useCallback(
({ item }: { item: Article }) => <Row title={item.title} />,
[]
);
const keyExtractor = useCallback((item: Article) => String(item.id), []);
return (
<FlatList
data={articles}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
);
}
FlatList virtualizes rows like Flutter’s builder. Wrapping Row in memo() and stabilizing callbacks with useCallback() stops React from re-rendering unchanged rows.
Kotlin Multiplatform Implementation
Shared logic lives in commonMain:
// commonMain: runs on Android and iOS
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import kotlinx.serialization.Serializable
@Serializable
data class Article(val id: Int, val title: String)
class ArticleRepository(private val client: HttpClient) {
suspend fun fetchArticles(): List<Article> =
client.get("https://api.example.com/articles").body()
}
Platform-specific code uses expect/actual:
// commonMain
expect fun platformName(): String
// androidMain
actual fun platformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"
// iosMain
import platform.UIKit.UIDevice
actual fun platformName(): String = UIDevice.currentDevice.systemName()
The ArticleRepository compiles once and runs everywhere. Your SwiftUI or Jetpack Compose layer simply calls fetchArticles(). Using Compose Multiplatform, you can also share the list UI itself with a LazyColumn.
Performance Comparison
Benchmarks vary by app, so treat this as directional guidance.
| Factor | Flutter | React Native | KMP |
|---|---|---|---|
| Startup time | Fast (AOT-compiled) | Good (Hermes bytecode) | Native-level (native UI) |
| Animation smoothness | Excellent (own renderer) | Good, needs Reanimated for complex cases | Native-level, or Compose-level |
| App size overhead | Larger (engine bundled) | Moderate (Hermes + runtime) | Smallest (native UI) |
| Memory overhead | Moderate | Moderate | Low |
| Native API access | Via platform channels / FFI | Via TurboModules | Direct |
KMP has the lowest overhead because it adds a shared library to a native app. Flutter’s overhead buys consistent rendering. React Native sits in the middle and gets closer to native with each New Architecture release.
Ecosystem, Tooling, and Hiring
| Factor | Flutter | React Native | KMP |
|---|---|---|---|
| Package registry | pub.dev | npm | Maven Central + KMP libraries |
| Hot reload | Excellent (stateful hot reload) | Excellent (Fast Refresh) | Compose previews; native tooling |
| Primary IDE | Android Studio / VS Code | VS Code / any | Android Studio / IntelliJ + Xcode |
| Web support | Yes (Wasm/CanvasKit) | Via React Native Web | Experimental (Compose for Web) |
| Desktop support | Windows, macOS, Linux | Windows/macOS via Microsoft | Compose Desktop (JVM) |
| Corporate backing | Meta + Expo/Microsoft/Callstack | JetBrains + Google |
Expo has become the default way to start a React Native project, which removes most native-toolchain setup. Flutter’s single, opinionated toolchain remains the easiest to install. KMP demands the most platform knowledge, since you will touch Xcode and Gradle.
Decision Framework: Match the Stack to Your Situation
Use these rules in order.
- You have existing native Android and iOS apps. Choose KMP. Share one module at a time and keep your current UI.
- Your team writes React and TypeScript daily. Choose React Native. Reusing skills beats a theoretically better framework.
- You need a custom, brand-heavy UI or heavy animation on every platform. Choose Flutter. A single renderer removes platform divergence.
- You target mobile, web, and desktop with one team. Choose Flutter first, or React Native with Expo if your web app is already React.
- You are an Android-first Kotlin team expanding to iOS. Choose KMP with Compose Multiplatform. Your skills transfer directly.
- You are building a startup MVP with a small team. Choose Flutter or React Native, whichever language your first hires know.
Best Practices: Bad Code vs. Good Code
Flutter: Avoid Rebuilding the World
Bad Code (Anti-pattern):
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override
Widget build(BuildContext context) {
// The entire screen, including the expensive child, rebuilds on every tap.
return Column(
children: [
Text('$count'),
ElevatedButton(
onPressed: () => setState(() => count++),
child: const Text('Add'),
),
ExpensiveChart(), // Not const; rebuilt every time
],
);
}
}
Good Code (Refactored):
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
final ValueNotifier<int> count = ValueNotifier(0);
@override
void dispose() {
count.dispose(); // Always release notifiers
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Only this subtree listens to changes.
ValueListenableBuilder<int>(
valueListenable: count,
builder: (_, value, __) => Text('$value'),
),
ElevatedButton(
onPressed: () => count.value++,
child: const Text('Add'),
),
const ExpensiveChart(), // const: skipped during rebuilds
],
);
}
}
Scoping state to the smallest subtree and marking static widgets const cuts rebuild work sharply.
React Native: Stop Using ScrollView for Long Lists
Bad Code (Anti-pattern):
// Renders all n rows at once: O(n) memory and mount cost
<ScrollView>
{articles.map((a) => (
<Text key={a.id} onPress={() => open(a.id)}>{a.title}</Text>
))}
</ScrollView>
Good Code (Refactored):
// Virtualized: cost scales with the visible window
<FlatList
data={articles}
keyExtractor={(a) => String(a.id)}
renderItem={renderItem} // stable reference from useCallback
initialNumToRender={10}
windowSize={5}
/>
KMP: Keep Platform Code Out of commonMain
Bad Code (Anti-pattern):
// commonMain: does not compile for iOS, and couples logic to Android
import android.content.Context
class Cache(private val context: Context) {
fun save(key: String, value: String) { /* SharedPreferences... */ }
}
Good Code (Refactored):
// commonMain: define a contract, inject the implementation per platform
interface KeyValueStore {
fun save(key: String, value: String)
fun read(key: String): String?
}
class Cache(private val store: KeyValueStore) {
fun remember(key: String, value: String) = store.save(key, value)
}
// androidMain / iosMain provide KeyValueStore implementations
Interfaces plus dependency injection keep shared code testable. Reserve expect/actual for small, stable platform differences.
Common Edge Cases and Pitfalls
- Flutter: Platform channels cost development time when you need niche native SDKs. Check that a maintained plugin exists before committing.
- React Native: Third-party libraries that still rely on the legacy architecture can block upgrades. Audit dependencies for New Architecture support.
- KMP: Kotlin/Native interop exposes Kotlin APIs to Swift through Objective-C headers, so generics and sealed classes can look awkward in Swift. Design shared APIs with iOS consumers in mind.
- All three: Accessibility, deep links, push notifications, and background tasks need real-device testing on both platforms. No framework fully abstracts them.
Migration and Risk Considerations
Rewrites fail more often than incremental adoption. KMP is the strongest choice for incremental change because it plugs into existing native projects. React Native supports brownfield integration through embedded views, and Flutter offers add-to-app modules, but both add more build complexity than KMP’s library approach.
Budget for the long term too. Framework upgrades arrive several times a year. Pin versions, automate dependency updates, and keep a CI pipeline that builds both platforms on every pull request.
Final Recommendation
Pick based on team skills first, UI requirements second, and benchmarks last. Flutter gives the most uniform UI from one codebase. React Native gives the fastest path for web and TypeScript teams. Kotlin Multiplatform gives native fidelity with shared logic and the safest migration path. Prototype your hardest screen in your top choice for one sprint before committing.
FAQ
Is Flutter still worth learning in 2026?
Yes. Flutter remains one of the most widely used cross-platform frameworks, with a stable rendering engine and strong tooling. Learn it if you want one codebase across mobile, web, and desktop with a consistent UI.
Is React Native better than Flutter?
Neither is universally better. React Native suits teams with JavaScript or React experience and apps that should use native components. Flutter suits teams that want full control over rendering and custom, animation-rich interfaces.
Is Kotlin Multiplatform ready for production?
Yes. KMP has been stable since late 2023, and Compose Multiplatform for iOS reached stable status in 2025. Many companies use it in production to share networking, data, and business logic across Android and iOS.
Which is best for a startup MVP?
Flutter or React Native. Both deliver fast iteration with hot reload and a single codebase. Choose React Native if your developers know TypeScript, and Flutter if you want one toolchain and a custom UI.




