Cross-Platform Mobile Development
Choose and implement the right cross-platform mobile framework. Covers React Native, Flutter, Kotlin Multiplatform, architecture differences, performance implications, native bridge patterns, and the decision framework for platform selection.
Cross-platform mobile development promises one codebase for iOS and Android. The reality is more nuanced — frameworks differ dramatically in architecture, performance characteristics, and native integration depth. Choosing the wrong framework can cost more than building natively.
Framework Comparison
| Aspect | React Native | Flutter | Kotlin Multiplatform |
|---|---|---|---|
| Language | JavaScript/TypeScript | Dart | Kotlin |
| UI rendering | Native components | Custom (Skia/Impeller) | Native (shared logic only) |
| Code sharing | 90-95% (including UI) | 95-98% (including UI) | 50-70% (logic only) |
| Performance | Near-native (with JSI) | Near-native | Native |
| Hot reload | Yes | Yes (faster) | Limited |
| App size | +7MB baseline | +5MB baseline | +1-2MB |
| Ecosystem | NPM (massive) | pub.dev (growing) | Maven/Gradle (mature) |
| Learning curve | Low (if JS/React) | Medium (new language) | Medium (if Kotlin) |
| Company | Meta | JetBrains |
React Native (New Architecture)
// React Native with JSI (direct native binding, no bridge)
import { TurboModuleRegistry, TurboModule } from 'react-native';
// New Architecture: Fabric (UI) + TurboModules (native)
interface Spec extends TurboModule {
getDeviceId(): string;
processPayment(amount: number, currency: string): Promise<PaymentResult>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('PaymentModule');
// Component with new Fabric renderer
const OrderCard: React.FC<{ order: Order }> = ({ order }) => {
return (
<View style={styles.card}>
<Text style={styles.title}>Order #{order.id}</Text>
<Text style={styles.amount}>${order.total.toFixed(2)}</Text>
<Pressable
style={styles.button}
onPress={() => handleConfirm(order)}
android_ripple={{ color: 'rgba(0,0,0,0.1)' }}
>
<Text style={styles.buttonText}>Confirm</Text>
</Pressable>
</View>
);
};
Flutter
// Flutter: Custom rendering engine (Skia/Impeller)
class OrderScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Orders')),
body: StreamBuilder<List<Order>>(
stream: orderRepository.watchAll(),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final order = snapshot.data![index];
return OrderCard(
order: order,
onConfirm: () => _confirmOrder(order),
);
},
);
},
),
);
}
}
// Platform channels for native functionality
class PaymentChannel {
static const platform = MethodChannel('com.app/payment');
Future<String> processPayment(double amount) async {
final result = await platform.invokeMethod('processPayment', {
'amount': amount,
'currency': 'USD',
});
return result;
}
}
Kotlin Multiplatform
// Shared module (used by both iOS and Android)
// Only business logic is shared, UI is native per platform
class OrderRepository(
private val api: OrderApi,
private val database: OrderDatabase,
private val dispatcher: CoroutineDispatcher
) {
suspend fun getOrders(): List<Order> = withContext(dispatcher) {
try {
val remote = api.fetchOrders()
database.saveOrders(remote)
remote
} catch (e: Exception) {
database.getOrders() // Offline fallback
}
}
fun watchOrders(): Flow<List<Order>> = database.observeOrders()
}
// Expect/actual for platform-specific implementations
expect class PlatformPayment() {
fun processPayment(amount: Double): PaymentResult
}
// Android: actual implementation
actual class PlatformPayment {
actual fun processPayment(amount: Double): PaymentResult {
// Google Pay integration
}
}
// iOS: actual implementation (in Kotlin, compiled to iOS framework)
actual class PlatformPayment {
actual fun processPayment(amount: Double): PaymentResult {
// Apple Pay integration
}
}
Decision Framework
Small team, web developers → React Native
Leverage existing React/JS skills
Large ecosystem, many libraries
Pixel-perfect custom UI, performance-critical → Flutter
Custom rendering = identical UI on both platforms
Best hot reload experience
Existing native apps, share business logic → Kotlin Multiplatform
Keep native UI, share 50-70% logic
Gradual adoption, no rewrite needed
Maximum native performance/UX → Native (Swift + Kotlin)
Full platform API access, best performance
2x development cost
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Choosing cross-platform for 1 platform | Unnecessary abstraction | Go native if targeting single platform |
| Ignoring platform conventions | App feels foreign on both platforms | Platform-specific UI adaptations |
| Too many native bridges | Performance bottleneck, complexity | Batch native calls, use new architecture |
| Cross-platform for heavy GPU/3D | Poor performance, limitations | Native or game engines (Unity) |
| Not measuring real-world perf | Assumptions instead of data | Profile on actual target devices |
Cross-platform is not free — it trades native platform depth for development velocity. The right choice depends on your team, your app complexity, and how much platform-specific behavior matters.