DEV Community

Cover image for Flutter vs Native App Development: A Code Comparison for Developers
Synfinity Dynamics Pvt Ltd
Synfinity Dynamics Pvt Ltd

Posted on

Flutter vs Native App Development: A Code Comparison for Developers

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!')),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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!")
  }
}
Enter fullscreen mode Exit fullscreen mode

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),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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")
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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")
  }
}
Enter fullscreen mode Exit fullscreen mode

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?

🌐 https://www.synfinitydynamics.com/flutter-vs-native-app-development?utm_source=devto&utm_medium=article&utm_campaign=blog_distribution

Top comments (0)