Lesson 75 of 77 advanced

KMP vs Flutter — Strategic Comparison for Senior Engineers

Choose the Right Cross-Platform Strategy for Your Team

Open interactive version (quiz + challenge)

Real-world analogy

Imagine you're a restaurant chain expanding to 10 cities. Flutter is like building identical franchise restaurants everywhere — same kitchen, same decor, same menu, all controlled from headquarters. Fast to roll out, but every location looks the same. KMP (Kotlin Multiplatform) is like keeping each city's unique local restaurant but sharing the same supply chain, recipes, and accounting system behind the scenes — customers get a native experience, but the business logic is unified. Neither approach is wrong — it depends on whether your customers care more about consistency or local flavor.

What is it?

KMP (Kotlin Multiplatform) and Flutter represent two fundamentally different approaches to cross-platform development. Flutter shares everything (UI + logic) using its own rendering engine and Dart language. KMP shares business logic in Kotlin while keeping native UI (Jetpack Compose + SwiftUI). This lesson provides a senior engineer's framework for choosing between them based on team composition, product requirements, existing codebase, and long-term strategy.

Real-world relevance

This comparison matters in real engineering decisions: startups choosing their initial tech stack, enterprises migrating legacy native apps, CTOs planning team structure, and senior engineers advising on architecture. Companies like Google actually use both — KMP for some Android shared logic and Flutter for consumer apps like Google Pay. The answer is never 'X is always better' — it's 'X is better for YOUR situation.'

Key points

Code example

// ============================================
// Side-by-side: Same feature in KMP vs Flutter
// ============================================

// --- KMP Shared Module (Kotlin) ---
// commonMain/src/UserRepository.kt
//
// class UserRepository(
//     private val api: UserApi,
//     private val cache: UserCache,
// ) {
//     suspend fun getUser(id: String): Result<User> {
//         return cache.get(id) ?: api.fetchUser(id).also {
//             cache.put(id, it)
//         }
//     }
// }
//
// // expect declaration — platform provides actual
// expect class UserCache() {
//     fun get(id: String): User?
//     fun put(id: String, user: User)
// }

// --- KMP iOS UI (Swift) ---
// struct UserProfileView: View {
//     @StateObject var viewModel = UserViewModel()
//     var body: some View {
//         NavigationView {
//             // Fully native SwiftUI — uses iOS conventions
//         }
//     }
// }

// --- KMP Android UI (Kotlin) ---
// @Composable
// fun UserProfileScreen(viewModel: UserViewModel) {
//     // Fully native Jetpack Compose — Material 3
// }

// ============================================
// --- Flutter Equivalent (Dart) ---
// ============================================

// Shared repository (same logic, one language)
class UserRepository {
  final UserApi _api;
  final UserCache _cache;

  UserRepository({required UserApi api, required UserCache cache})
      : _api = api,
        _cache = cache;

  Future<User> getUser(String id) async {
    final cached = _cache.get(id);
    if (cached != null) return cached;

    final user = await _api.fetchUser(id);
    _cache.put(id, user);
    return user;
  }
}

// Shared UI (one codebase, all platforms)
class UserProfileScreen extends StatelessWidget {
  final String userId;

  const UserProfileScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: FutureBuilder<User>(
        future: context.read<UserRepository>().getUser(userId),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}'),
            );
          }
          final user = snapshot.data!;
          return _buildProfile(user);
        },
      ),
    );
  }

  Widget _buildProfile(User user) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        CircleAvatar(radius: 50, backgroundImage: NetworkImage(user.avatarUrl)),
        const SizedBox(height: 16),
        Text(user.name, style: const TextStyle(fontSize: 24)),
        Text(user.email, style: const TextStyle(color: Colors.grey)),
        const SizedBox(height: 24),
        // Platform-adaptive: Material on Android, Cupertino on iOS
        if (Platform.isIOS)
          CupertinoButton(
            child: const Text('Edit Profile'),
            onPressed: () => _editProfile(user),
          )
        else
          ElevatedButton(
            onPressed: () => _editProfile(user),
            child: const Text('Edit Profile'),
          ),
      ],
    );
  }

  void _editProfile(User user) {
    // Navigate to edit screen
  }
}

// ============================================
// Decision Helper — Use in architecture docs
// ============================================
enum ProjectCharacteristic {
  newGreenfield,
  existingNativeApps,
  smallTeam,
  separatePlatformTeams,
  customBrandedUI,
  platformNativeUICritical,
  needsWebDesktop,
  deepHardwareIntegration,
  rapidPrototyping,
  enterpriseCompliance,
}

String recommendApproach(Set<ProjectCharacteristic> traits) {
  int flutterScore = 0;
  int kmpScore = 0;

  final scoring = {
    ProjectCharacteristic.newGreenfield: (2, 0),
    ProjectCharacteristic.existingNativeApps: (0, 3),
    ProjectCharacteristic.smallTeam: (3, 0),
    ProjectCharacteristic.separatePlatformTeams: (0, 3),
    ProjectCharacteristic.customBrandedUI: (3, 1),
    ProjectCharacteristic.platformNativeUICritical: (0, 3),
    ProjectCharacteristic.needsWebDesktop: (3, 1),
    ProjectCharacteristic.deepHardwareIntegration: (0, 2),
    ProjectCharacteristic.rapidPrototyping: (3, 0),
    ProjectCharacteristic.enterpriseCompliance: (1, 2),
  };

  for (final trait in traits) {
    final (f, k) = scoring[trait]!;
    flutterScore += f;
    kmpScore += k;
  }

  if (flutterScore > kmpScore + 3) return 'Strong Flutter recommendation';
  if (kmpScore > flutterScore + 3) return 'Strong KMP recommendation';
  if (flutterScore > kmpScore) return 'Slight Flutter lean — evaluate team skills';
  if (kmpScore > flutterScore) return 'Slight KMP lean — evaluate team skills';
  return 'Either works — decide based on team expertise';
}

Line-by-line walkthrough

  1. 1. The KMP shared module shows UserRepository in Kotlin — this exact code compiles for both Android and iOS. The business logic (fetch, cache) is written once.
  2. 2. The expect class UserCache declares what's needed without implementation — each platform provides its actual version (NSUserDefaults on iOS, SharedPreferences on Android).
  3. 3. KMP's iOS UI is pure SwiftUI — it calls the shared Kotlin module but renders with fully native iOS components and navigation patterns.
  4. 4. KMP's Android UI is pure Jetpack Compose — same shared logic, native Material 3 rendering. Two UI codebases, one logic layer.
  5. 5. Flutter's UserRepository is functionally identical to KMP's — similar syntax (Dart and Kotlin are both modern languages), but it's one codebase for everything.
  6. 6. Flutter's UserProfileScreen is shared across ALL platforms — one Widget tree renders on iOS, Android, web, and desktop.
  7. 7. The Platform.isIOS check shows Flutter's approach to platform adaptation — you CAN make things look native, but it requires explicit conditional code.
  8. 8. The decision helper enum codifies project characteristics that influence the Flutter vs KMP choice — this is useful for real architecture discussions.
  9. 9. The scoring system weights each characteristic — smallTeam and rapidPrototyping strongly favor Flutter, while existingNativeApps and separatePlatformTeams favor KMP.
  10. 10. The recommendation function returns nuanced advice — it acknowledges that close scores mean team expertise should be the tiebreaker, not framework features.

Spot the bug

// Common mistakes in cross-platform architecture decisions

// Bug 1: Assuming Flutter can't look native
class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Always using Material, even on iOS
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () {},
            child: Text('Settings'),
          ),
        ],
      ),
    );
  }
}

// Bug 2: Wrong KMP assumption — sharing UI code
// Expecting KMP to share this Composable on iOS
// @Composable
// fun SharedScreen() {
//     MaterialTheme { Text("This runs on iOS too!") }
//     // ^ This only works with Compose Multiplatform (beta on iOS)
//     // Standard KMP does NOT share UI
// }

// Bug 3: Ignoring platform channels exist
// "Flutter can't access native APIs"
// This is wrong — platform channels bridge any native API
Need a hint?
Three misconceptions are embedded as code: Flutter platform adaptation, KMP UI sharing scope, and Flutter native API access.
Show answer
1) Flutter CAN look native — use flutter_platform_widgets or conditional rendering with Platform.isIOS to show CupertinoNavigationBar, CupertinoButton, etc. The code always uses Material which looks wrong on iOS. 2) Standard KMP does NOT share UI — only Compose Multiplatform (beta on iOS) can share Composables. Confusing KMP with Compose Multiplatform is a common interview mistake. 3) Flutter absolutely CAN access native APIs via platform channels, FFI, and the plugin ecosystem. Saying 'Flutter can't access native APIs' is a misconception — it just requires a bridge layer.

Explain like I'm 5

Imagine you're building sandcastles on two different beaches. Flutter gives you one magic mold that stamps out the exact same castle on both beaches instantly — super fast! But it always looks the same. KMP is different — it lets you share the blueprint for the castle's foundation and tunnels, but on each beach, you hand-sculpt the towers and decorations to match what locals like. Beach A gets pointy towers, Beach B gets round ones. The foundations are identical, but each castle looks like it belongs on its beach.

Fun fact

Kotlin Multiplatform was announced in 2017 — the same year as Flutter's first alpha. They've been evolving in parallel for 8+ years. JetBrains (KMP's creator) actually built their own conference app in Flutter before switching it to Compose Multiplatform — proving that even framework creators acknowledge the competition. Meanwhile, Google officially endorses BOTH Flutter and KMP for different use cases within Android development.

Hands-on challenge

You're a senior engineer presenting to your CTO. Create a written technical decision document comparing Flutter vs KMP for your company's next project: a fintech app that needs to launch on iOS, Android, and web within 6 months. Your existing team has 2 Android (Kotlin) developers, 1 iOS (Swift) developer, and 2 full-stack developers. You have an existing Android app with 50K LOC. Include: 1) Architecture diagram for each approach, 2) Timeline estimate, 3) Team ramp-up plan, 4) Risk analysis, 5) Your recommendation with justification. Present trade-offs honestly.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery