Rapid-fire questions and live coding
The technical part of a Flutter developer interview almost always starts with the same two formats: rapid-fire (10-20 short Dart questions, 20-30 seconds each) and live coding (one small problem, 20-40 minutes).
Rapid-fire is not there to probe depth — it is there to see whether the fundamentals are settled. A long answer is therefore a negative signal: it says you did not notice the question was simple.
The three-part answer formula — use this shape for every short question:
1. The direct answer (one sentence): "const is a compile-time constant, final is a variable that can be assigned exactly once."
2. *One line of why* (the mechanism): "...because `const` objects are created at compile time and canonicalised."
3. One concrete example**: "...so two const Money(100, 'AZN') with the same arguments make identical return true."
The formula fits in 20-30 seconds and shows three things at once: you know it, you understand it, you have used it.
If you don't know: "I don't know that precisely. My guess would be..., but I'd verify it" — a normal, respectable answer. Two or three of those out of fifteen questions is expected. Making something up is the worst option: interviewers almost always sense it, and afterwards they doubt every other answer.
| Question | Model answer (20-30 seconds) |
|---|---|
| Difference between `const` and `final`? | `final` is assigned once and its value may be computed at runtime (`final now = DateTime.now()`). `const` must be fully known at compile time and is deeply immutable; `const` objects with equal arguments are canonicalised, so `identical` returns `true` for them. |
| Difference between `?.` and `!`? | `?.` skips the call and yields `null` when the receiver is null. `!` is a promise to the compiler that the value is non-null; if you are wrong it throws at runtime. `?.` is safe, `!` needs proof. |
| What is `late` for? | Two things: initialising a non-nullable field after the constructor, and lazy computation (`late final x = expensive()` runs on first access, then caches). The price: a compile-time check becomes the risk of a runtime `LateInitializationError`. |
| Difference between a `sealed` class and an `enum`? | An `enum` is a fixed set of **values**; a `sealed` class is a fixed set of **subtypes**, each with its own fields. Both make `switch` exhaustive, but `sealed` can carry data: `Loading`, `Success(data)`, `Failure(error)` — impossible with an enum. |
| Difference between a `mixin` and an interface? | A `mixin` provides implementation — you attach it with `with` and get the methods. `implements` only imposes a contract: you write every member yourself. Every Dart class also defines an implicit interface, so `implements SomeClass` is legal. Inheritance stays single: `extends` from one class only. |
| When `abstract class`, when `interface class`? | `abstract class` when you want to share code and state (it cannot be instantiated directly). Dart 3's `interface class` modifier forbids `extends` outside the library — only `implements` is allowed. That protects an API against future changes. |
| Difference between a record and a class? | A record is an anonymous, lightweight value bundle: `(String, int)` or `({String name, int age})`. Structural equality comes for free — no `==`/`hashCode` to write. A class gives you a name, behaviour (methods) and invariants (validation). My rule: a record to return two values from a function, a class for a domain concept. |
| What must you not forget when overriding `==`? | `hashCode`. The rule: equal objects must have equal hashes, otherwise the object appears as a duplicate in a `Set` and cannot be found again as a `Map` key. I use `Object.hash(a, b)`, or `Object.hashAll(...)` for a collection, and keep the fields `final`. |
| How is an extension method resolved? | **Statically**, from the variable's **static type**, at compile time. That is why extensions do not work on `dynamic` and cannot be overridden — the key difference from ordinary method dispatch. If two extensions collide, you disambiguate with `MyExt(obj).method()`. |
| Difference between `dynamic` and `Object?`? | Both hold anything, but `Object?` keeps type checking — you may call only `Object` members and need a cast or a pattern for the rest. `dynamic` **switches checking off**: every call compiles and may blow up at runtime. The default choice is `Object?`. |
| How do Dart generics behave with regard to variance? | They are covariant: a `List<int>` is also a `List<num>`. Convenient for reading, dangerous for writing — add `1.5` to a `List<int>` you are holding as a `List<num>` and you get a runtime `TypeError`. That is why I accept read-only parameters as `Iterable<T>` in shared APIs. |
| Question | Model answer (20-30 seconds) |
|---|---|
| Difference between a `Future` and a `Stream`? | A `Future` is **one** result (or one error) later, consumed with `await`. A `Stream` is **zero or more** events over time, consumed with `listen` or `await for`. Simple rule: an HTTP request is a Future; a WebSocket or the changes of a search field are a Stream. |
| Difference between the microtask queue and the event queue? | The event loop first drains the microtask queue **completely**, then takes one item from the event queue. Future callbacks go to microtasks; `Timer`s, I/O and user events go to the event queue. Practical consequence: schedule microtasks endlessly and the event queue never gets its turn — the app freezes. |
| Difference between an isolate and async? | `async` is **not parallelism**: everything runs on one thread and one event loop — it uses waiting time well but does not distribute CPU work. An isolate is a separate thread with separate memory; no shared state, only messages. For heavy JSON parsing or image processing, `Isolate.run(() => ...)` keeps a five-second computation off the main isolate. |
| What does the `async` keyword do to a function? | It wraps the result in a `Future` automatically and allows `await` inside. Key detail: the body runs **synchronously up to the first `await`** — calling an `async` function does not by itself defer anything. |
| Difference between `sync*` and `async*`? | `sync*` returns a lazy `Iterable` — values are produced on demand. `async*` returns a `Stream` — values are emitted asynchronously. In both, `yield` emits one element and `yield*` delegates a whole sequence. |
| Difference between a single-subscription and a broadcast stream? | A single-subscription stream can be listened to once — a second `listen` throws; events are buffered until a listener arrives. A broadcast stream accepts many listeners but **buffers nothing**: a late subscriber misses earlier events. Reading a file is single-subscription; UI events are broadcast. |
| What do `??`, `??=` and `?.` do? | `a ?? b` yields `b` when `a` is null. `a ??= b` assigns `b` to `a` only when `a` is null. `a?.m()` calls the method unless `a` is null, in which case the result is `null`. They chain: `user?.address?.city ?? 'Baku'`. |
| What is a `factory` constructor for? | Unlike a generative constructor it is **not obliged to create a new object**: it can return a cached instance, a subtype, or a different object after validation. It has no access to `this`. Typical uses: `factory User.fromJson(...)` and cache-backed instances. |
| What does a `const` constructor give you? | The ability to build the object at compile time: all fields must be `final`. `const` objects built with equal arguments are canonicalised — one instance in memory, and `identical` returns `true`. On the Flutter side this is valuable because it prevents needless rebuilds. |
| Why are immutable models and `copyWith` the norm? | An immutable object can be shared safely, never mutates behind your back, has trustworthy `==`/`hashCode` and can be cached. When something must change you create a new copy: `user.copyWith(email: newEmail)`. In state management this removes the question "who mutated this object?" entirely. |
| Difference between `identical(a, b)` and `a == b`? | `==` is content equality (defined by the class), `identical` asks whether it is the same object. Example: `StringBuffer('dart').toString() == 'dart'` is `true` while `identical(...)` is `false`. In everyday code always `==`; `identical` only for canonicalisation and cache questions. |
Live coding: the process matters more than the answer. The task is usually simple (one function, a small class) — what is judged is how you work. Six steps:
1. Clarify (1-2 minutes). Do not start typing. "How large can the input get?", "Can it be empty?", "Does order matter?", "Should the result be a List, or is an Iterable fine?" These questions solve half the task and show your real working style.
2. Examples and edge cases. Agree the expected output on one simple example. Then enumerate the boundaries: empty input, a single element, duplicates, nulls, a very large input. Naming edge cases up front is a strong signal — far better than remembering them later.
3. State the approach and the complexity. "I'll group into a map and then sort: O(n log n) time, O(n) memory. An alternative would be... but the simpler version is enough here." Make stating complexity unprompted a habit.
4. Narrate while coding. Silent coding is the most commonly lost point. "Now the grouping part — I'm using putIfAbsent so the key is created when missing..."
5. Dry-run. When the code is written, walk it line by line yourself on the example: "With [3, 1, 3], after the first step the map is {3: 2, 1: 1}..." Finding your own bug before the interviewer does leaves a strong impression.
6. Discuss trade-offs. "This version is readable but makes two passes over the data; if performance mattered I'd fold it into one. For tests I'd start with an empty list and duplicate keys."
What to do when you get stuck — the moment where most points are lost, although the right behaviour makes it nearly harmless:
- Don't go silent. Sixty quiet seconds read as "doesn't know". Say "let me think for a moment" — that is entirely normal.
- Externalise the thinking: "I see two approaches: a simple O(n²) and an O(n) with a map. Shall I write the working one first and optimise after?" — most interviewers want exactly that.
- Write the simple working solution. Brute force plus "here's how I'd improve it" beats an unfinished "perfect" answer.
- Ask for a hint: "I'm stuck here — could you point me in a direction?" It is not a penalty; asking for help is valued in real work too.
- If you can't recall an API: "I don't remember the exact name, it's something like
groupBy; let me write the logic and I'd confirm it in the IDE." Nobody deducts points for syntax; they do for logic.