These Flutter interview questions test whether a developer understands how Flutter actually renders and how to ship one codebase to iOS and Android without it feeling second-rate on either. They cover Dart 3 features (records, patterns, sealed classes), the widget, element and render object trees, state management with Riverpod and Bloc, platform channels and FFI, jank diagnosis, the Impeller renderer, and release engineering. At Ryz, recruiters source senior Flutter engineers from apps they have shipped to both stores, candidates complete structured NTRVSTA AI interviews on topics like these, and recruiters review each candidate before and after. AI scores are advisory, and the decisions belong to people.
Flutter is easy to start and hard to scale. The questions that separate seniors are about rebuild scope, async gaps, platform integration and profiling, not widget names. Choose five to seven per interview: two fundamentals to calibrate, then go deep in rendering and performance for anyone who will own a production app.
Ask candidates to sketch the widget tree for a screen from your own app and explain where state lives. Then use the exercise below to watch them build and profile something real.
Widgets are immutable configuration, cheap to create and rebuilt often. Elements are the long-lived instances that hold a widget's place in the tree, hold State for stateful widgets, and decide whether to update or replace children by comparing runtime type and key. Render objects do layout, painting and hit testing. Rebuilding a widget does not mean relayout or repaint unless the render object's properties change.
What a strong answer shows: They know why frequent rebuilds can be cheap and what actually costs time.
initState runs once, didChangeDependencies when inherited widgets change, didUpdateWidget when the parent rebuilds with new configuration, dispose at the end. After an await, the widget may have been disposed, so using context or calling setState can throw or act on a dead screen.
Future<void> _save() async {
await repository.save(form);
if (!context.mounted) return;
Navigator.of(context).pop();
}
What a strong answer shows: They check mounted after async gaps and react to didUpdateWidget when inputs change.
When stateful widgets of the same type change position in a list. Without keys, Flutter matches elements by position, so deleting the first item in a list of stateful text fields shifts the state: the second item shows the first item's text. A ValueKey from a stable ID fixes it. GlobalKey is for moving state across the tree or reaching a widget's state, and it is expensive.
What a strong answer shows: They understand element matching and use stable IDs, never indexes.
Flutter layout is "constraints go down, sizes go up, parent sets position". A Column gives children unbounded vertical space, and a ListView wants to be as tall as possible. Give it a bound with Expanded or a fixed height. shrinkWrap: true also works but lays out every item, which defeats lazy building.
Column(
children: [
const Header(),
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(item: items[i]),
),
),
],
)
What a strong answer shows: They explain the constraint model and know the cost of shrinkWrap.
A const widget is canonicalized at compile time, so on rebuild the framework sees the identical instance and can skip rebuilding that subtree. It also avoids allocations. Lints like prefer_const_constructors make this habitual.
What a strong answer shows: They link const to rebuild skipping, not just style.
The future is created inside build, so each rebuild passes a new Future and the builder restarts. Create it once in initState or a state management layer, or use a provider that caches the result.
What a strong answer shows: They know build must be side-effect free and can run many times.
It is the element, the widget's position in the tree. Lookups like Theme.of(context) walk up from that position to find the nearest inherited widget and register a dependency, so the widget rebuilds when it changes. Using a context from above a Scaffold to find the Scaffold fails for the same reason.
What a strong answer shows: They understand context as tree position, which explains many common errors.
Records return multiple values without a class, and patterns destructure them. Switch expressions with patterns replace chains of if checks, including on JSON maps.
(String, int) parseVersion(String s) {
final [name, number] = s.split('@');
return (name, int.parse(number));
}
String describe(Object json) => switch (json) {
{'type': 'user', 'name': String name} => 'User $name',
{'type': 'team', 'members': List m} when m.isNotEmpty => 'Team of ${m.length}',
_ => 'Unknown',
};
What a strong answer shows: Comfortable, readable use of patterns, including guards and the fact that a list pattern throws if the length does not match.
A sealed class makes the switch exhaustive, so adding a state forces every UI to handle it.
sealed class OrdersState {}
class OrdersLoading extends OrdersState {}
class OrdersLoaded extends OrdersState { OrdersLoaded(this.orders); final List<Order> orders; }
class OrdersError extends OrdersState { OrdersError(this.message); final String message; }
Widget build(BuildContext context) => switch (state) {
OrdersLoading() => const CircularProgressIndicator(),
OrdersLoaded(:final orders) => OrderList(orders: orders),
OrdersError(:final message) => ErrorView(message: message),
};
What a strong answer shows: They prefer exhaustive types over flags and nullable fields.
late moves a null check from compile time to runtime. Reading it before assignment throws a LateInitializationError, often in a code path that runs before initState finished or after a failed load. Use it for values assigned in initState or lazily computed fields, not to avoid thinking about nullability.
What a strong answer shows: Respect for sound null safety and limited use of escape hatches.
Dart runs UI code on one isolate, so heavy synchronous work blocks frames. Move it to a background isolate with Isolate.run (or compute), which copies the result back. For repeated work, keep a long-lived isolate and communicate through ports.
final orders = await Isolate.run(() {
final data = jsonDecode(body) as List<dynamic>;
return data.map((e) => Order.fromJson(e as Map<String, dynamic>)).toList();
});
What a strong answer shows: They know isolates do not share memory and that async alone does not move CPU work off the UI thread.
setState and ValueNotifier are fine for local widget state. Riverpod gives compile-safe dependency injection, caching, automatic disposal and easy testing with overrides. Bloc gives explicit events and states with strong conventions and traceability, which large teams often like. The best choice is the one the team applies consistently.
What a strong answer shows: They can argue both sides and do not mix three patterns in one app.
ref.watch in build subscribes and rebuilds on change. ref.read reads once, used in callbacks like button presses. Watching inside a callback, or reading in build, causes stale UI. Auto-disposing providers release their state when nothing listens, which prevents leaks but refetches data when the user returns unless you keep it alive deliberately.
What a strong answer shows: Precise rules and an understanding of cache lifetime.
Use a declarative router such as go_router: routes map URLs to screens, so deep links and web URLs work the same way. A redirect callback sends unauthenticated users to sign-in and back afterwards, refreshed when auth state changes. Configure Android App Links and iOS universal links so system links open the right route.
What a strong answer shows: URL-based navigation that keeps deep links and auth consistent.
Run in profile mode on a real, lower-end device, never debug mode. Open the DevTools performance view and check which thread misses the frame budget: the UI thread (build and layout too slow) or the raster thread (painting too expensive). Then use the timeline and "track widget rebuilds" to find the cause.
What a strong answer shows: Correct build mode and the UI versus raster distinction.
Impeller precompiles a smaller set of shaders at build time, which removes the first-run shader compilation jank that Skia suffered from. It is the default on iOS and on modern Android devices, with a fallback on older hardware. Some visual differences and edge cases still appear, so test effects, blurs and custom painters on both platforms after upgrades.
What a strong answer shows: They know why the renderer changed and test rendering after Flutter upgrades.
Opacity on a large subtree, ShaderMask, BackdropFilter, clips with anti-aliasing and anything that triggers saveLayer. Prefer FadeTransition or a color with alpha, apply effects to the smallest subtree, and wrap frequently changing areas in RepaintBoundary so they repaint alone.
What a strong answer shows: Knowledge of what actually costs GPU time, and a habit of verifying with the raster timeline.
Image decode size first: a 4000-pixel photo shown at 150 logical pixels still decodes at full size unless you pass cacheWidth or cacheHeight (or use ResizeImage). Check the image cache limits, use lazy builders, and look for controllers and stream subscriptions not cancelled in dispose with the DevTools memory view.
What a strong answer shows: They reason about decoded bitmap size and leak sources.
Use builder constructors so only visible items build, set itemExtent or prototypeItem when rows have fixed height so scrolling does not measure every row, keep item widgets cheap and const where possible, and avoid rebuilding the whole list when one item changes by scoping state per item.
What a strong answer shows: Specific list optimizations rather than generic advice.
Build with --obfuscate --split-debug-info=<dir> and upload the symbols to your crash reporter, tree-shake icons, compress assets, and ship Android App Bundles. Check the size analysis with --analyze-size to find large packages. Obfuscation is not security; keep secrets off the client.
What a strong answer shows: Release tooling knowledge and realistic security expectations.
Platform channels: MethodChannel for calls, EventChannel for streams. Pigeon generates type-safe Dart, Kotlin and Swift code from one schema, which avoids stringly typed method names. Handlers run on the platform main thread by default, so heavy native work must move to a background thread.
const _channel = MethodChannel('com.example/battery');
Future<int?> batteryLevel() async {
try {
return await _channel.invokeMethod<int>('getBatteryLevel');
} on PlatformException catch (e) {
log('battery failed: ${e.message}');
return null;
}
}
What a strong answer shows: They have written both sides, prefer Pigeon for anything non-trivial, and know the threading rules.
For a C or C-compatible library (image processing, crypto, a shared Rust core) called often or with large data. FFI calls are synchronous and avoid message serialization. ffigen generates bindings from headers. Native memory must be freed explicitly, and long calls should run in an isolate.
What a strong answer shows: They choose by call pattern and handle native memory safely.
Feature-first packages in a monorepo (Dart pub workspaces or Melos), a shared design system package, a core package for networking and storage, and clear dependency rules enforced in CI. Each feature exposes routes and providers through a small public API.
What a strong answer shows: They plan for build times, ownership and boundaries.
Add-to-app: embed a Flutter module in the iOS and Android projects. Prewarm a FlutterEngine to avoid a blank delay on first open, and use engine groups when showing multiple Flutter screens to share resources. Agree on navigation ownership and data passing through Pigeon.
What a strong answer shows: Awareness of startup cost and clear boundaries between native and Flutter.
Unit tests for logic, widget tests with provider or bloc overrides for screens, golden tests for key components with fonts loaded so they are stable across machines, and integration_test flows on real devices or emulators in CI. Mock platform channels in widget tests.
What a strong answer shows: A layered approach and experience with flaky golden tests.
Flavors on Android and schemes on iOS, configuration through --dart-define-from-file rather than hardcoded values, pinned Flutter versions per branch (FVM or a version file), and CI that builds both platforms, runs tests and uploads to TestFlight and Play internal testing.
What a strong answer shows: Reproducible builds and a release flow covering both stores.
Use semantic widgets and add Semantics labels to custom controls, respect text scaling, and test with VoiceOver and TalkBack. Use adaptive widgets or platform checks for things users notice (back gestures, date pickers, scroll physics) while sharing the rest.
What a strong answer shows: They test with screen readers and know where platform differences matter.
Upgrade on a branch soon after each stable release, read breaking changes, run dart fix, update packages and check abandoned ones, then run full tests and manual checks on rendering-heavy screens. Avoid falling several versions behind, which turns upgrades into projects.
What a strong answer shows: A routine for upgrades and attention to package health.
build.shrinkWrap: true as a general fix for layout errors.context after await without checking mounted.async moves CPU work off the UI thread.GlobalKey to pass data between widgets.Run a 90-minute pairing session on a starter app: a product catalog with search, a detail screen and a cart, backed by a fake API with latency. The candidate fixes a seeded jank problem (full-size images in a grid), moves a large JSON parse to an isolate, models the cart state with sealed classes, and adds one widget test.
If you want engineers who already pass this bar, hire senior Flutter developers through Ryz. They are the top 1% of the candidates we interview, they work on your team and in your repos and ship to both stores, and they work within ±1h of US time zones. See our vetting process for how they are screened, and our Flutter developer job description for scoping the role.
Seniors should be able to read and write enough Kotlin and Swift to build a plugin, debug a native crash, and configure signing on both platforms. They do not need to be full native engineers, but pure Dart experience becomes a ceiling in production apps.
Two or three. Dart 3 features like patterns and sealed classes show whether someone keeps up, but most hiring signal comes from rendering, state and platform questions.
Send a small app with a deliberate performance problem and ask the candidate to share their screen while profiling it in DevTools. How they choose the build mode and read the frame chart is more telling than any verbal explanation.
Questions we didn't answer? Email info@ryzlabs.com.