Flutter · 12 min read

Flutter machine coding interview: tasks and grading

What a Flutter machine coding interview asks, how the build is graded, and the state and structure choices that decide it before the UI is finished.

This page is for Flutter engineers with a Flutter machine coding interview or coding round ahead, live or take-home. You get the difference between the two kinds of coding round, the practical exercises worth rehearsing, how to spend the first fifteen minutes, what the interviewer grades, a worked paginated feed in Dart with the test that proves it, and the Flutter traps that cost points at the end. The grade in a machine coding round is usually decided by the structure you choose early, not by how the screen looks when time runs out.

How to talk while you type is covered in Mobile live coding interview. This page is about what you build and how it is judged.

Machine coding or algorithm round

A machine coding round asks you to build a small working feature: a screen or two, a data source, some state, often with a mock API. An algorithm round asks you to solve a data structure problem in Dart, usually without Flutter at all. Ask the recruiter which one it is, which environment you will use, whether packages are allowed, and how long you have. The answers change what you open before the call.

The practical rounds to rehearse

500 Flutter Interview Questions has three practical rounds in its end matter, each with the minimum evidence or the rubric an interviewer grades against. In short:

  1. A resilient paginated feed. Load pages from a mock API, support refresh, show loading and failure, and stay correct when a refresh and the next page overlap.
  2. Refactor a stateful screen. A screen that does I/O in the widget, owns a controller or subscription it never disposes, and has no test seam. Refactor it step by step.
  3. An animated progress visualisation. A circular progress indicator with CustomPainter, an explicit animation, semantics, reduced-motion behaviour and tests.

Build the first one against a timer before any machine coding interview. It covers most of what the other tasks test: state that changes under you, async work that can arrive late, and a UI that has to survive both.

The first fifteen minutes

The first minutes decide how the rest goes. Spend them on:

  • Two or three questions that change the design. “Can a refresh happen while the next page is loading?” “Should failures keep the items already loaded?”
  • The state, written down before any widget. A controller or notifier with items, loading, error and the cursor. Say it out loud.
  • The data boundary. An abstract API class, so the controller can be tested with a fake.
  • Something running. The first page printed or shown in a plain list by minute fifteen. Polish comes last.

State management under a clock

Pick the lightest tool that makes the state explicit. A ChangeNotifier or ValueNotifier with a clear owner is enough for a feed and is fast to write. If the company’s codebase uses BLoC or Riverpod and the interviewer asks for it, use it. Otherwise, reaching for the heaviest package under a clock is read as a judgement miss: you spend twenty minutes on setup that proves nothing about the problem.

Say why you chose it: “I will use a ChangeNotifier because the state is small and has one owner. In a larger app with shared state, I would put this behind the team’s state management package.” That sentence is worth more than the package.

What is graded

The book’s evidence list for the feed task is the core of the scorecard. This is how I read it in the room:

What I look for Strong Weak
Logic outside widgets The controller works and is tested without a widget Fetching and state inside build or initState
Races handled A stale response cannot overwrite newer data Refresh and pagination can interleave and corrupt the list
Failure keeps data A failed page shows an error and keeps what was loaded One error clears the screen
One test that matters A unit test on the race or the failure case No tests, or only a widget smoke test
Disposal Controllers and subscriptions disposed Leaks that only show up later

For the refactoring task the order matters more than the result: finding the lifecycle leak before redesigning everything, adding boundaries only where they buy testability, and keeping local what does not need to be shared.

The animation task is graded on details most candidates skip. shouldRepaint returns true only when a painted value changed. The AnimationController is disposed, and the State that owns it provides the ticker. Constraints and device pixels are handled, so the ring stays sharp at any size. Semantics give a screen reader a label and a value, and you can say how much difference your golden tests tolerate before they fail.

Worked example: the paginated feed

The plan, in three sentences: “A FeedController owns the items, the loading and error flags, and the next cursor. Every refresh increases a generation number, and any response from an older generation is dropped. A failure sets the error and keeps the items.”

class FeedController extends ChangeNotifier {
  FeedController(this._api);
  final FeedApi _api;

  List<Post> items = const [];
  bool loading = false;
  Object? error;
  String? _cursor;
  bool _hasMore = true;
  int _generation = 0; // a refresh starts a new generation

  Future<void> refresh() => _load(reset: true);
  Future<void> loadMore() => (loading || !_hasMore) ? Future.value() : _load(reset: false);

  Future<void> _load({required bool reset}) async {
    final gen = reset ? ++_generation : _generation;
    loading = true;
    error = null;
    notifyListeners();
    try {
      final page = await _api.fetch(cursor: reset ? null : _cursor);
      if (gen != _generation) return; // a newer refresh owns the list now
      items = reset ? page.items : [...items, ...page.items];
      _cursor = page.next;
      _hasMore = page.next != null;
    } catch (e) {
      if (gen != _generation) return;
      error = e; // keep what was already loaded
    } finally {
      if (gen == _generation) {
        loading = false;
        notifyListeners();
      }
    }
  }
}

The test that proves the race is handled, with a fake API that lets the test decide when each request completes:

class FakeFeedApi implements FeedApi {
  final calls = <Completer<FeedPage>>[];
  @override
  Future<FeedPage> fetch({String? cursor}) {
    final call = Completer<FeedPage>();
    calls.add(call);
    return call.future;
  }
}

test('a refresh during loadMore drops the stale page', () async {
  final api = FakeFeedApi();
  final feed = FeedController(api);

  final first = feed.refresh();
  api.calls[0].complete(FeedPage([Post('a')], next: 'p2'));
  await first;

  final more = feed.loadMore();   // page 2 in flight
  final again = feed.refresh();   // the user pulls to refresh meanwhile
  api.calls[2].complete(FeedPage([Post('b')], next: 'p2'));
  await again;
  api.calls[1].complete(FeedPage([Post('stale')], next: null));
  await more;

  expect(feed.items.map((p) => p.id), ['b']);
});

What the interviewer asks next, and what a strong answer says:

  • “Why a generation number and not cancelling the request?” A Dart Future cannot be cancelled. Some HTTP clients offer a cancel token, and I would use it too, to save the network. The generation check is what guarantees correctness, whatever the client does.
  • “Add offline caching.” The controller reads from a local store first and the network writes into it. Then the question becomes invalidation: when is the cached first page too old to show?
  • “Scrolling is janky on one item type.” Profile mode, find whether the UI or raster thread misses the frame, then fix that item: an image decoded too large, a heavy shadow, work in build.

Flutter traps that cost points late

  • setState or notifyListeners after dispose. An async call returns after the screen is gone. Check mounted in a State, or guard the controller.
  • BuildContext across an async gap. Navigating after an await without checking mounted works in the demo and crashes in review.
  • Keys in the list. Rows with state need keys from the item id, or state moves to the wrong row after a refresh.
  • Everything in one widget. A single build method with the fetch, the parsing and the UI cannot be tested, and the interviewer will ask how you would test it.
  • pumpAndSettle with a spinner on screen. An indefinite animation never settles, and the widget test times out. Use pump with a duration.

None of these shows up in the first ten minutes. They show up in review, when there is no time left to fix them, so check your build against this list before you say it is done.

Sources

What to do next

Whichever you pick, answer out loud and time yourself. Saying the answer is the part a page cannot do for you, and it is the part the round scores.

Free guide: the Phoenix Method