Jetpack Compose vs Flutter — How to Answer Without Sounding Biased
A senior engineer's balanced take on declarative UI, state, navigation, and ecosystem fit
Open interactive version (quiz + challenge)Real-world analogy
Asking 'Compose or Flutter?' is like asking whether a Toyota built in Japan or Germany is better. Both are excellent cars. The smart answer is: it depends on the road, the team, and how far you need to go.
What is it?
Jetpack Compose is Google's native Android declarative UI toolkit built in Kotlin. Flutter is Google's cross-platform UI SDK built in Dart. Both use a reactive, widget/composable-based model, but they differ fundamentally in rendering strategy, ecosystem integration, and target platforms.
Real-world relevance
Google's own apps — Google Pay, Maps, and Messages — use Jetpack Compose. eBay's mobile app, Alibaba's Xianyu, and BMW's companion app use Flutter. Neither choice is 'wrong' at scale; the decision is architectural, not tribal.
Key points
- Declarative UI — same idea, different DNA — Both Compose and Flutter use a declarative model where UI is a function of state. Compose builds on Kotlin and integrates natively with the Android SDK. Flutter builds its own widget tree in Dart and bypasses native UI components entirely, rendering through Skia/Impeller.
- State — remember vs StatefulWidget/BLoC — Compose uses remember{} and rememberSaveable{} for local state, and ViewModel + StateFlow/MutableState for screen-level state. Flutter uses StatefulWidget with setState() for local state, and BLoC, Riverpod, or Provider for structured state management — architecturally equivalent roles.
- Navigation — NavHost vs Navigator 2.0 / GoRouter — Compose Navigation uses NavController and NavHost with typed routes. Flutter Navigator 2.0 is powerful but verbose; GoRouter (now officially recommended by the Flutter team) provides a declarative URL-based API close in spirit to Compose Navigation.
- Theming — MaterialTheme in both, but different depths — Compose uses MaterialTheme with colorScheme, typography, and shapes — fully composable and type-safe. Flutter's ThemeData is a large flat object. Material 3 is supported in both but more naturally expressed in Compose because the Kotlin type system maps to M3 color roles precisely.
- Performance — two different rendering models — Flutter skips native views and renders everything on its own GPU canvas, giving pixel-perfect consistency across platforms. Compose uses native Android Views under the hood (via Canvas/RenderNode) which integrates seamlessly with accessibility, IME, and system gestures. Neither is universally faster — profile before claiming.
- Interoperability — native or cross-platform? — Compose interops with Views (AndroidView, ComposeView), Camera2, Maps SDK, and all Jetpack libraries out of the box. Flutter communicates with native Android/iOS code via Platform Channels, which adds serialization overhead for anything beyond basic calls.
- Ecosystem — Jetpack vs pub.dev — Compose inherits the entire Jetpack ecosystem (Room, WorkManager, CameraX, Health Connect) with zero friction. Flutter's pub.dev packages vary widely in quality and maintenance; platform-specific features often require writing native plugins.
- Hiring & team fit — Compose requires Kotlin expertise — a stronger hire bar for Android. Flutter requires Dart, which is easy to learn but less common. If a team already has strong Android engineers, Compose is faster to adopt. A cross-platform startup targeting Android + iOS with one team often picks Flutter.
- When to pick Compose — Single-platform Android app, needs deep OS integration (biometrics, NFC, health data), team is Kotlin-fluent, app must follow Android platform conventions (edge-to-edge, predictive back, Material You).
- When to pick Flutter — True cross-platform (Android + iOS, optionally web/desktop), pixel-perfect custom UI that must look identical on all platforms, startup with a small team, game-like or highly animated interfaces.
- The interview answer template — State: 'Both are excellent declarative UI frameworks. I prefer Compose for native Android because [reason]. I'd choose Flutter when [cross-platform scenario]. The decision should be driven by team skills, platform needs, and long-term maintainability — not trend.' This shows maturity.
- 2026 landscape — Compose is now the default Android UI recommendation from Google, with Compose Multiplatform (JetBrains) extending it to iOS/desktop. Flutter continues to grow, especially in emerging markets and fintech (Google Pay, Alibaba Xianyu). Both are production-grade.
Code example
// ── Jetpack Compose — simple counter screen ──────────────────
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsStateWithLifecycle()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Count: $count",
style = MaterialTheme.typography.headlineMedium
)
Spacer(Modifier.height(16.dp))
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}
}
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() { _count.update { it + 1 } }
}
// ── Flutter equivalent ────────────────────────────────────────
// class CounterScreen extends StatefulWidget { ... }
// class _CounterScreenState extends State<CounterScreen> {
// int _count = 0;
// void _increment() => setState(() => _count++);
//
// Widget build(BuildContext context) => Column(children: [
// Text('Count: $_count'),
// ElevatedButton(onPressed: _increment, child: Text('Increment')),
// ]);
// }
//
// With BLoC:
// BlocBuilder<CounterBloc, CounterState>(
// builder: (context, state) => Text('Count: ${state.count}'),
// )Line-by-line walkthrough
- 1. CounterScreen is a @Composable function — it describes UI as a function of state, not imperative commands
- 2. viewModel() retrieves or creates a CounterViewModel scoped to the current NavBackStackEntry, surviving rotation
- 3. collectAsStateWithLifecycle() converts StateFlow into Compose State, pausing collection when the app is in the background — battery-safe
- 4. Column with fillMaxSize, verticalArrangement, and horizontalAlignment replaces XML LinearLayout + gravity attributes
- 5. MaterialTheme.typography.headlineMedium is a Material 3 type role — guaranteed contrast and scale across the design system
- 6. Button's onClick lambda calls viewModel.increment(), keeping business logic out of the composable
- 7. CounterViewModel extends ViewModel — survives configuration changes automatically via ViewModelStore
- 8. MutableStateFlow(0) holds the current count; asStateFlow() exposes a read-only StateFlow to the UI
- 9. update{} is atomic and thread-safe — correct even if increment() is called from a background thread
- 10. The Flutter equivalent in comments shows setState() for local state and BlocBuilder for BLoC — structurally identical roles but different APIs
Spot the bug
@Composable
fun ProductListScreen() {
var products by remember { mutableStateOf(emptyList<Product>()) }
LaunchedEffect(Unit) {
products = fetchProductsFromNetwork() // suspend fun
}
LazyColumn {
items(products) { product ->
Text(product.name)
}
}
}Need a hint?
Think about what happens to 'products' when the user rotates the device. Is remember enough here?
Show answer
The bug is that remember{} does not survive configuration changes (screen rotation, locale change). When the device rotates, the composable is recomposed from scratch, remember{} starts with emptyList() again, and LaunchedEffect re-fires, causing a redundant network call and a visible blank flash. Fix: move the state and network call into a ViewModel. Use a MutableStateFlow<List<Product>> in the ViewModel and collect it with collectAsStateWithLifecycle() in the composable. The ViewModel survives rotation; the network call is only made once.
Explain like I'm 5
Compose is like building with Lego made specifically for your bedroom floor — it fits perfectly and works with all your other Lego sets. Flutter is like bringing a special Lego kit that works in any room of any house, but you need an adapter to use your other bedroom Lego with it.
Fun fact
Compose's compiler plugin transforms @Composable functions into a call graph that the Compose runtime can skip (recompose) selectively. Flutter's widget rebuild works similarly but must diff the entire subtree. Both arrived at the same performance insight independently.
Hands-on challenge
Implement a minimal two-screen app in Compose (a list screen and a detail screen) using NavController. Then sketch in comments how the equivalent GoRouter setup would look in Flutter. Identify two architectural differences between the two navigation approaches.
More resources
- Jetpack Compose — Official Docs (Android Developers)
- Flutter — Official Docs (flutter.dev)
- GoRouter for Flutter (pub.dev)
- Compose Multiplatform (JetBrains)
- Compose vs Flutter — Android Developers Blog (Google)