Lesson 44 of 77 intermediate

Jetpack Compose vs Flutter — Tradeoffs & Interview Answers

Declarative UI comparison, state management, navigation, and when to choose each

Open interactive version (quiz + challenge)

Real-world analogy

Compose and Flutter are like two modern architecture firms — both use the same blueprint philosophy (declarative UI) but different materials. Compose uses Android's steel (Kotlin, JVM, full OS access). Flutter uses its own pre-fabricated panels (Dart, Skia/Impeller). The Compose building integrates perfectly with the local city. The Flutter building looks identical on every street in every city.

What is it?

Jetpack Compose is Android's modern declarative UI toolkit, analogous to Flutter in philosophy but native to the Android platform. Understanding the comparison is essential for senior hybrid Flutter+Android roles where you may need to justify technology choices, work with Compose screens embedded in Flutter, or advise on greenfield app architecture.

Real-world relevance

A SaaS collaboration platform deciding between Flutter and Compose for their Android/iOS apps: Flutter wins if they need iOS parity fast with one team. Compose wins if they are Android-only and need deep Android integration (Google account sign-in, Android widgets, Nearby API). A senior engineer can articulate both arguments with concrete tradeoffs.

Key points

Code example

// === COMPOSE STATE MANAGEMENT ===
@Composable
fun ClaimsScreen(viewModel: ClaimsViewModel = viewModel()) {
    // Collect StateFlow as Compose State — lifecycle-safe
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // Local state with remember
    var showFilter by remember { mutableStateOf(false) }

    Scaffold(topBar = { ClaimsTopBar() }) { padding ->
        when (uiState) {
            is ClaimsState.Loading -> CircularProgressIndicator()
            is ClaimsState.Success -> ClaimsList(
                claims = (uiState as ClaimsState.Success).claims,
                onClaimClick = viewModel::onClaimSelected
            )
            is ClaimsState.Error -> ErrorView(message = uiState.message)
        }
    }
}

// === FLUTTER EQUIVALENT ===
class ClaimsScreen extends StatelessWidget {
  const ClaimsScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ClaimsBloc, ClaimsState>(
      builder: (context, state) => Scaffold(
        appBar: const ClaimsTopBar(),
        body: switch (state) {
          ClaimsLoading() => const CircularProgressIndicator(),
          ClaimsSuccess(:final claims) => ClaimsList(claims: claims),
          ClaimsError(:final message) => ErrorView(message: message),
        },
      ),
    );
  }
}

// === COMPOSE NAVIGATION ===
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    NavHost(navController, startDestination = "claims") {
        composable("claims") { ClaimsScreen(navController) }
        composable("detail/{id}") { backStack ->
            val id = backStack.arguments?.getString("id")
            ClaimDetailScreen(id = id, navController = navController)
        }
    }
}

// === FLUTTER go_router EQUIVALENT ===
final router = GoRouter(routes: [
  GoRoute(path: '/claims', builder: (_, __) => const ClaimsScreen()),
  GoRoute(
    path: '/claims/:id',
    builder: (_, state) => ClaimDetailScreen(id: state.pathParameters['id']!),
  ),
]);

// === THEMING COMPARISON ===
// Compose
MaterialTheme(
  colorScheme = darkColorScheme(primary = Color(0xFFD9FE06)),
  typography = Typography(bodyLarge = TextStyle(fontFamily = FontFamily.Default)),
) { /* content */ }

// Flutter
MaterialApp(
  theme: ThemeData(
    colorSchemeSeed: const Color(0xFFD9FE06),
    useMaterial3: true,
    brightness: Brightness.dark,
  ),
)

Line-by-line walkthrough

  1. 1. collectAsStateWithLifecycle() converts StateFlow to Compose State — collection pauses when lifecycle drops below STARTED
  2. 2. remember { mutableStateOf(false) } — local state survives recomposition but is reset on configuration change (use rememberSaveable for persistence)
  3. 3. when(uiState) sealed class pattern — exhaustive state matching, identical philosophy to Flutter's switch on sealed classes (Dart 3+)
  4. 4. BlocBuilder in Flutter reads stream state and rebuilds only when state changes — analogous to Compose reading StateFlow
  5. 5. NavHost + composable routes — Compose's type-safe navigation graph, similar to go_router's GoRoute definitions
  6. 6. GoRoute with path parameters (:id) — Flutter's equivalent of Compose Navigation backstack arguments
  7. 7. MaterialTheme wrapping — Compose's theme propagation via CompositionLocal, equivalent to Flutter's ThemeData in MaterialApp
  8. 8. colorSchemeSeed in Flutter auto-generates Material 3 colour scheme from a seed colour — same concept as Compose's darkColorScheme

Spot the bug

@Composable
fun UserProfile(userId: String) {
    var userData by remember { mutableStateOf<User?>(null) }

    LaunchedEffect(Unit) {
        userData = repository.getUser(userId)
    }

    userData?.let { UserCard(it) }
}
Need a hint?
This works initially but has a subtle bug when the userId changes. What is it?
Show answer
Bug: LaunchedEffect(Unit) runs only once — when the composable first enters the composition. If userId changes (e.g., user navigates to a different profile), the LaunchedEffect does NOT re-run because the key is Unit (constant). Fix: use LaunchedEffect(userId) — the effect re-runs whenever userId changes, fetching the correct user data. This is a common Compose bug that mirrors Flutter's didUpdateWidget pattern where you must handle parameter changes explicitly.

Explain like I'm 5

Imagine two art studios. Compose is the studio inside Google's Android building — it has all the special Android tools right there on the shelves. Flutter is a travelling studio that brings its own identical tools and canvas to any building (Android, iPhone, web). If you only paint for the Android building, the built-in studio is convenient. If you paint for many buildings, bring your own studio.

Fun fact

Google uses both Flutter and Compose internally. Google Pay uses Flutter. Google Meet uses Kotlin/Compose. The YouTube Music Android app was one of Compose's early large-scale real-world tests. Google intentionally maintains both toolkits — Flutter for cross-platform reach, Compose for deep Android integration.

Hands-on challenge

You are advising a fintech company choosing between Flutter and Compose for a new app that targets Android (primary) and iOS (future, 18 months out), requires BankID integration (Android SDK available), and has a team of 2 Android developers and 1 Flutter developer. Write your recommendation with three specific tradeoffs.

More resources

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