Desenvolvimento Mobile Multiplataforma: Escolhendo sua Estratégia
A comprehensive comparison of React Native, Flutter, Kotlin Multiplatform, and native development — with practical advice for teams choosing their mobile strategy.
Every mobile team reaches a crossroads. You have users on iOS and Android, a limited engineering budget, and pressure to ship features faster than your current velocity allows. The question is no longer whether to consider cross-platform development — it is which approach fits your specific constraints. The landscape in 2026 is more mature than ever, with four dominant strategies competing for your attention: React Native, Flutter, Kotlin Multiplatform, and the native SDKs themselves. Each has evolved significantly in the past few years, and the gaps between them have narrowed.
This guide compares these approaches across every dimension that matters: developer experience, performance, UI fidelity, state management, testing, deployment, and long-term maintainability. It does not declare a single winner, because there is none. The right choice depends on your team's existing skills, your product's requirements, and your tolerance for platform-specific workarounds. What this guide will do is give you a structured framework for making that decision.
The cross-platform landscape in 2026
Cross-platform development has matured dramatically since the early days of PhoneGap and Cordova. The current generation of frameworks delivers native-quality experiences with code-sharing ratios that would have seemed impossible a decade ago. React Native has gone through its architectural renaissance, Flutter has expanded beyond mobile into desktop and web, and Kotlin Multiplatform has emerged as the pragmatic choice for teams that want shared logic without sacrificing native UI.
The market has also clarified. Framework churn — the fear that your chosen technology will be abandoned in two years — is less of a concern now than it was in 2022. Meta continues to invest heavily in React Native, Google shows no signs of slowing Flutter development, and JetBrains has stabilized Kotlin Multiplatform with strong industry adoption. The risk profile of each platform is well understood, and that stability makes it easier to commit long-term.
What has not changed is the fundamental tension: shared code reduces development cost but introduces abstraction layers that can leak, especially when you need deep platform integration. The winning strategy in 2026 is not to find a framework that hides the platform entirely — it is to choose one that makes the platform boundary predictable and ergonomic when you need to cross it.
React Native
Architecture and the new architecture
React Native has undergone the most significant architectural transformation of any cross-platform framework. The New Architecture — consisting of Fabric (the new rendering system) and TurboModules (the new native module system) — is now the default in React Native 0.76 and later. This is not an incremental improvement. It is a ground-up rewrite of the bridge that has been React Native's bottleneck since its inception.
The old bridge was asynchronous and serialized everything through JSON. Every interaction between JavaScript and native code crossed a serialization boundary, which added latency to every gesture, animation, and API call. Fabric replaces this with a synchronous interface that runs on a new thread called the JavaScript Interface (JSI), allowing JavaScript to hold references to native objects and call them directly. The result is faster startup times, lower memory usage, and animation performance that rivals native code for most use cases.
TurboModules lazy-load native modules, so your app only pays the initialization cost for modules it actually uses. Under the old bridge, every native module was loaded at startup regardless of whether it was needed — a significant penalty for apps with many dependencies. The new architecture also enables interop with synchronous native APIs like file I/O and database access, which previously required awkward workarounds.
Expo and the developer experience
Expo has transformed from a walled garden into the recommended way to start a React Native project. The expo-dev-client approach allows you to use Expo's managed workflow for most development while maintaining the ability to drop into bare workflow for custom native modules. Expo Application Services (EAS) provide cloud-based builds, submissions, and updates that eliminate most of the DevOps pain traditionally associated with React Native deployment.
For teams that value developer experience, Expo's tooling is difficult to overstate. Hot reload works reliably across platforms. The build pipeline handles code signing, provisioning profiles, and app store metadata. Over-the-air updates through Expo Updates let you push JavaScript bundles without going through app store review — a capability that alone changes the calculus for many teams.
Expo has done more for React Native developer experience than any single change to the framework itself. The gap between starting a project and shipping to the store has narrowed from weeks to hours.
A practical React Native component
import { View, Text, FlatList, Pressable, StyleSheet } from "react-native";
import { useCallback, useMemo } from "react";
import Animated, { FadeInUp } from "react-native-reanimated";
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
}
interface ProductListProps {
products: Product[];
onProductPress: (id: string) => void;
}
function ProductCard({ product, onPress }: {
product: Product;
onPress: (id: string) => void;
}) {
return (
<Animated.View entering={FadeInUp.duration(300)}>
<Pressable
style={styles.card}
onPress={() => onPress(product.id)}
>
<Text style={styles.name}>{product.name}</Text>
<Text style={styles.price}>
${product.price.toFixed(2)}
</Text>
{!product.inStock && (
<Text style={styles.outOfStock}>Out of stock</Text>
)}
</Pressable>
</Animated.View>
);
}
export function ProductList({ products, onProductPress }: ProductListProps) {
const renderItem = useCallback(
({ item }: { item: Product }) => (
<ProductCard product={item} onPress={onProductPress} />
),
[onProductPress]
);
return (
<FlatList
data={products}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.container}
/>
);
}
const styles = StyleSheet.create({
container: { paddingHorizontal: 16, paddingVertical: 8 },
card: {
backgroundColor: "#fff",
borderRadius: 12,
padding: 16,
marginBottom: 8,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
},
name: { fontSize: 16, fontWeight: "600" },
price: { fontSize: 14, color: "#666", marginTop: 4 },
outOfStock: { fontSize: 12, color: "#e53e3e", marginTop: 4 },
});This component demonstrates React Native's strengths: a familiar React API, platform-abstracted primitives like Pressable and FlatList, and access to the reanimated library for performant animations. The same code runs on both platforms with identical behavior in most cases.
Flutter
Dart, widgets, and the rendering engine
Flutter takes a fundamentally different approach from React Native. Instead of bridging to native UI components, Flutter ships its own rendering engine — Skia (and now Impeller on iOS) — and paints every pixel itself. This means Flutter does not depend on the platform's UI toolkit at all. A Flutter button is not a UIButton or an Android Button. It is pixels drawn by Flutter's engine, which gives the framework complete control over the rendering pipeline.
The practical consequence is predictability. Flutter widgets behave identically on iOS and Android because they are the same widgets. There is no platform divergence, no edge case where a text input looks correct on one platform but broken on the other. This consistency is Flutter's killer feature for teams that prioritize brand expression over platform convention.
The tradeoff is that Flutter apps can feel slightly foreign on each platform. A Flutter app on iOS does not use UIKit's native scroll physics, navigation transitions, or text selection behaviors out of the box. Google has invested in platform adaptation libraries like Cupertino widgets, which replicate iOS design language, but these are approximations rather than the real thing. Users who are deeply accustomed to platform conventions may notice the difference.
Dart, the language behind Flutter, continues to evolve. Sound null safety has eliminated an entire category of runtime crashes. Pattern matching and records (introduced in Dart 3) make the language competitive with modern TypeScript and Kotlin. The learning curve for developers coming from JavaScript or Java is shallow, though the widget-tree composition style takes adjustment for developers accustomed to JSX templates.
A Flutter widget example
import 'package:flutter/material.dart';
class Product {
final String id;
final String name;
final double price;
final bool inStock;
const Product({
required this.id,
required this.name,
required this.price,
required this.inStock,
});
}
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback onTap;
const ProductCard({
super.key,
required this.product,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
if (!product.inStock)
const Text(
'Out of stock',
style: TextStyle(
fontSize: 12,
color: Colors.red,
),
),
],
),
),
),
);
}
}
class ProductList extends StatelessWidget {
final List<Product> products;
final void Function(String id) onProductTap;
const ProductList({
super.key,
required this.products,
required this.onProductTap,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductCard(
product: product,
onTap: () => onProductTap(product.id),
);
},
);
}
}Flutter's widget composition is explicit and verbose compared to React Native, but it gives you complete control over the widget tree. Every visual element is a widget — padding, margins, alignment, even gesture handling. There is no CSS or JSX; everything is Dart code. This approach eliminates the context-switching between layout files and logic files that plagues many web-based frameworks.
Impeller and rendering performance
Impeller, Flutter's new rendering runtime, addresses the most common complaint about Flutter on iOS: jank during the first frame of animations. Skia's shader compilation lag caused visible stuttering on initial renders. Impeller precompiles shaders at build time, eliminating this jank entirely. Combined with Flutter's existing strengths in 120 fps scrolling and custom animation, the rendering story is now compelling across all platforms.
Kotlin Multiplatform
Shared logic, native UI
Kotlin Multiplatform (KMP) takes a different philosophical position than React Native or Flutter. Instead of providing a unified UI framework, KMP focuses exclusively on sharing business logic — networking, data models, validation, repositories, and state management — while leaving the UI layer entirely native. You write your shared code in Kotlin, compile it to platform-specific binaries via Kotlin/Native, and call it from Swift on iOS and Kotlin/JVM on Android.
This approach resonates with teams that have strong opinions about platform-specific UI but want to eliminate the duplication of business logic. If you are building an app with complex data models, sophisticated networking logic, or shared validation rules, KMP lets you write that code once and use it from both platform codebases. The UI code remains idiomatic SwiftUI or Jetpack Compose, which means your iOS app feels like an iOS app and your Android app feels like an Android app.
The tradeoff is that KMP requires proficiency in Kotlin, Swift, and Android development — a broader skill set than React Native or Flutter demand. The shared code is Kotlin, which is not as widely known as JavaScript or Dart outside the Android community. Teams adopting KMP typically need to invest in cross-training or hire developers comfortable with multiple ecosystems.
Integration patterns
- Shared networking layer with Ktor client — define API contracts once, use from both platforms
- Shared data models and validation logic — eliminates drift between platform-specific implementations
- Repository pattern with platform-specific storage implementations via expect/actual declarations
- Shared ViewModel or use-case layer with platform-observable state flows
- Shared analytics, logging, and crash reporting infrastructure
The expect/actual mechanism is KMP's primary tool for handling platform divergence. You declare an expected API in common code and provide actual implementations for each platform. This is cleaner than React Native's native module bridge because the interface is checked at compile time — if an expected function is not implemented on a platform, your code does not compile.
Compose Multiplatform
JetBrains has also been investing in Compose Multiplatform, which extends Jetpack Compose to iOS and desktop. This is a more direct competitor to Flutter, sharing UI code across platforms via Compose's declarative rendering model. As of 2026, Compose Multiplatform for iOS is stable but less mature than Flutter in terms of widget library depth and third-party ecosystem. It is worth watching for teams already invested in the Kotlin ecosystem, but it is not yet at parity with the more established cross-platform UI frameworks.
Performance comparison and benchmarks
Performance is the area where cross-platform frameworks have made the most progress — and where marketing claims are most divorced from reality. Every framework can demonstrate a demo where it outperforms native code. The question is what happens in a real app with complex navigation, background threads, and device fragmentation.
- Flutter leads in raw rendering throughput due to its self-contained engine. Impeller delivers consistent 120 fps even on mid-range devices. The cost is a larger app binary — typically 15-30 MB more than a native equivalent.
- React Native with Fabric closes the gap significantly. JSI eliminates the bridge bottleneck, and the new architecture matches native scroll and animation performance for 95% of use cases. The remaining 5% — complex gesture interactions, heavy list recycling — can still require native intervention.
- Kotlin Multiplatform has zero rendering overhead because the UI is native. Performance is determined entirely by your platform UI code. The shared logic layer adds no runtime cost beyond the interop boundary, which is negligible for most operations.
- Native development remains the ceiling. If your app pushes the boundaries of what mobile hardware can do — real-time video processing, AR, high-frequency sensor data — native code is still the only option that does not add a layer of abstraction between your logic and the metal.
The practical takeaway is that for the vast majority of consumer and enterprise apps, the performance of any modern cross-platform framework is good enough. The bottlenecks in most mobile apps are network latency, inefficient data fetching, and poor state management — not the rendering framework. Prematurely optimizing for rendering performance while ignoring these larger concerns is a common anti-pattern.
We see teams spend months optimizing render performance for a cross-platform framework when their app's real bottleneck is a single SQL query that runs on the main thread. Profile your actual app before you bet your architecture on a benchmark.
Native modules and platform-specific code
Every cross-platform framework eventually encounters the boundary where its abstractions leak. A React Native app needs to integrate with a Bluetooth SDK that only has a Swift library. A Flutter app needs to use Apple's StoreKit for in-app purchases. A KMP app needs to access a Kotlin library that is not available on iOS. How each framework handles this boundary is one of the most important factors in choosing your platform.
React Native native modules
React Native's TurboModules system provides a clean interface for writing native code. You implement a protocol in Swift or Java, register it with the module registry, and call it from JavaScript. The new architecture makes this synchronous and type-safe through code generation. Libraries like react-native-bluetooth-scan and react-native-video are well-maintained and cover most common native integration needs.
The challenge is that writing a native module requires knowledge of the platform SDK, which means you need iOS and Android developers on the team. Many React Native teams find that they need native expertise anyway — either for module authoring or for debugging platform-specific issues. The dream of a JavaScript-only mobile development team is rarely realized in practice.
Flutter platform channels
Flutter uses platform channels to communicate with native code. The mechanism is similar to React Native's bridge — messages are serialized and sent asynchronously between Dart and the platform. Flutter's Pigeon package generates type-safe platform channel code from a shared interface definition, reducing the boilerplate and eliminating serialization errors.
Flutter's ecosystem includes packages for most common native use cases — camera, location, biometrics, payments — through pub.dev. The platform channel API is straightforward for simple integrations, but complex native interactions (like a custom map view that needs bidirectional communication) can become unwieldy compared to the direct access you get with native development.
KMP expect/actual
Kotlin Multiplatform's approach to platform-specific code is the most type-safe of the three. The expect/actual mechanism enforces at compile time that every platform provides the required implementation. This eliminates the runtime crashes that occur when a React Native module is missing on one platform. The downside is that the compile-time check increases development friction — adding a new platform capability requires updates to both the expect declaration and all actual implementations before your code will compile.
UI consistency and platform conventions
The tension between brand consistency and platform conventions is one of the oldest debates in mobile development. Cross-platform frameworks tend to bias toward brand consistency — your app looks the same on iOS and Android. This is valuable for products where visual identity is critical. But it can frustrate users who expect platform-standard behaviors.
- iOS users expect swipe-back gestures, spring animations, and native text selection handles. Flutter and React Native can approximate these, but the behavior is never pixel-perfect with the platform implementation.
- Android users expect the back button to work consistently, material ripple effects, and share intents. Cross-platform frameworks handle most of these but edge cases — like accessibility services or third-party keyboard integrations — frequently reveal differences.
- Navigation patterns diverge significantly. iOS uses navigation bars with back buttons and push transitions. Android uses a hardware or software back button and increasingly relies on predictive back gestures. Cross-platform navigation libraries abstract these differences, but the abstraction is imperfect.
- Accessibility is where the gap is widest. Every cross-platform framework has improved accessibility support, but native screen readers, dynamic type, and assistive technologies behave differently on each platform. Achieving platform-quality accessibility requires platform-specific work regardless of your framework choice.
The pragmatic approach is to decide which platform conventions are essential for your users and which can be sacrificed for brand consistency. A banking app needs standard text inputs and keyboard behaviors because users expect them to work with password managers and autofill. A media consumption app can afford more custom UI because users are focused on content, not navigation conventions.
State management across platforms
State management patterns differ significantly across cross-platform frameworks. React Native inherits React's ecosystem, which means you have choices like Redux Toolkit, Zustand, Jotai, and MobX. Each has its own mental model, and the fragmentation can be a source of confusion. However, the React community has converged around a few patterns — global stores for server state, local state with hooks for UI state, and URL-driven navigation state for routing.
Flutter's state management landscape is more fragmented. The framework provides setState for simple cases, but real apps need something more structured. Provider, Riverpod, BLoC, and GetX compete for mindshare, with none achieving the dominance that Redux once held in the React world. Riverpod has emerged as the community favorite for new projects — it is type-safe, testable, and does not depend on the widget tree. But the fragmentation means that moving between Flutter projects often requires learning a new state management paradigm.
Kotlin Multiplatform sidesteps this question by delegating state management to the platform. Shared Kotlin code exposes state flows that the platform UI observes. On Android, this integrates naturally with Jetpack Compose's state model. On iOS, SwiftUI observations or Combine publishers connect to KMP flows. The shared code handles the business logic and data flow; the platform decides how to render it.
Regardless of framework, the most important state management decision you will make is how to handle server state. Every mobile app fetches data from APIs, and the pattern you choose for caching, refetching, optimistic updates, and offline support will have more impact on your app's quality than which state management library you pick. React Query (TanStack Query), SWR, and similar tools are available across frameworks and are universally recommended over hand-rolled caching solutions.
Navigation
Navigation is one of the most platform-specific concerns in mobile development, and it is where cross-platform abstractions are most likely to fail. iOS and Android have fundamentally different navigation metaphors. iOS uses a navigation stack with push/pop transitions. Android historically used an activity-based system and has moved toward a single-activity architecture with Jetpack Navigation Compose. Cross-platform frameworks must reconcile these differences, and the results are never perfect.
React Navigation is the de facto standard for React Native. It supports stack, tab, drawer, and modal navigators with platform-adaptive transitions. The library has matured significantly and handles deep linking, state persistence, and screen tracking reliably. The main criticism is that it adds a significant JavaScript bundle size — roughly 100 KB gzipped — and that custom transitions require native code.
GoRouter is Flutter's recommended navigation solution. It provides declarative routing with type-safe parameters, deep linking support, and shell routes for persistent layouts. Flutter's navigation replaced a notoriously confusing Navigator 1.0 API, and GoRouter represents a dramatic improvement. It is less flexible than React Navigation for complex transition animations but more predictable for standard use cases.
Testing strategies for cross-platform apps
Testing a cross-platform app introduces complexity that pure native development does not. You need to test the shared code, the platform integration layer, and the platform-specific UI — and the boundaries between these layers are where bugs hide.
For shared business logic, unit testing is straightforward regardless of framework. React Native uses Jest, Flutter uses flutter_test, and KMP uses kotlin.test. The key is structuring your code so that business logic is isolated from framework and platform dependencies. Dependency injection is not optional for testability — it is the difference between testing your logic in milliseconds versus minutes.
UI testing is where cross-platform frameworks diverge. React Native has React Native Testing Library for component tests and Detox for end-to-end tests. Flutter has integration_test, which runs on real devices and simulators and shares the same API as widget tests. KMP leaves UI testing to the platform — you test with XCUITest on iOS and Espresso on Android. Flutter's unified testing story is arguably the strongest, since the same test code runs on both platforms.
Snapshot testing is particularly valuable for cross-platform apps. A single API call should produce the same data model on both platforms, and snapshot tests catch regressions where the data shape diverges. Tools like react-native-screenshots and Flutter's golden file testing allow you to visually verify that UI components render correctly across platforms, which is especially important when platform theme differences cause unexpected layout changes.
App store deployment and CI/CD
Getting a cross-platform app to the store involves all the same steps as native development — code signing, provisioning profiles, app store metadata, review — plus the additional complexity of building for two platforms from a shared codebase. The tooling has improved dramatically, but it is not yet seamless.
Expo Application Services provides the most polished CI/CD experience for React Native. EAS Build handles both iOS and Android builds from a single configuration file. EAS Submit manages store uploads for both platforms. EAS Update handles over-the-air updates. For teams already using Expo, the deployment pipeline is effectively a solved problem — you can go from git push to store submission without touching Xcode or Android Studio.
Flutter's build tooling is built into the framework. The flutter build command generates platform-specific bundles, and Codemagic provides a CI/CD service specifically optimized for Flutter. Fastlane integration works well for both platforms. The main challenge with Flutter deployment is binary size — the Flutter engine adds approximately 15-25 MB to each platform binary, which can be a concern for markets where app size limits are strict or where users have limited storage.
KMP's CI/CD is the most complex because it involves two platform-specific build pipelines. The shared code compiles to a native library that is consumed by the iOS and Android projects. This means your CI pipeline must build the KMP shared module, then build the iOS app and the Android app separately. Gradle handles the Android side well, but the iOS integration requires running Xcode build steps, which typically means macOS-based CI runners.
When to choose native instead
Cross-platform frameworks have improved to the point where native-only development is no longer the default choice for most apps. But there are clear cases where native development remains the better option. Recognizing these cases before you commit to a framework saves significant rework.
- Your app is the platform — if you are building a camera app, a video editor, or a game, the abstraction layers of cross-platform frameworks add latency and complexity that directly impact your core value proposition.
- You need deep platform integration — apps that use ARKit, CoreML, HealthKit, or other platform-specific APIs extensively will spend more time writing native modules than they save through code sharing.
- Your team is already platform-native — if you have experienced iOS and Android developers who are productive in Swift and Kotlin, cross-platform frameworks add training costs and abstraction overhead without clear benefits.
- Performance is your differentiator — if your app processes sensor data at high frequency, renders complex 3D scenes, or needs to start in under 200 milliseconds, native code gives you the most direct path to meeting those requirements.
- You need platform-quality accessibility — cross-platform accessibility has improved, but native screen readers, dynamic type, and assistive technology support remain more consistent on each platform's native SDK.
The decision is not permanent. Several successful apps have started with a cross-platform framework and migrated to native as their user base grew and their requirements became more platform-specific. The reverse is also common — apps that prototype with Flutter or React Native to validate the market, then invest in native for the production version. The key is understanding that your choice is not a lifetime commitment.
Start with the framework that lets you ship the fastest, then rewrite the parts that matter in native code when the data tells you to. The perfect architecture is the one you can afford to iterate on.
Making the right choice for your team
After reviewing all the technical dimensions, the most important factor in your decision is your team. A framework that your team is productive in will outperform a technically superior framework that your team struggles with. This is not an argument for staying with what you know — it is an argument for being honest about the learning curve and the time it takes to reach production velocity.
If your team is primarily web developers with React experience, React Native is the natural choice. The mental model transfers almost directly, and the migration path from a web app to a mobile app is well documented. Your team will be productive in weeks rather than months. The risk is that they will treat mobile development as web development with a different render target, missing platform-specific concerns like memory pressure, battery usage, and gesture handling.
If your team is starting fresh or comes from a design-forward background, Flutter offers the most polished developer experience. Dart is easy to learn, Flutter's tooling is excellent, and the framework's built-in design system allows teams to ship beautiful, consistent UIs without platform expertise. The risk is that Flutter's ecosystem is smaller than React Native's, and finding packages for niche requirements can be harder.
If your team has strong Android and iOS expertise and wants to share business logic without compromising on platform UI, Kotlin Multiplatform is the strategic choice. The shared code layer eliminates duplication without imposing a foreign UI paradigm on either platform. The risk is that you need expertise in three areas (Kotlin, Swift, and Android) rather than two.
Regardless of your choice, invest in your infrastructure early. Cross-platform development amplifies the importance of good CI/CD, automated testing, and reproducible builds. A single code change can break two platforms simultaneously, and manual testing for both platforms quickly becomes a bottleneck. The teams that succeed with cross-platform development are the ones that treat their build pipeline and test suite as first-class concerns, not afterthoughts.
The cross-platform landscape in 2026 is more forgiving than it has ever been. The major frameworks are stable, well-documented, and proven at scale. The differences between them have narrowed, and the remaining gaps are well understood. Your job is not to find the objectively best framework — that framework does not exist. Your job is to find the best fit for your team, your product, and your users. The rest is implementation.
