Widgets • State Management • Async & Isolates • Layout & Navigation • Performance • 2026

Flutter Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 37 min read

This page is for anyone facing a Flutter round, from a first mobile job to a senior app role. Most Flutter interviews open with widgets, the three trees, BuildContext and keys, then test state management and how you choose between setState, Provider, Riverpod and Bloc. After that come async code and isolates, layout constraints, navigation, platform channels and performance. Senior rounds add a real project story and a judgement call. Each question shows what the interviewer is checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then swap in your own projects.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Widgets & Trees 5 questions

Easy Technical round Fresher Practice question

1. When do you use a StatelessWidget and when a StatefulWidget? And where does the state of a StatefulWidget actually live?

What the interviewer is really testing:
Whether you know that widgets are immutable and that state lives in a separate State object, which explains most rebuild behaviour later.
Answer frame:

Stateless: output depends only on constructor arguments and inherited data; nothing changes inside it.

Stateful: it holds data that changes over its life, like a text field value or a toggle.

Where state lives: in the State object, which the element keeps across rebuilds while the widget itself is thrown away and recreated.

Sample spoken answer:

"I use a StatelessWidget when everything it shows comes from its constructor or from something above it like the theme. I switch to a StatefulWidget when the widget itself owns data that changes, like whether a panel is expanded or the current page of a carousel. The key point is that the widget is always immutable, even a stateful one. The changing data lives in a separate State object. When the parent rebuilds, Flutter creates a new widget instance, but the element in that spot keeps the same State object and just points it at the new widget. That's why a counter doesn't reset to zero every time the parent rebuilds. So I keep state as low in the tree as I can, and I don't make something stateful just because it takes a callback."

Red flag to avoid:

Saying a StatefulWidget is mutable, or that the State object is recreated every time the widget rebuilds.

They may ask next:
  • If a StatefulWidget receives a new value from its parent, how does its State find out?
  • Can a StatelessWidget ever rebuild? What makes it happen?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

2. Flutter talks about a widget tree, an element tree and a render tree. What does each one do, and why have three?

What the interviewer is really testing:
Whether you understand how Flutter can rebuild widgets constantly and still be fast, which is the base for reasoning about keys, BuildContext and performance.
Answer frame:

Widgets: cheap, immutable descriptions of what the UI should look like; recreated on every build.

Elements: the long-lived instances at each spot in the tree; they hold State, compare old and new widgets and decide what to update.

Render objects: do the expensive work of layout, painting and hit testing; kept and updated in place, not recreated.

Why three: rebuilding descriptions is cheap, so Flutter can rebuild often and only touch the costly layer where something really changed.

Sample spoken answer:

"Widgets are just configuration. They're small immutable objects, and Flutter creates new ones every time build runs. Elements are the real instances. Each element sits at one position in the tree, holds a reference to its current widget, and for stateful widgets it holds the State object. When a rebuild happens, the element compares the new widget with the old one. If the type and key match, it updates itself and passes the change down instead of tearing anything down. Render objects are the heavy layer. They handle layout, painting and hit testing, and they're only created by widgets like Padding or a Row, not by every widget. They're updated in place when their properties change. The split exists so that building is cheap. I can rebuild a whole screen of widgets and the framework only touches the render objects that actually need new layout or paint."

Red flag to avoid:

Saying Flutter repaints the whole screen from scratch on every setState, or that widgets are the objects doing layout.

They may ask next:
  • Which object is the BuildContext you receive in build?
  • When would an element be thrown away and recreated instead of updated?
  • Does every widget have a render object, and can you name one that does and one that does not?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

3. What is a BuildContext really, and why does something like Scaffold.of(context) sometimes fail to find the Scaffold?

What the interviewer is really testing:
Whether you know a context is a position in the tree and that lookups only search upward from it, which explains a very common class of errors.
Answer frame:

What it is: the element for this widget, meaning its location in the tree.

Lookups go up: of(context) methods walk up through ancestors only.

Common failure: using the context of the widget that builds the Scaffold, which sits above it; fix with a Builder or a separate child widget.

Async gaps: after an await, check the widget is still mounted before using context.

Sample spoken answer:

"A BuildContext is a handle to the widget's element, so it's really this widget's position in the tree. Calls like Theme.of or Navigator.of start at that position and search upward through the ancestors. That's why Scaffold.of can fail. If my build method returns the Scaffold and I use that same build method's context, I'm asking from a spot above the Scaffold, so there's nothing to find. The fix is to wrap the part that needs it in a Builder, or move it into its own widget, so it gets a context below the Scaffold. The other trap is using context after an await. By the time the future finishes, the user may have left the screen and the element may be unmounted, so I check mounted first before showing a snackbar or navigating."

Red flag to avoid:

Treating context as a global app object that can be passed around and used anywhere, at any time.

They may ask next:
  • Why is it a problem to store a BuildContext in a field and use it later?
  • Why can't you call an inherited lookup like Theme.of from initState?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

4. Walk me through the lifecycle of a State object. Where do you start a stream subscription, and where do you cancel it?

What the interviewer is really testing:
Whether you put setup and cleanup in the right lifecycle methods, which is how leaks and setState-after-dispose errors are avoided.
Answer frame:

Start: createState, then initState once, then didChangeDependencies, then build.

Updates: didUpdateWidget when the parent passes a new widget config; setState for own changes.

End: deactivate when removed from the tree, dispose when it is gone for good.

Rule: subscribe in initState, re-subscribe in didUpdateWidget if the source changed, cancel in dispose.

Sample spoken answer:

"The framework calls createState, then initState exactly once, which is where I start controllers and subscriptions. Next comes didChangeDependencies, which also runs later whenever an inherited widget I depend on changes, so that's the place for lookups like MediaQuery that I want to react to. Then build runs, and it can run many times. If the parent rebuilds and hands me a new widget of the same type, didUpdateWidget fires with the old widget, so I compare them and re-subscribe if the stream I was given changed. When the widget leaves the tree, deactivate runs, and if it isn't reinserted, dispose runs. That's where I cancel subscriptions and dispose controllers. If I skip that, the stream keeps calling setState on a dead State and I get an error, plus a memory leak."

Code:
class _PriceTagState extends State<PriceTag> {
  StreamSubscription<double>? _sub;
  double _price = 0;

  void _listen() =>
      _sub = widget.prices.listen((p) => setState(() => _price = p));

  @override
  void initState() {
    super.initState();
    _listen();
  }

  @override
  void didUpdateWidget(PriceTag oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.prices != widget.prices) { _sub?.cancel(); _listen(); }
  }

  @override
  void dispose() { _sub?.cancel(); super.dispose(); }

  @override
  Widget build(BuildContext context) => Text(_price.toStringAsFixed(2));
}
Red flag to avoid:

Starting subscriptions or API calls inside build, or never cancelling them in dispose.

They may ask next:
  • Why does super.initState() go first but super.dispose() go last?
  • Is build a good place to start a network call? Why not?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

5. What are keys for in Flutter? Give me a case where a list behaves wrongly without them.

What the interviewer is really testing:
Whether you understand how elements are matched to widgets on rebuild, and can tell which key type fits which problem.
Answer frame:

Matching rule: on rebuild, an element is reused if the new widget has the same runtime type and key.

The bug: reorder or remove stateful items with no keys and the state stays by position, so it ends up on the wrong item.

Fix: a ValueKey from a stable id on the top widget of each item.

Other keys: ObjectKey, UniqueKey, and GlobalKey for reaching a State or moving a subtree, used sparingly.

Sample spoken answer:

"Keys control how Flutter matches new widgets to existing elements. Without keys, it matches children by position and runtime type. Say I have a list of todo rows, each a StatefulWidget holding its own checkbox state. If I delete the first row with no keys, Flutter sees one fewer widget of the same type, reuses the first element for what used to be the second item, and drops the last element. The text updates because it comes from the new widget, but the checked state stays where it was, so the wrong row looks checked. Giving each row a ValueKey with the todo's id fixes it, because now the element follows its item. The key has to go on the outermost widget of each list item, the one that's a direct child of the list. GlobalKey is different: it's unique across the app and lets me reach a State, like a form's, but it's costly so I use it rarely."

Red flag to avoid:

Saying keys are needed on every widget, or putting the key on a widget deep inside the item instead of the item itself.

They may ask next:
  • Why is using the list index as a key usually no better than no key at all?
  • What goes wrong if you create a UniqueKey inside build?
  • When have you actually needed a GlobalKey?
Say it in 60 seconds

State Management 7 questions

Easy Technical round Fresher Practice question

6. What actually happens when you call setState? And what goes wrong if you call it after the widget is gone?

What the interviewer is really testing:
Whether you know setState only marks the widget dirty and schedules a rebuild, rather than rebuilding on the spot.
Answer frame:

Runs the callback: your change is applied right away, synchronously.

Marks dirty: the element is flagged and a frame is scheduled; build runs on that next frame.

Batched: several setState calls before the frame lead to one build.

After dispose: it is an error; check mounted after any await.

Sample spoken answer:

"setState runs the function I pass it straight away, then marks this widget's element as dirty and asks the engine for a new frame. It doesn't rebuild on the spot. On the next frame, Flutter rebuilds the dirty elements, so if I call setState three times in one handler I still get one build. Only this widget and what it returns rebuild, not the whole app. The callback should just change fields. It shouldn't be async, and I do the slow work before calling setState, not inside it. The classic problem is calling it after the widget has been disposed, usually when a network call returns after the user has left the screen. Flutter reports an error saying setState was called after dispose. The fix is checking mounted after the await, and also cancelling timers and subscriptions in dispose so they can't fire later."

Red flag to avoid:

Saying setState rebuilds the entire app, or that it rebuilds immediately inside the call.

They may ask next:
  • If setState only rebuilds this widget, why can it still feel slow on a big screen?
  • What happens if the callback you pass to setState is marked async?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

7. How does InheritedWidget pass data down the tree, and how does the Provider package build on top of it?

What the interviewer is really testing:
Whether you know the mechanism behind Theme.of and Provider, so you can predict which widgets rebuild when shared data changes.
Answer frame:

Lookup: a descendant calls dependOnInheritedWidgetOfExactType, gets the nearest one above and registers as a dependent.

Notify: when the inherited widget is replaced and updateShouldNotify returns true, only dependents rebuild.

Provider adds: creating and disposing the object, lazy creation, listening to a ChangeNotifier, and simple read or watch calls.

Sample spoken answer:

"An InheritedWidget sits high in the tree and holds some data. When a widget below calls dependOnInheritedWidgetOfExactType, which is what Theme.of does under the hood, it gets the nearest one above it quickly and gets registered as a dependent. When the inherited widget is rebuilt with new data, Flutter calls updateShouldNotify, and if that returns true, only the registered dependents rebuild, not everything in between. The catch is that raw InheritedWidget is immutable and a bit verbose, and it doesn't manage the life of the object it holds. Provider wraps that same mechanism. It creates the object, disposes it when the provider leaves the tree, can create it lazily, and with ChangeNotifierProvider it listens to the notifier and triggers the rebuild for me. In widgets I use context.watch when I want to rebuild on changes and context.read inside callbacks when I just need the object."

Red flag to avoid:

Believing every widget between the provider and the consumer rebuilds when the value changes.

They may ask next:
  • What is the difference between context.watch, context.read and context.select?
  • Why is it a mistake to call context.watch inside a button's onPressed?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

8. setState, Provider, Riverpod, Bloc: how would you choose a state management approach for a new Flutter app?

What the interviewer is really testing:
Whether you choose from the app's needs and the team's skills instead of fashion, and know that approaches can be mixed.
Answer frame:

Split the state: local UI state stays in setState; shared app state needs a proper tool.

Weigh the app: size, how many screens share data, testing needs, async complexity.

Weigh the team: what people know, how much structure and boilerplate they will accept.

Commit: pick one for app state, write the conventions down, stay consistent.

Sample spoken answer:

"First I separate two kinds of state. Things like whether a dropdown is open or which tab is selected are local, and setState is perfectly fine for them. The real decision is for shared app state, like the signed-in user, a cart or data loaded from an API. For a small app with a few shared objects, Provider with ChangeNotifier is simple and well understood. If the app is bigger and has lots of async data that depends on other data, I like Riverpod because providers don't depend on the widget tree and are easy to override in tests. If the team wants strict structure, with every change going through an explicit event and a clear log of state transitions, Bloc fits well, at the cost of more boilerplate. I'd also weigh what the team already knows. Whatever we pick, the big win is consistency, so I'd write down the conventions early."

Red flag to avoid:

Declaring one library the best for every app, or putting every bit of UI state into a global store.

They may ask next:
  • Would you ever mix two approaches in one app? Where would you draw the line?
  • How does your choice change how you write tests?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

9. Explain the Bloc pattern. How do events, states and the UI fit together, and when would you use a Cubit instead?

What the interviewer is really testing:
Whether you have used Bloc properly: immutable states, side effects kept out of build, and a sense of when the full pattern is worth it.
Answer frame:

Flow: the UI adds events; the bloc handles each event and emits new states; the UI rebuilds from state.

States: immutable objects, often compared by value so identical states do not rebuild.

Widgets: BlocBuilder to rebuild, BlocListener for one-off actions like navigation or a snackbar.

Cubit: same idea without events; public methods call emit directly.

Sample spoken answer:

"With Bloc, the UI never changes state directly. It adds events, like LoginSubmitted with the email and password. The bloc registers a handler for each event type, does the work, and emits new states, like loading, then success or failure. States are immutable, and I usually make them compare by value so emitting the same state twice doesn't rebuild anything. On the UI side, BlocBuilder rebuilds from the current state, and BlocListener handles one-off side effects like navigating after login or showing a snackbar, which should never happen inside build. A Cubit is the lighter version. There are no event classes; the UI calls methods like increment or load, and those methods call emit. I start with a Cubit for simple features and use a full Bloc when I want the event log, or need event handling control like dropping repeated taps or debouncing search input."

Red flag to avoid:

Mutating a state object and emitting it again, then wondering why the UI did not update.

They may ask next:
  • How would you test a bloc that calls a repository?
  • Where do you put navigation after a successful login, and why not in BlocBuilder?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

10. What problems does Riverpod solve that Provider has? What do ref.watch and ref.read each do?

What the interviewer is really testing:
Whether you understand Riverpod's design beyond syntax: tree independence, compile-time safety, composition and disposal.
Answer frame:

Not tied to the tree: providers are declared once and read through ref, so no runtime provider-not-found errors.

Composition: a provider can watch other providers and recompute when they change.

Lifecycle: autoDispose frees state when no one listens; family passes a parameter like an id.

watch vs read: watch in build to rebuild on change; read in callbacks for a one-time value.

Sample spoken answer:

"Provider depends on the widget tree. If I ask for something that isn't above me, I find out at runtime with an exception, and having two providers of the same type is awkward. Riverpod declares providers as top-level objects and stores their state in a ProviderScope at the root, so reading one is always valid and it doesn't need a BuildContext. Providers can watch other providers, so a filtered list provider can watch the list and the filter and recompute when either changes. autoDispose throws the state away when nothing is listening, which stops stale screens holding memory, and family lets me create one provider per parameter, like a product by id. In widgets, ref.watch subscribes and rebuilds when the value changes, so it belongs in build. ref.read just grabs the current value once, so I use it in callbacks like onPressed. Testing is easier too, since I can override any provider with a fake."

Red flag to avoid:

Saying Riverpod keeps state in global variables, or using ref.read in build and wondering why the widget does not update.

They may ask next:
  • Why is calling ref.read inside build a subtle bug?
  • How would you load data by id and cancel the request when the user leaves the screen?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

11. Tell me about a time you changed how state was managed in an existing Flutter app. How did you avoid breaking features along the way?

What the interviewer is really testing:
Whether you can run a risky refactor in steps, with tests and team buy-in, instead of a big-bang rewrite.
Answer frame:

Why: the concrete pain with the old approach, such as bugs, untestable code or tangled rebuilds.

Plan: migrate feature by feature behind a clear boundary, old and new side by side.

Safety net: tests written before moving each feature, careful release checks.

Outcome: what improved and what you would do differently.

Sample spoken answer:

"In my last role we had an app where most shared state lived in a few huge ChangeNotifiers, and any change to the cart rebuilt half the app and caused odd bugs. The team agreed to move to Bloc, but we couldn't freeze features for a rewrite. So I proposed going one feature at a time. For each feature I first wrote widget tests around the current behaviour, then added a repository layer so data access sat behind one interface, then built the new bloc on top and switched the screens over. Old notifiers and new blocs lived side by side for a few releases, and we only deleted a notifier when nothing referenced it. We started with the cart, which was the most painful, so the team saw the benefit early. Bugs in that area dropped and tests got much easier to write. Looking back, I'd write the conventions doc before the first migration, not after the second."

Red flag to avoid:

A story about rewriting everything at once with no tests and no plan for the in-between period.

They may ask next:
  • How did you handle a screen that needed data from both the old and the new system?
  • How did you convince the team the migration was worth the time?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

12. A teammate proposes putting all app state, including form fields and animation flags, into one global store. How do you respond?

What the interviewer is really testing:
Whether you can push back with reasons, separating local UI state from shared app state, while keeping a working relationship.
Answer frame:

Understand first: ask what problem they want to solve, perhaps scattered state or hard debugging.

Split the state: local UI state stays close to the widget; shared app state goes in the store.

Costs: needless rebuilds, coupling between features, noisy code for tiny changes.

Agree: a written rule on what goes where, and try it on one feature.

Sample spoken answer:

"I'd start by asking what's bothering them, because there's usually a real problem underneath, like state spread across too many places or bugs that are hard to trace. Then I'd separate two kinds of state. Things like whether a password field is obscured or where an animation is are local and short-lived, and they belong in the widget with setState or a controller. Things like the signed-in user, the cart or loaded data are shared, and a store is great for those. Putting everything in one global store means a keystroke in a form can trigger listeners across the app, features become coupled to one giant object, and every tiny change needs its own event and handler. I'd suggest we write down a simple rule for what goes where, try it on one feature, and review it together. That way it's a team decision, not me overruling them."

Red flag to avoid:

Dismissing the idea without asking what problem it solves, or agreeing without mentioning any cost.

They may ask next:
  • What if the team lead agrees with your teammate?
  • Is there any local UI state you would still keep in the store?
Say it in 60 seconds

Async & Isolates 3 questions

Easy Technical round Fresher Practice question

13. What is the difference between a Future and a Stream in Dart, and which widgets do you use to show each one?

What the interviewer is really testing:
Whether you know the basic async types and can connect them to the widgets that render them, including loading and error states.
Answer frame:

Future: one value or one error, later.

Stream: many values over time, then maybe done; single-subscription by default, broadcast if many listeners.

In the UI: FutureBuilder and StreamBuilder, always handling waiting, error and data.

Sample spoken answer:

"A Future is a single result that arrives later, like the response to one HTTP request. It completes once, with a value or an error, and I usually consume it with async and await. A Stream is a sequence of values over time, like location updates, chat messages or a live database query that keeps pushing changes. I listen to it, and it can emit many values and errors before it closes. Most streams are single-subscription, so listening twice throws; if several places need it, I use a broadcast stream. In the UI, FutureBuilder shows a future and StreamBuilder shows a stream. Both give me a snapshot, and I always handle three cases: still waiting, an error, and data. The most common mistake I see is only handling the data case, so the user stares at a blank screen when something fails."

Red flag to avoid:

Saying a Stream is just a Future that repeats, or ignoring error and loading states in the builders.

They may ask next:
  • How do you write a function that returns a Stream using async* and yield?
  • What happens if you forget to await a Future that throws?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

14. Your FutureBuilder calls the API again every time the screen rebuilds. Why is that happening, and how do you fix it?

What the interviewer is really testing:
Whether you understand that build can run many times, so it must not start side effects like network calls.
Answer frame:

Cause: the future is created inside build, so every rebuild makes a new request.

Fix: create the future once in initState and store it in a field.

Changes: create a new one in didUpdateWidget if the id changes, or on retry.

Bigger apps: move loading into a state management layer that caches results.

Sample spoken answer:

"The usual cause is writing future: api.fetchUser(id) directly inside build. Build can run many times, when a parent rebuilds, when the keyboard opens and the screen size changes, when the theme changes. Each time, calling fetchUser fires a brand new request, and FutureBuilder sees a different future and drops back to the waiting state. The fix is to create the future once, in initState, store it in a field and pass that field to FutureBuilder. If the user id can change, I compare in didUpdateWidget and create a new future only then. For a retry button, I assign a new future inside setState. In a larger app I'd rather move this into Riverpod or a bloc that caches the result, so the widget just renders state and the rule 'build has no side effects' holds everywhere."

Code:
class _ProfileScreenState extends State<ProfileScreen> {
  late Future<User> _userFuture;

  @override
  void initState() {
    super.initState();
    _userFuture = api.fetchUser(widget.userId);
  }

  void _retry() => setState(() {
        _userFuture = api.fetchUser(widget.userId);
      });

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: _userFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) return const CircularProgressIndicator();
        if (snapshot.hasError) return TextButton(onPressed: _retry, child: const Text('Retry'));
        return Text(snapshot.data!.name);
      },
    );
  }
}
Red flag to avoid:

Fixing it by making the widget stateless or adding a flag, without understanding that build can run many times.

They may ask next:
  • What would you change if the userId passed in can change while the screen is open?
  • How would you show the old data while a refresh is loading?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

15. Dart runs your code on one thread. When does async still freeze the UI, and when would you reach for an isolate?

What the interviewer is really testing:
Whether you separate waiting on I/O, which async handles, from heavy CPU work, which blocks the event loop and needs an isolate.
Answer frame:

Event loop: async code yields while waiting, so I/O like network calls does not block frames.

What still freezes: long synchronous work such as parsing a huge JSON string or processing an image.

Isolates: separate memory and their own event loop; they talk by passing messages, not by sharing objects.

Tools: Isolate.run or compute for one-off jobs; a long-lived isolate with ports for a stream of work.

Sample spoken answer:

"Dart's main isolate runs an event loop. When I await a network call, the function pauses and the loop keeps drawing frames, so waiting on I/O doesn't freeze anything. What does freeze it is heavy synchronous work. If I decode a multi-megabyte JSON response or resize an image on the main isolate, that code runs start to finish without yielding, and the UI misses frames. Marking the function async doesn't help, because the work itself is still synchronous. That's when I use an isolate. An isolate has its own memory and its own event loop, so nothing is shared. I send it input and get a result back as messages, and sending very large inputs has its own cost. For a one-off job I use Isolate.run or compute. If I need a worker that handles many jobs, I spawn a long-lived isolate and talk to it through ports."

Code:
import 'dart:convert';
import 'dart:isolate';

Future<List<Product>> parseProducts(String body) {
  return Isolate.run(() {
    final list = jsonDecode(body) as List<dynamic>;
    return list
        .map((e) => Product.fromJson(e as Map<String, dynamic>))
        .toList();
  });
}
Red flag to avoid:

Saying that marking a function async moves its work to a background thread.

They may ask next:
  • Why can't two isolates share a list and both modify it?
  • What happens with isolates when the same app runs on the web?
  • How would you confirm the parsing was really the cause of the jank?
Say it in 60 seconds

Layout & Navigation 4 questions

Medium Technical round Fresher, Mid-level Practice question

16. Explain "constraints go down, sizes go up, parent sets position". Then tell me why a ListView inside a Column throws an unbounded height error.

What the interviewer is really testing:
Whether you can debug layout errors from the rule itself instead of trying random wrappers until something works.
Answer frame:

The rule: a parent gives each child min and max width and height; the child picks a size inside them; the parent places it.

The Column: it lets non-flexible children be as tall as they like in its main axis.

The ListView: it wants to fill all the height it is given, and infinity is not a size.

Fixes: Expanded or Flexible, a fixed-height box, or shrinkWrap for short lists only.

Sample spoken answer:

"Layout in Flutter is one pass down and one pass up. Each parent passes constraints to its children, a minimum and maximum width and height. Each child picks a size within those limits and reports it back, and then the parent decides where to put it. A child can't just choose any size it likes; it only chooses within what it was given. Now a Column: for children that aren't wrapped in Expanded or Flexible, it doesn't limit height, because it wants to know how tall they naturally are. A ListView is a scrollable viewport that tries to be as tall as allowed. Given unlimited height, it can't pick a size, so Flutter throws the unbounded height error. The usual fix is wrapping the ListView in Expanded so it gets the leftover space. A SizedBox with a set height works too. shrinkWrap also works, but it lays out every item, so I only use it for short lists."

Red flag to avoid:

Reaching for shrinkWrap everywhere without knowing it lays out every child and hurts long lists.

They may ask next:
  • Why does a Container with width 100 sometimes fill the whole screen anyway?
  • How would you put a horizontal ListView inside a vertical one?
Say it in 60 seconds
Easy Technical round Fresher Practice question

17. In a Row or a Column, what is the difference between Expanded and Flexible?

What the interviewer is really testing:
Whether you know how flex space is shared and can pick the right widget when a child should or should not fill its share.
Answer frame:

Shared idea: both take a share of the space left after fixed-size children, split by their flex values.

Expanded: a tight fit; the child must fill its whole share.

Flexible: a loose fit by default; the child can be smaller than its share.

Where: only as direct children of a Row, Column or Flex.

Sample spoken answer:

"Both are for children of a Row, Column or Flex. The layout first sizes the children that aren't flexible, then splits the leftover space between the flexible ones according to their flex value, so flex 2 gets twice the share of flex 1. The difference is what the child does with its share. Expanded forces the child to fill it completely; under the hood it's just Flexible with a tight fit. Flexible, by default, uses a loose fit, so the child can be as small as it wants, up to its share. So if I have a label and a button in a row and I want the label to take all the remaining width and push the button to the edge, I use Expanded. If I want a long text to shrink and ellipsize when space is tight, but stay its natural width when there's room, Flexible is the better fit."

Red flag to avoid:

Saying they are the same, or using Expanded outside a Row, Column or Flex.

They may ask next:
  • What happens if you put an Expanded inside a Container that sits inside the Row?
  • When would you use Spacer instead?
Say it in 60 seconds
Easy Coding round Fresher Practice question

18. How do you move between screens with Navigator push and pop, and how does a screen send a result back to the one that opened it?

What the interviewer is really testing:
Whether you know the navigator is a stack and that push returns a Future you can await for a result.
Answer frame:

Stack: push adds a route on top; pop removes it and shows the one below.

Result: push returns a Future that completes with whatever the next screen passes to pop.

Safety: the result can be null if the user just goes back; check mounted before using context.

Sample spoken answer:

"The Navigator keeps a stack of routes. When I call push with a MaterialPageRoute, the new screen goes on top, and pop takes it off and reveals the previous one. The nice part is that push returns a Future. So if I open a picker screen, I can await that push, and when the picker calls pop with a value, that value comes back as the result. I always treat the result as nullable, because the user might just press back or swipe away, and then it comes back null. Since there's an await in between, I also check that the widget is still mounted before calling setState or using context. For passing data forward, I just pass it in the new screen's constructor, which keeps it typed and simple."

Code:
// On the first screen
Future<void> _pickColor() async {
  final picked = await Navigator.of(context).push<String>(
    MaterialPageRoute(builder: (_) => const ColorPickerScreen()),
  );
  if (!mounted || picked == null) return;
  setState(() => _color = picked);
}

// Inside ColorPickerScreen, when the user taps a colour
Navigator.of(context).pop('blue');
Red flag to avoid:

Passing results back through global variables, or assuming the result is never null.

They may ask next:
  • How do you clear the whole stack after login so back does not return to the login screen?
  • What changes if you use named routes instead of MaterialPageRoute?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

19. Why would a team move from plain Navigator push calls to declarative routing, for example the Router API or go_router?

What the interviewer is really testing:
Whether you know the real reasons, deep links, web URLs and auth redirects, rather than switching because it is newer.
Answer frame:

Problem: push calls make the stack hard to rebuild from a URL or a notification link.

Declarative idea: the page stack is worked out from app state or the URL.

Wins: deep links, correct browser URLs and back button, auth redirects in one place, nested navigation for tabs.

Cost: more setup; a small app with no deep links may not need it.

Sample spoken answer:

"Push and pop are fine until the app has to open straight to a screen from outside. A push notification says 'open order 42', or on the web the user pastes a URL or presses the browser back button. With only imperative pushes, rebuilding the right stack for that is messy. The Router API, sometimes called Navigator 2.0, flips it around: the list of pages is worked out from state, so the URL or app state decides what's on screen. It's powerful but verbose, so most teams use a package like go_router on top. There I declare routes with paths like /orders/:id, the package parses deep links for me, and a redirect function handles things like sending signed-out users to login in one place. It also supports shell routes for a bottom navigation bar that stays on screen, and the stateful version keeps a separate stack per tab. For a small app with no deep links, I'd honestly keep plain push."

Red flag to avoid:

Not being able to name a single concrete problem, like deep links or auth redirects, that declarative routing solves.

They may ask next:
  • In go_router, what is the difference between go and push?
  • How would you keep each bottom tab's own navigation history?
Say it in 60 seconds

Platform & Tooling 4 questions

Medium Technical round Mid-level, Senior Practice question

20. How do you call native Android or iOS code from Flutter? Walk me through how a platform channel works.

What the interviewer is really testing:
Whether you have crossed the Dart and native boundary yourself and understand its async, message-based nature and error handling.
Answer frame:

MethodChannel: a named channel; Dart calls invokeMethod, native code registers a handler with the same name.

Messages: async and encoded by a codec, so only simple types like numbers, strings, lists and maps cross.

Errors: native errors arrive as PlatformException; no handler gives MissingPluginException.

Variants: EventChannel for streams of events, Pigeon for generated type-safe code, FFI for C libraries.

Sample spoken answer:

"A platform channel is a named, async message pipe between Dart and the host platform. On the Dart side I create a MethodChannel with a unique name and call invokeMethod with a method name and arguments. On Android, in Kotlin, and on iOS, in Swift, I register a handler on a channel with the same name, switch on the method name, do the work and send back a result or an error. Everything is serialized by a codec, so I stick to simple types like strings, numbers, lists and maps. Because it's async, the Dart call returns a Future. A native error comes back as a PlatformException, and if nothing is registered on the other side I get a MissingPluginException, which usually means a wrong channel name or the native code wasn't rebuilt. For continuous data like sensor readings I use an EventChannel. On bigger integrations I prefer Pigeon, which generates typed code for both sides so method names can't drift."

Code:
const _channel = MethodChannel('com.example.app/battery');

Future<int?> batteryLevel() async {
  try {
    return await _channel.invokeMethod<int>('getBatteryLevel');
  } on PlatformException catch (e) {
    debugPrint('Native call failed: ${e.message}');
    return null;
  }
}
Red flag to avoid:

Thinking Dart objects can be passed directly to native code, or never handling PlatformException.

They may ask next:
  • What thread does the native handler run on by default, and why does that matter for slow work?
  • Why might hot reload not pick up a change you made to the native handler?
Say it in 60 seconds
Easy Technical round Fresher Practice question

21. What is the difference between hot reload and hot restart, and name a change that hot reload will not pick up.

What the interviewer is really testing:
Whether you understand what each one does to the running app, so you do not waste time chasing changes that never loaded.
Answer frame:

Hot reload: injects changed Dart code into the running app and rebuilds the tree; state is kept.

Hot restart: restarts the Dart app from main; state is lost; faster than a full rebuild.

Missed by hot reload: code in main or initState that already ran, changed global or static initializers, some type changes.

Neither: native code, new plugins and some config need a full stop and rebuild.

Sample spoken answer:

"Both only work in debug mode, because that build runs Dart with a JIT compiler that can accept new code. Hot reload sends the changed code into the running app, and the framework rebuilds the widget tree while keeping state, so if I'm five screens deep with a form half filled in, I stay there and see my UI change. Hot restart throws the Dart state away and runs main again, so the app starts from the first screen, but it's still much faster than a full build. Hot reload won't show changes to code that already ran and doesn't run again, like main or initState of a widget that's already on screen. Changed initial values of global or static variables also aren't applied. Some structural changes, like turning a StatelessWidget into a StatefulWidget, often need a restart too. And changes to native Kotlin or Swift code, or adding a plugin, need a full stop and rebuild."

Red flag to avoid:

Thinking hot reload also reloads native code or works in release mode.

They may ask next:
  • Why is hot reload not available in release builds?
  • Your change to an initState is not showing up after hot reload. What do you do?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

22. Tell me about a bug in a Flutter app that only happened on Android or only on iOS. How did you track it down?

What the interviewer is really testing:
Whether you can debug below the Dart layer, using native logs and tools, when the shared code is not the problem.
Answer frame:

Symptom: what broke, on which platform or OS version, and how you reproduced it.

Narrow it down: Dart or native side, which plugin, which device settings.

Tools: native logs, Xcode or Android Studio, a small reproduction project.

Fix and prevention: the change and how you stopped it recurring.

Sample spoken answer:

"On an app I worked on at my last company, photo uploads worked on Android but silently failed on some iPhones. The Dart code was the same, so I suspected the native side. I ran the app from Xcode to see the native console, and saw the image picker returning a file in a format our backend rejected, because those phones saved photos in a high-efficiency format by default. On the Android phones we tested, the same flow gave us a JPEG. I confirmed it by logging the file extension and size on both devices. The fix was converting every image to JPEG on the Dart side before upload, so we never depended on the platform default. I added a check that rejected unsupported types with a clear message instead of failing silently, and from then on we tested media features on both platforms before every release."

Red flag to avoid:

Only ever looking at Dart logs and never opening the native tooling.

They may ask next:
  • How do you decide whether a bug is in your code, a plugin or the platform itself?
  • What do you do when a plugin you depend on has a platform bug and the maintainer is slow to respond?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

23. Product wants a feature that needs a native SDK, and there is no maintained Flutter plugin for it. How do you approach it?

What the interviewer is really testing:
Whether you can plan native integration work realistically, keep the Dart API small and set honest expectations with product.
Answer frame:

Check first: look for existing plugins, even unmaintained ones worth forking, and read the SDK docs for both platforms.

Design small: a narrow Dart API over the calls the feature needs, not the whole SDK.

Build: a local plugin with platform channels or Pigeon; EventChannel for callbacks; platform views if the SDK shows its own UI.

Expectations: estimate both platforms separately, plan device testing and ownership of upgrades.

Sample spoken answer:

"I'd first make sure there really is nothing usable, including older plugins we could fork, and I'd read the SDK docs for both Android and iOS, because the two versions often behave differently. Then I'd design a small Dart API around just what this feature needs, maybe three or four calls, rather than wrapping the entire SDK. I'd build it as a local plugin inside our repo, using Pigeon so the Dart, Kotlin and Swift sides share typed method signatures, and an EventChannel if the SDK pushes callbacks like status updates. If the SDK draws its own screens, I'd look at platform views, knowing they have a performance cost. With product, I'd be honest that this is really two native integrations plus a Dart layer, estimate each platform separately, and flag that someone must own SDK upgrades. A quick spike on one platform first would tell us how rough the estimate is."

Red flag to avoid:

Promising a quick estimate without checking both platforms, or wrapping the whole SDK instead of what the feature needs.

They may ask next:
  • How would you test the plugin without the real SDK in every test run?
  • What would make you push back on the feature entirely?
Say it in 60 seconds

Performance 3 questions

Easy Technical round Fresher, Mid-level Practice question

24. The linter keeps asking you to add const in front of widgets. What does const actually save at runtime?

What the interviewer is really testing:
Whether you know the real mechanism behind const widgets and do not overstate it as a fix for every rebuild.
Answer frame:

Compile time: a const widget is created once and the same instance is reused.

Rebuild skip: when a parent rebuilds and hands over the identical instance, Flutter skips rebuilding that subtree.

Less garbage: fewer objects allocated per frame.

Limits: a const widget still rebuilds on its own setState or when an inherited dependency changes.

Sample spoken answer:

"A const constructor lets Dart build the object at compile time, and every use of the same const expression gives you the exact same instance. That matters during rebuilds. When a parent rebuilds, the element compares the new child widget with the old one, and if it's literally the same instance, Flutter knows nothing changed and skips rebuilding that whole subtree. So wrapping a static header or an icon in const means a parent's setState doesn't touch it. It also means fewer objects created every frame, so less work for the garbage collector. It isn't magic though. A const widget can still rebuild if it has its own state that changes, or if it depends on something like Theme.of and the theme changes. And it only works when all the arguments are constant, so anything built from runtime data can't be const."

Red flag to avoid:

Claiming const makes a widget never rebuild, or that it is only about code style.

They may ask next:
  • Why does extracting a piece of UI into its own widget class often beat a helper method that returns a widget?
  • Can a widget that takes a callback ever be const?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

25. A long list scrolls with visible jank on mid-range Android phones. How do you find the cause and fix it?

What the interviewer is really testing:
Whether you measure in the right mode on real hardware and know the usual causes on both the UI and raster sides.
Answer frame:

Measure right: profile mode on a real mid-range device, never debug mode or an emulator.

Find the thread: DevTools shows whether frames are slow on the UI thread or the raster thread.

UI thread fixes: build items lazily with ListView.builder, keep item build cheap, no parsing or sorting in build.

Raster fixes: avoid costly effects like Opacity and heavy clips, decode images at display size.

Sample spoken answer:

"First I reproduce it properly: a profile build on a real mid-range phone, because debug mode is much slower and would send me after the wrong problem. Then I open DevTools and look at the frame chart. At sixty frames a second each frame has about sixteen milliseconds, and the chart tells me whether the UI thread, which runs my build and layout, or the raster thread, which draws, is over budget. If it's the UI thread, I check that the list uses ListView.builder so only visible items are built, that item widgets don't format dates or sort data in build, and that there's no shrinkWrap forcing every item to lay out. If it's the raster thread, the usual suspects are Opacity widgets, heavy clipping and shadows, and large images decoded at full size, which I fix with cacheWidth or properly sized thumbnails. Then I measure again to confirm the fix actually helped."

Red flag to avoid:

Guessing at fixes in debug mode on an emulator without looking at a profiler.

They may ask next:
  • Why is debug mode a bad place to judge performance?
  • How do itemExtent or a prototype item help a long list?
  • What does a RepaintBoundary do, and when can it make things worse?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

26. Tell me about a Flutter screen you made noticeably smoother or faster. How did you find out where the time was going?

What the interviewer is really testing:
Whether you measured before changing code, found a real cause and can show the result, not just list generic tips.
Answer frame:

Situation: the screen, the symptom and who noticed it.

Measure: profile build, device used, what DevTools showed.

Fix: the specific cause and the change you made.

Result: how you checked it improved, and what you changed in the team's habits.

Sample spoken answer:

"At my last company, our product feed felt choppy on cheaper Android phones, and support kept hearing about it. I ran a profile build on one of those phones and opened DevTools. The raster thread was fine, but the UI thread was blowing the frame budget whenever new items scrolled in. Digging into the timeline, each card was formatting prices and dates and filtering a list of tags inside build, and the whole card rebuilt whenever a like counter changed anywhere on screen. I moved the formatting into the model when the data loaded, split the like button into its own small widget listening only to its own state, and made the static parts const. Then I measured again on the same phone, and the slow frames mostly disappeared. The lasting change was a team rule: no data work inside build, and we check new screens in profile mode on a low-end device."

Red flag to avoid:

A story with no measurement, where the fix was a guess and nobody checked it worked.

They may ask next:
  • What would you have done if the raster thread had been the slow one?
  • How did you stop the problem coming back in later releases?
Say it in 60 seconds

Dart Language 1 questions

Easy Technical round Fresher, Mid-level Practice question

27. Explain Dart's sound null safety. What do the question mark, the exclamation mark and the late keyword each mean?

What the interviewer is really testing:
Whether you use the type system to prevent null errors instead of silencing it with the bang operator everywhere.
Answer frame:

Default: types cannot hold null unless marked with a question mark.

Sound: the compiler guarantees a non-nullable variable is never null at runtime.

Bang: ! tells the compiler a value is not null; it throws if you are wrong.

late: a non-nullable variable set after declaration; reading it before that throws.

Sample spoken answer:

"In Dart, types are non-nullable by default. A String can never be null. If a value might be missing, I write String with a question mark, and then the compiler makes me handle null before I use it. It's called sound because the compiler can guarantee that a non-nullable variable really never holds null at runtime. The exclamation mark is me telling the compiler 'trust me, this isn't null'. If I'm wrong, it throws at runtime, so I use it rarely and prefer a null check, the ?. operator, or ?? to give a default. After I check a local variable for null, flow analysis promotes it to the non-nullable type automatically. The late keyword is for non-nullable variables I can't set at declaration, like a controller created in initState. Reading a late variable before it's set throws an error, so it moves the check to runtime and I only use it when the order of setup is clear."

Red flag to avoid:

Adding ! wherever the compiler complains, turning compile-time errors back into runtime crashes.

They may ask next:
  • Why does checking a class field for null sometimes not let you use it as non-null?
  • What does late do when the variable has an initializer?
Say it in 60 seconds

Testing & Release 3 questions

Medium Coding round Fresher, Mid-level Practice question

28. How do you write a widget test in Flutter? Write one that taps a button and checks that the text on screen changes.

What the interviewer is really testing:
Whether you actually test Flutter UI and know the pump cycle, finders and where widget tests sit next to unit and integration tests.
Answer frame:

Layers: unit tests for logic, widget tests for one screen or widget, integration tests on a real device for full flows.

Pump: pumpWidget builds the widget; after an action, pump triggers the next frame.

Find and check: finders like find.text or find.byKey, matchers like findsOneWidget.

Dependencies: inject fakes for APIs so tests are fast and repeatable.

Sample spoken answer:

"Flutter has three layers of tests. Unit tests check plain Dart logic, widget tests render a widget in a fake environment without a device, and integration tests run the real app on a device or emulator. Widget tests are the sweet spot, because they're fast and still exercise the UI. In a test, pumpWidget builds my widget, usually wrapped in a MaterialApp so it has a theme and directionality. I find the button, tap it, and then call pump, because tapping only schedules the rebuild and nothing changes on screen until the next frame is pumped. If there are animations, I use pumpAndSettle to let them finish. Then I assert on finders, like expecting to find exactly one widget with the text 1. For screens that call an API, I pass in a fake repository or override the provider, so the test never hits the network."

Code:
testWidgets('tapping plus increments the counter', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: CounterPage()));

  expect(find.text('0'), findsOneWidget);

  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();

  expect(find.text('1'), findsOneWidget);
  expect(find.text('0'), findsNothing);
});
Red flag to avoid:

Forgetting to pump after the tap and concluding the widget is broken, or testing screens against the real network.

They may ask next:
  • Why can pumpAndSettle time out, and what would you do instead?
  • What are golden tests, and what makes them fragile?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

29. What is different between debug, profile and release builds, and what do you check before shipping a release?

What the interviewer is really testing:
Whether you have shipped a real Flutter app and know that debug behaviour is not what users get.
Answer frame:

Debug: JIT compiled, asserts on, hot reload, debugger; slow and not for judging speed.

Profile: compiled ahead of time like release, but keeps what is needed for performance tracing.

Release: compiled ahead of time, asserts off, no debugging, smallest and fastest.

Before shipping: test the real release build, signing, version numbers, permissions, obfuscation with symbols kept, crash reporting.

Sample spoken answer:

"Debug builds run Dart through a JIT compiler with assertions turned on, which is what gives me hot reload and the debugger, but it's much slower than what users get. Profile mode is compiled ahead of time like release, but keeps enough hooks for DevTools to trace performance, so that's where I measure speed. Release is compiled ahead of time with asserts removed and debugging stripped, so it's fast and small. Before shipping, I install the actual release build on real Android and iOS devices, because some bugs only show up there. I check signing is set up properly, bump the version name and build number, and confirm the permissions and platform config are right for release. If we obfuscate the Dart code, I keep the split debug info files, otherwise crash stack traces become unreadable. And I make sure crash reporting is wired in so we hear about problems before the reviews do."

Red flag to avoid:

Only ever testing in debug mode and assuming release behaves the same.

They may ask next:
  • Why should you never put code with side effects inside an assert?
  • How do you read an obfuscated stack trace from a crash report?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

30. Two days before launch, the app crashes on startup in release mode but runs fine in debug. What do you do?

What the interviewer is really testing:
Whether you stay methodical under deadline pressure, know the usual release-only causes and communicate the risk early.
Answer frame:

Reproduce: run a release build on a real device and capture native logs.

Read the trace: symbolize obfuscated stack traces with the saved debug symbols.

Usual suspects: logic inside asserts, native code shrinking removing classes a plugin needs, missing config for release, differences in keys or environment.

Communicate: tell the team early, and weigh fixing against rolling back the change.

Sample spoken answer:

"First I'd tell the lead straight away, because a launch date is at risk and they need to know early. Then I'd reproduce it with a release build on a real device while watching the native logs, Logcat on Android or the Xcode console on iOS, since a startup crash can happen before any Dart error handler runs. If the code is obfuscated, I'd use the saved symbol files to make the stack trace readable. Then I'd check the usual release-only causes. Code inside an assert doesn't run in release, so any setup hidden there is skipped. On Android, code shrinking can remove classes a native SDK loads by reflection. Release can also use different config, like missing keys or a different environment file. I'd look at what changed since the last good release build to narrow it quickly. If the fix is small and clear, ship it with a full retest. If not, I'd roll back the change that caused it and protect the date."

Red flag to avoid:

Changing things at random until it works, or hiding the problem from the team until the last moment.

They may ask next:
  • How would you stop this kind of problem reaching the last week before launch next time?
  • What would you say to a manager who wants to ship with the crash and fix it later?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card