If you've been wrestling with the "Flutter or native?" question, you're not alone. It's one of the most common architectural decisions in mobile development today. Rather than giving you another abstract pros-and-cons list, this article goes straight to the code comparing how you accomplish the same tasks in Flutter, Swift (iOS), and Kotlin (Android).
By the end, you'll have a practical feel for each approach, not just a checklist.
1. Hello World: The Starting Point
Every comparison starts here and even this simple example reveals philosophical differences.
Flutter (Dart)
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: "const Text('Hello Flutter')),"
body: const Center(child: Text('Hello, World!')),
),
);
}
}
Swift (iOS - SwiftUI)
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationTitle("Hello Swift")
}
}
}
Kotlin (Android - Jetpack Compose)
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MyApp() }
}
}
@Composable
fun MyApp() {
Scaffold(
topBar = { TopAppBar(title = { Text("Hello Kotlin") }) }
) {
Text("Hello, World!")
}
}
Key takeaway: Flutter uses a single widget tree for all platforms. SwiftUI and Jetpack Compose are structurally similar to Flutter's declarative model but they're platform-specific. With Flutter, you write once; with native, you write twice but stay idiomatic to each platform's ecosystem.
2. State Management
State is where things get interesting and divergent.
Flutter (with setState simple local state)
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Count: $_count', style: const TextStyle(fontSize: 24))),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _count++),
child: const Icon(Icons.add),
),
);
}
}
Swift (with @State and @ObservableObject)
import SwiftUI
import Combine
class CounterViewModel: ObservableObject {
@Published var count = 0
func increment() { count += 1 }
}
struct CounterView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
.font(.largeTitle)
Button("Increment") {
viewModel.increment()
}
.buttonStyle(.borderedProminent)
}
}
}
Kotlin (with ViewModel + StateFlow)
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count = _count.asStateFlow()
fun increment() { _count.value++ }
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Scaffold {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Count: $count", style = MaterialTheme.typography.headlineLarge)
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}
}
}
Key takeaway: Flutter's setState is the simplest starting point, but for larger apps you'd reach for Riverpod, Bloc, or Provider. Swift's @StateObject and Kotlin's StateFlow + ViewModel are idiomatic, well-supported, and deeply integrated with their respective toolchains.
3. Network Calls (Fetching JSON)
Real apps talk to APIs. Here's how each platform handles a basic GET request and JSON parsing.
Flutter (using http package)
import 'dart:convert';
import 'package:http/http.dart' as http;
class Post {
final int id;
final String title;
Post({required this.id, required this.title});
factory Post.fromJson(Map<String, dynamic> json) =>
Post(id: json['id'], title: json['title']);
}
Future<List<Post>> fetchPosts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts');
}
}
Swift (using URLSession + Codable)
import Foundation
struct Post: Codable {
let id: Int
let title: String
}
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Post].self, from: data)
}
Kotlin (using Retrofit + Coroutines)
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
data class Post(val id: Int, val title: String)
interface PostService {
@GET("posts")
suspend fun getPosts(): List<Post>
}
val retrofit = Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(PostService::class.java)
// In a ViewModel or coroutine scope:
viewModelScope.launch {
try {
val posts = service.getPosts()
// update state
} catch (e: Exception) {
// handle error
}
}
Key takeaway: Swift's Codable + async/await is arguably the cleanest of the three. Kotlin with Retrofit is excellent too Retrofit's interface-based approach is elegant for larger projects. Flutter's approach is explicit and portable, but JSON parsing is more manual unless you add code generation via json_serializable.
4. Navigation
Routing and navigation is one of the most commonly debated areas between Flutter and native.
Flutter (using Navigator 2.0 / go_router)
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
GoRoute(path: '/detail/:id', builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailScreen(id: id);
}),
],
);
// Navigate from a widget:
context.go('/detail/42');
Swift (NavigationStack - iOS 16+)
import SwiftUI
struct AppRoot: View {
var body: some View {
NavigationStack {
HomeView()
.navigationDestination(for: Int.self) { id in
DetailView(id: id)
}
}
}
}
struct HomeView: View {
var body: some View {
NavigationLink("Go to Detail", value: 42)
}
}
Kotlin (Jetpack Navigation Compose)
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable("home") {
HomeScreen(onNavigate = { id -> navController.navigate("detail/$id") })
}
composable("detail/{id}") { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
DetailScreen(id = id)
}
}
}
Key takeaway: SwiftUI's NavigationStack with typed values is the most type-safe of the three. Flutter's go_router is solid for cross-platform deep linking. Kotlin's Jetpack Navigation works well but string-based routes can be error-prone without the Safe Args plugin.
5. Platform-Specific Features (e.g., Camera)
Where native truly shines or where Flutter needs a bridge.
Flutter (using image_picker plugin)
import 'package:image_picker/image_picker.dart';
final picker = ImagePicker();
Future<void> pickImage() async {
final XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
// use image.path
}
}
Flutter doesn't call the camera directly it delegates to a plugin that wraps native platform code. Most common use cases are covered by packages on pub.dev, but for truly custom platform behavior, you write a Platform Channel.
Swift (native AVFoundation / PHPickerViewController)
import PhotosUI
import SwiftUI
struct ImagePickerView: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.filter = .images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, PHPickerViewControllerDelegate {
var parent: ImagePickerView
init(_ parent: ImagePickerView) { self.parent = parent }
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
results.first?.itemProvider.loadObject(ofClass: UIImage.self) { image, _ in
DispatchQueue.main.async {
self.parent.selectedImage = image as? UIImage
}
}
}
}
}
Kotlin (using ActivityResultContracts)
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.*
@Composable
fun ImagePickerScreen() {
var imageUri by remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri -> imageUri = uri }
Button(onClick = { launcher.launch("image/*") }) {
Text("Pick Image")
}
}
Key takeaway: Native code has direct, zero-abstraction access to platform APIs. Flutter's plugin ecosystem covers the most common needs well, but any bleeding-edge or custom hardware integration still requires writing native code and bridging it over. This is Flutter's most significant real-world limitation.
6. Build Size & Performance
This is hard to reduce to code, but worth addressing concretely:
| Metric | Flutter | Swift (iOS) | Kotlin (Android) |
|---|---|---|---|
| Minimum release APK/IPA | ~7–10 MB (APK) | ~1–3 MB | ~1–4 MB |
| Rendering engine | Skia / Impeller (custom) | Core Animation (native) | Skia / HW Canvas (native) |
| Startup time | Slightly slower (engine init) | Fast | Fast |
| Animations at 60/120fps | Excellent | Excellent | Excellent |
| Access to new OS APIs | Delayed (waiting for plugins) | Same day | Same day |
Flutter ships its own rendering engine, which is why it has a larger baseline size but also why it looks pixel-identical across platforms. Native apps lean on the OS renderer, making them leaner but more susceptible to platform-specific rendering quirks.
When to Choose Flutter
- You have one engineering team and need to ship to iOS + Android (and potentially web/desktop) simultaneously
- Your UI is primarily custom branded components, animations, games
- You want strong consistency across platforms without per-platform design work
- Your team is productive in Dart or willing to learn it
When to Choose Native
- Your app is deeply integrated with platform-specific APIs (ARKit, HealthKit, CoreML, Android Widgets, NFC, etc.)
- You need to adopt new OS features on day one (e.g., Dynamic Island, Live Activities)
- You have separate iOS and Android teams and can invest in platform-specific quality
- Performance-critical code where every millisecond matters (e.g., real-time audio, video processing)
Final Thoughts
Flutter and native development are not adversaries they're tools with different trade-offs. Flutter has matured enormously and is genuinely production-ready for the majority of apps. Native development remains the right call for apps deeply tied to platform capabilities or where platform-native UX is a product requirement.
The best advice: prototype a critical screen in both, show it to your team, and let the code quality and velocity tell you where to invest.
Read the Complete Guide
This article provides a developer-focused comparison. If you're evaluating Flutter and native development from a business perspective including cost, scalability, maintenance, and long-term ROI check out my complete guide:
📖 Flutter vs Native App Development: Which Is Better for Your Business in 2026?
Top comments (0)