MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Native vs Flutter vs Kotlin Multiplatform

Comprehensive comparison of React Native, Flutter, and Kotlin Multiplatform: performance, ecosystem, developer experience, and when to choose each.

React NativeFlutterKMPMobile

By MinhVo

Introduction

Cross-platform mobile development has matured significantly over the past decade, with three frameworks emerging as the dominant choices for teams building applications that target both iOS and Android from a single codebase. React Native, created by Meta, leverages the massive JavaScript ecosystem and renders to native platform views. Flutter, backed by Google, takes a fundamentally different approach with its own Skia-based rendering engine that draws every pixel on screen. Kotlin Multiplatform, developed by JetBrains, focuses on sharing business logic and data layers while preserving fully native user interfaces on each platform.

The choice between these three frameworks is not simply a technical decision — it has profound implications for hiring, team structure, long-term maintenance costs, and the user experience your application can deliver. A React Native team draws from the enormous pool of JavaScript developers, while a Kotlin Multiplatform team requires engineers comfortable with both Kotlin and platform-specific UI frameworks like SwiftUI and Jetpack Compose. Flutter demands expertise in Dart, a language with a smaller community but excellent tooling and language design.

This guide provides a deep technical comparison across architecture, performance, developer experience, ecosystem maturity, and production readiness. Each section includes code examples, benchmark data, and practical guidance drawn from real-world production applications built with all three frameworks. By the end, you will have the information needed to make an informed decision aligned with your team's skills, project requirements, and long-term strategic goals.

Cross-platform development

Architecture Deep Dive

React Native: JavaScript Bridge to Native

React Native runs JavaScript in a dedicated thread and communicates with native UI components through a bridge layer. Components defined in JavaScript render to actual UIKit views on iOS and Android Views on the Android platform. This means your application uses the same scrolling, text input, and navigation components that fully native applications use.

The original architecture relied on an asynchronous bridge that serialized messages between JavaScript and native code using JSON. This serialization introduced latency, particularly during animations and rapid UI updates. The new architecture, which has been rolling out since 2022, replaces this bridge with JSI (JavaScript Interface), enabling direct synchronous calls between JavaScript and native code. Fabric, the new rendering system, allows the JavaScript thread to directly manipulate the shadow tree without waiting for bridge round-trips.

// React Native — renders native UIKit/Android views
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <View style={styles.container}>
      <Text style={styles.count}>{count}</Text>
      <TouchableOpacity 
        onPress={() => setCount(c => c + 1)}
        style={styles.button}
        activeOpacity={0.7}
      >
        <Text style={styles.buttonText}>Increment</Text>
      </TouchableOpacity>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: { padding: 20, alignItems: 'center' },
  count: { fontSize: 48, fontWeight: 'bold' },
  button: { 
    padding: 12, 
    backgroundColor: '#007AFF', 
    borderRadius: 8, 
    marginTop: 16 
  },
  buttonText: { color: 'white', fontSize: 18 },
});

The key architectural insight is that React Native applications are split into two worlds: the JavaScript thread where your application logic runs, and the main thread where native UI rendering happens. Understanding this separation is critical for writing performant React Native code, because any work that blocks the JavaScript thread will delay UI updates and cause dropped frames.

Flutter: Custom Rendering Engine

Flutter takes a radically different architectural approach. Rather than delegating to platform-native UI components, Flutter ships its own rendering engine based on Skia (the same library that powers Chrome's graphics). Every widget, text element, and animation is drawn by Flutter's engine onto a Skia canvas. The only native components are the surface (a UIView on iOS, a SurfaceView on Android) and platform services like camera, GPS, and file system access.

This architecture provides pixel-perfect consistency across platforms. A Flutter application looks identical on iOS and Android because the rendering is not influenced by platform-specific widget implementations. This is both Flutter's greatest strength and its most significant tradeoff — you get complete visual control, but your application does not adopt the native look and feel of each platform by default.

// Flutter — draws its own UI with Skia
import 'package:flutter/material.dart';
 
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) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            '$count',
            style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: () => setState(() => count++),
            style: ElevatedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8),
              ),
            ),
            child: const Text('Increment', style: TextStyle(fontSize: 18)),
          ),
        ],
      ),
    );
  }
}

Flutter compiles Dart to native ARM code using AOT (Ahead-of-Time) compilation for release builds, and uses JIT (Just-in-Time) compilation during development to enable hot reload. The Dart VM in development mode supports sub-second hot reload by injecting updated source code into the running application without restarting it.

Kotlin Multiplatform: Shared Business Logic

Kotlin Multiplatform takes the most conservative architectural approach. Rather than replacing the native UI layer, KMP focuses on sharing the code that sits beneath the UI: business logic, networking, data persistence, and domain models. You write the UI layer natively for each platform using SwiftUI on iOS and Jetpack Compose on Android, while sharing 40-70% of your codebase through a common Kotlin module.

This architecture is particularly well-suited for teams with existing native applications. You can incrementally adopt KMP by creating a shared module and migrating business logic into it over time. The expect/actual mechanism allows you to define platform-specific implementations for APIs that differ between iOS and Android, such as file system access, date formatting, and cryptographic operations.

// Shared Kotlin module (commonMain)
class UserRepository(
    private val api: ApiClient,
    private val db: Database
) {
    suspend fun getUsers(): Result<List<User>> = runCatching {
        val users = api.fetchUsers()
        db.saveUsers(users)
        users
    }.recover { 
        db.getCachedUsers()
    }
}
 
// iOS UI (SwiftUI)
struct UserListView: View {
    @StateObject var viewModel = UserListViewModel()
 
    var body: some View {
        NavigationView {
            List(viewModel.users, id: \.id) { user in
                UserRow(user: user)
            }
            .navigationTitle("Users")
            .refreshable { await viewModel.refresh() }
        }
    }
}
 
// Android UI (Jetpack Compose)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UserList(viewModel: UserListViewModel = koinViewModel()) {
    val users by viewModel.users.collectAsState()
    val isRefreshing by viewModel.isRefreshing.collectAsState()
    
    Scaffold(
        topBar = { TopAppBar(title = { Text("Users") }) }
    ) { padding ->
        SwipeRefresh(
            state = rememberSwipeRefreshState(isRefreshing),
            onRefresh = { viewModel.refresh() },
        ) {
            LazyColumn(contentPadding = padding) {
                items(users, key = { it.id }) { user ->
                    UserRow(user = user)
                }
            }
        }
    }
}

Architecture comparison

Performance Comparison

Startup Time

Startup time is one of the most impactful performance metrics because it directly affects user retention. Studies show that a one-second delay in startup time can reduce conversions by 7%.

MetricReact NativeFlutterKMP
Cold start~1.5-2s~0.8-1.2s~0.6-1s
JS engine init~300msN/AN/A
Dart VM initN/A~200msN/A
First meaningful paint~1s~0.5s~0.4s

KMP wins startup time because there is no virtual machine to initialize — shared code compiles directly to native libraries. React Native pays the cost of initializing the JavaScript engine (Hermes or JavaScriptCore) before any application code can execute. Flutter's Dart VM initialization is faster than JavaScript engine startup but still adds measurable overhead.

Runtime Performance

ScenarioReact NativeFlutterKMP
List scrollingGood (native)ExcellentExcellent (native)
AnimationsGood (native driver)Excellent (Skia)Excellent (native)
Complex UIGoodExcellentExcellent
Heavy computationPoor (JS thread)Good (Dart)Excellent (native)
Memory usageHigh (JS heap)MediumLow

Performance Deep Dive

React Native performance depends on the bridge. Frequent JS-to-native communication can cause frame drops, particularly during animations that require continuous updates. The new architecture with Fabric and TurboModules addresses this by enabling synchronous direct native access through JSI, eliminating the serialization overhead that caused performance bottlenecks in the old bridge architecture. The Hermes JavaScript engine, which Facebook developed specifically for React Native, provides faster startup times and lower memory usage compared to JavaScriptCore.

Flutter performance is consistent because it controls the entire rendering pipeline from Dart to Skia to the GPU. There is no platform widget translation layer — Flutter draws everything itself. This eliminates the variance introduced by platform-specific widget implementations. However, the application must bundle the Flutter engine (~4MB compressed), which increases download size. Flutter's Impeller rendering engine, introduced in Flutter 3.16, replaces Skia on iOS and provides more predictable performance by eliminating shader compilation jank.

KMP shares compiled native code, so business logic runs at native speed with no runtime overhead. The UI is platform-native, meaning scrolling, animations, and interactions are indistinguishable from fully native applications. There is no virtual machine, no bridge, and no rendering engine to initialize or maintain. The shared Kotlin code compiles to native frameworks on iOS and native libraries on Android through Kotlin/Native and Kotlin/JVM respectively.

Developer Experience

Language and Learning Curve

FrameworkLanguageLearning CurvePrior Knowledge
React NativeJavaScript/TypeScriptLow (web devs)React, JS
FlutterDartMediumNew language
KMPKotlinMedium-HighAndroid/Kotlin

React Native has the lowest barrier to entry for teams with web development experience. If your team already knows React, the transition to React Native is relatively smooth — the component model, hooks, and state management patterns are identical. The main learning curve involves understanding the native bridge, platform-specific APIs, and the differences between web and mobile interaction patterns.

Flutter requires learning Dart, which is a clean, well-designed language but has a much smaller community than JavaScript or Kotlin. Developers coming from Java or C# will find Dart's syntax familiar. The real learning curve with Flutter is understanding its widget composition model and the extensive widget catalog.

Kotlin Multiplatform requires the most diverse skill set. Your team needs to be comfortable with Kotlin (which has a steeper learning curve than JavaScript but is very productive once mastered), plus SwiftUI for iOS and Jetpack Compose for Android. This is a significant investment, but it produces the most native results.

Hot Reload

All three support hot reload, but the experience varies significantly:

// React Native: Fast Refresh
// Preserves component state, updates changed components
// Sub-second reload for most changes
// Some changes require full reload (native module changes, new imports)
 
// Flutter: Hot Reload
// Preserves app state, rebuilds widget tree
// Very fast (< 1 second) — best hot reload experience
// Works for most widget changes, not for state initialization changes
 
// KMP: Limited hot reload
// Business logic changes require full rebuild (15-60 seconds)
// UI hot reload via Compose Preview and SwiftUI Preview
// Compose Preview is excellent for iterative UI development

Flutter has the best hot reload experience overall — changes appear in under a second and the application state is preserved. React Native's Fast Refresh is close behind, with most changes appearing in 1-2 seconds. KMP's hot reload is limited because changes to the shared Kotlin module require recompilation, though Compose Preview and SwiftUI Preview provide excellent iterative development for the UI layer.

Code Sharing Potential

LayerReact NativeFlutterKMP
Business logic~100%~100%~100%
UI code~100%~100%~0% (native)
Networking~100%~100%~100%
Data layer~100%~100%~100%
Platform code~5%~0%~30%

KMP shares less code overall because the UI layer is native, but the shared code is the most valuable and complex part — business logic, networking, and data management. React Native and Flutter share more code, including the UI, but this comes at the cost of not using native platform widgets (Flutter) or relying on a bridge to access them (React Native).

Ecosystem comparison

Ecosystem and Libraries

React Native

Strengths:

  • Massive npm ecosystem with 2 million+ packages available
  • Expo provides a complete development toolchain with managed and bare workflows
  • React Native Web enables sharing code with web applications from the same codebase
  • Active community with 115k+ GitHub stars and thousands of production applications
  • Strong job market — largest talent pool among cross-platform frameworks
  • Reanimated 3 provides silky-smooth animations running on the UI thread

Weaknesses:

  • Package quality varies significantly — many community packages are unmaintained
  • Native module linking can be complex, especially for modules with native dependencies
  • Breaking changes between major versions require significant migration effort
  • iOS release delays when Apple deprecates or changes native APIs

Flutter

Strengths:

  • Official packages from Google for Firebase, Maps, Ads, and other services
  • Consistent API design with excellent documentation and code samples
  • Material Design 3 and Cupertino widgets built into the framework
  • Flutter Web and Flutter Desktop extend the framework beyond mobile
  • pub.dev package quality is generally high due to Flutter Favorites program
  • Impeller rendering engine eliminates shader compilation jank

Weaknesses:

  • Smaller ecosystem than npm with approximately 45k packages on pub.dev
  • Some platform features require platform channels to native code
  • Web support has SEO challenges because Flutter renders to a canvas
  • Limited native platform integration compared to React Native and KMP
  • Dart language has a much smaller community than JavaScript or Kotlin

Kotlin Multiplatform

Strengths:

  • Native performance for shared code with zero runtime overhead
  • Full access to platform APIs through the expect/actual mechanism
  • JetBrains IDE support with excellent debugging and refactoring tools
  • Growing ecosystem: Ktor for networking, SQLDelight for databases, Koin for dependency injection, Decompose for navigation
  • Kotlin coroutines provide elegant asynchronous programming without callback hell
  • Compose Multiplatform enables sharing UI code across platforms

Weaknesses:

  • Smallest ecosystem of the three — fewer ready-made solutions for common tasks
  • Still maturing — officially graduated from Beta in November 2023
  • Requires platform-specific UI knowledge unless using Compose Multiplatform
  • iOS debugging experience is less polished than Android debugging
  • Build times for Kotlin/Native iOS targets can be slower than Android builds

Production Considerations

App Size

FrameworkAndroid APKiOS IPA
React Native~8-15MB~10-20MB
Flutter~5-10MB~8-15MB
KMP~3-8MB~5-12MB

KMP produces the smallest applications because there is no runtime engine — shared code compiles to native libraries that are linked directly into the application binary. React Native bundles the JavaScript engine (Hermes or JavaScriptCore) and the bridge infrastructure. Flutter bundles the Skia rendering engine and the Dart runtime.

Who Uses What

React Native:

  • Facebook, Instagram, Shopify, Discord, Pinterest, Bloomberg, Walmart, Tesla, Uber Eats, Coinbase

Flutter:

  • Google Pay, BMW, Alibaba, eBay, Nubank, Toyota, Philips Hue, Tencent, ByteDance

KMP:

  • Netflix, Philips, Cash App (Square), VMWare, Quizlet, McDonald's, Bosch, Careem

Migration Path

React Native: Supports incremental adoption in existing native applications through the Brownfield approach. You can embed React Native views within existing UIKit or Android View hierarchies, starting with a single screen and expanding gradually. This is the most mature brownfield integration story among the three frameworks.

Flutter: Can be integrated into existing applications using FlutterViewController on iOS and FlutterFragment on Android. However, the integration is more complex because Flutter manages its own rendering surface, which can create layering and lifecycle challenges when embedded in a native view hierarchy.

KMP: The most natural fit for existing native applications. You create a shared Kotlin module, add it as a dependency to your existing Android and iOS projects, and gradually migrate business logic into it. The UI layer remains unchanged — you are only extracting and sharing the logic that does not depend on platform-specific APIs.

When to Choose Each

Choose React Native When:

  • Your team has web or React experience and you want the fastest ramp-up time
  • You need rapid prototyping with an excellent developer experience through Expo
  • Code sharing with web applications is valuable for your product strategy
  • You want access to the largest ecosystem of third-party libraries
  • Hiring is a priority — the JavaScript developer pool is the largest globally
  • You are building a content-driven application where pixel-perfect native feel is not critical

Choose Flutter When:

  • You want pixel-perfect custom UI that looks identical on both platforms
  • Smooth animations and consistent rendering performance are critical requirements
  • You are building a new application from scratch without an existing native codebase
  • You want a single codebase with minimal platform differences and strong design system support
  • You are targeting multiple platforms including mobile, web, and desktop
  • Your design team uses Material Design or wants complete visual control

Choose KMP When:

  • You have existing native applications that you want to modernize incrementally
  • Platform-native UI is a requirement — your users expect the authentic iOS and Android experience
  • Your team has Kotlin experience and is comfortable with native mobile development
  • You want maximum performance and the smallest possible application binary size
  • You are building a long-term product where native quality and platform integration matter
  • You want to share business logic without compromising on platform-specific user experience

Best Practices

Mobile development

  1. Prototype in all three — Build a small but representative feature in each framework before committing to understand the real tradeoffs for your specific use case
  2. Consider your team first — Language familiarity is the single biggest factor in development velocity; a team that knows JavaScript will be 2-3x more productive in React Native than learning Flutter from scratch
  3. Plan for long-term maintenance — Evaluate corporate backing, community health, and ecosystem trajectory; React Native and Flutter have strong corporate investment, while KMP benefits from JetBrains' commitment to Kotlin
  4. Profile on real devices — Simulator performance is misleading, especially for memory usage, scroll performance, and battery consumption; test on mid-range devices that represent your target audience
  5. Start with shared logic — Even if you choose native UI, sharing business logic through KMP or a shared library provides 60-80% code reduction with minimal risk

Common Pitfalls

PitfallImpactSolution
Choosing based on hype or blog postsFramework mismatches team skillsEvaluate team skills and project requirements first
Ignoring platform differencesInconsistent user experienceTest on both platforms early and often
Over-sharing UI codePlatform-specific rendering issuesKeep platform-specific UI native when it matters
Not profiling on real devicesSlow performance in productionProfile early on mid-range devices, not just flagships
Underestimating maintenance costsTechnical debt accumulates rapidlyConsider long-term ecosystem health and upgrade burden
Skipping performance budgetsApp size and startup time balloonSet and enforce performance budgets from day one

Detailed Framework Comparison

CategoryReact NativeFlutterKotlin Multiplatform
LanguageJavaScript/TypeScriptDartKotlin
UI approachNative platform viewsSkia canvas renderingNative platform views
Code sharing60-80%80-95%40-70%
Hot reloadFast RefreshHot reloadHot reload (limited)
PerformanceGoodExcellentExcellent
App size overhead~7MB~4MB~1MB
Learning curveLow (JS devs)MediumMedium (Kotlin devs)
Community sizeLargestLargeGrowing
Corporate backingMetaGoogleJetBrains
Production maturity8+ years6+ years4+ years
Web supportReact Native WebFlutter WebKotlin/JS
Desktop supportCommunityOfficialOfficial

Migration Strategies

When migrating an existing native application to a cross-platform framework, adopt an incremental approach rather than attempting a full rewrite. Start by identifying modules that are good candidates for sharing: business logic, data models, networking code, and utility functions. Implement these shared modules using your chosen framework and integrate them into the existing native application.

With Kotlin Multiplatform, this migration is particularly natural because you can add a shared Kotlin module to an existing Android project and an existing Xcode project simultaneously. Business logic migrates gradually while the UI layer remains untouched. React Native supports embedding React Native views within existing native applications through the Brownfield integration approach, allowing you to replace individual screens one at a time. Flutter can be integrated into existing applications using FlutterViewController on iOS and FlutterFragment on Android, though the integration requires more careful management of the Flutter engine lifecycle.

This incremental strategy reduces risk dramatically. Your team gains experience with the framework on a small scale before committing to a full migration. You can measure the impact on developer productivity, application performance, and user experience with real data rather than projections. If the framework proves to be a poor fit, you can stop the migration without having lost the investment in your existing native codebase.

Testing Strategies Across Frameworks

Each cross-platform framework has its own testing ecosystem and best practices. React Native supports Jest for unit testing, React Native Testing Library for component testing, and Detox for end-to-end testing that interacts with the actual native UI. Flutter provides the flutter_test package for unit and widget testing with golden file testing for visual regression, integration_test for integration testing that runs on real devices, and Patrol for end-to-end testing with native interactions like permission dialogs and notifications. Kotlin Multiplatform leverages platform-specific testing frameworks through Kotlin's expect/actual mechanism, allowing you to write shared tests that run on all platforms while also writing platform-specific tests where needed.

For all frameworks, implement a testing pyramid with many fast unit tests at the base, fewer integration tests in the middle, and a small number of end-to-end tests at the top. Run tests on both platforms in your CI pipeline to catch platform-specific regressions early. Use snapshot testing for UI components to detect unintended visual changes. Mock external dependencies at the network layer rather than the component layer to test real integration paths.

Conclusion

React Native, Flutter, and Kotlin Multiplatform each solve cross-platform development differently, and each approach has clear strengths aligned with specific team profiles and project requirements. React Native leverages the web ecosystem and provides the largest talent pool. Flutter delivers pixel-perfect control and consistent rendering across platforms. KMP maximizes native quality by sharing only the most valuable code — business logic — while preserving authentic platform user experiences.

For most teams, the decision comes down to your starting point:

  • Web developers → React Native (familiar language, fast iteration, massive ecosystem)
  • Custom UI priority → Flutter (consistent rendering, great animations, multi-platform reach)
  • Native quality priority → KMP (platform-native UI, shared business logic, smallest app size)

Key takeaways:

  1. React Native — Best for web teams transitioning to mobile, largest ecosystem, native platform views with excellent brownfield integration
  2. Flutter — Best for teams that want complete visual control, consistent rendering across platforms, and multi-platform support from a single codebase
  3. KMP — Best for teams with existing native applications, platform-native UI requirements, and long-term products where native quality matters
  4. All three are production-ready — Used by major companies worldwide with millions of users
  5. Prototype before committing — Build a representative feature in each framework to evaluate the real tradeoffs for your team
  6. Consider long-term strategy — Ecosystem maturity, corporate backing, and maintenance costs matter more than initial development speed