Flutter · 15 min
Senior Flutter interview questions and follow-ups
Twelve senior Flutter interview questions on rendering, keys, state, isolates, platform channels and architecture, with the follow-up to each.
Flutter · 10 min read
Flutter architecture interview questions on layering, state ownership, packages and testing, with the decision each one tests and what it costs a team.
This page is for Flutter engineers facing an architecture round, or the architecture half of a senior technical round. You get what the round scores, a decision matrix by team size, ten Flutter architecture interview questions with the weak answer and the answer that scores, a worked example on where the cart lives, and the one follow-up that sits behind all of them. Flutter’s documentation has an architecture guide now, with a UI layer and a data layer, but real codebases are split across state management approaches, so the round is less about reciting a pattern and more about defending the one you chose.
| Signal | Strong | Weak |
|---|---|---|
| Decision | Chooses for this app, this team and this lifetime | Names Clean Architecture and stops |
| Ownership | Says who owns each piece of state and for how long | “It is all in the provider” |
| Cost | Names what the choice makes harder: boilerplate, generated code, onboarding | “It scales” |
| Change | Says when they would do it differently, and how they would migrate | Defends every layer for every screen |
The cost of a pattern in a team of three is not its cost in a team of thirty. An answer that ignores team size is not yet an architecture answer.
| Situation | Reasonable default | What it costs |
|---|---|---|
| Small app, one or two engineers | Feature folders, a repository per data source, local state where it belongs, one simple state holder | Logic can drift into widgets as the app grows |
| Team of five to ten | Feature-first structure with presentation, domain and data inside each feature; one agreed state management approach; dependency injection | More files per feature; the team has to agree on the approach and keep to it |
| Several teams | Separate Dart packages per feature in a monorepo, a design system package, dependencies that only point one way, lint rules that enforce it | Tooling, versioning and code generation across packages; slower changes to shared code |
| Large legacy app | New features in the target structure, old screens migrated when touched, the “god” widgets split first | Two styles side by side for a long time |
Weak answer: the three layers of Clean Architecture, with no mention of the app.
The answer that scores: the layers as they exist, then the decisions: where state lives, which repositories own which data, where the rules shared between screens sit, and one thing you would change now. The change is the part that scores.
The answer that scores: when the business rules are complex, a team will maintain it for years, the logic needs heavy testing, or a data source is likely to be replaced. It is overkill for a prototype or a one-screen app, and saying so is part of the answer. A pass-through use case that only calls a repository is ceremony, not architecture.
The answer that scores: layer-first (models/, views/, controllers/) is fine for a small app and gets hard to navigate as it grows, because one change touches every folder. Feature-first keeps a change inside one feature, and makes the later split into packages easier. The layers still exist, inside each feature.
The answer that scores: the data model mirrors the API or the database, with its serialisation. The domain entity is what the rest of the app uses. Keeping them separate means an API rename changes one mapper instead of every screen. The cost is the mapping code, which is not worth it for a model used in one place.
The answer that scores: compare on ownership, how transitions are represented, what rebuilds, how it is tested, and what migrating away would cost. Prove it on one hard flow first, then write the decision down with examples and lint guidance. Do not rewrite stable features for uniformity. The deeper version is question 6 of Senior Flutter interview questions.
The answer that scores: a singleton hides the dependency and keeps state between tests. A service locator like GetIt makes dependencies replaceable but still hidden at the call site. Injection through constructors or providers makes them visible and easy to fake. For a small app a locator can be the pragmatic choice; the cost appears in testing.
The answer that scores: a core package for networking and shared infrastructure, a design system package, and a package per feature. Features depend on core and the design system, never on each other; they talk through interfaces or the routing layer. A package cannot import what its pubspec.yaml does not declare, so most violations fail the build instead of waiting for a reviewer. lib/src is a convention, not a wall, so add a lint or a dependency check for what the structure cannot express.
The follow-up: “Monorepo or several repositories?” A monorepo gives atomic changes across packages and one CI, at the cost of tooling and CI time. Separate repositories give clear ownership and access control, at the cost of version drift and cross-repository refactoring.
The answer that scores: mechanisms, not documents. Packages that make forbidden imports impossible, custom lint rules for what packages cannot express, a short written record of each decision and why, and review that checks the rule instead of arguing it again. A rule in a wiki erodes. A rule in the build holds.
The answer that scores: typed flags with an owner, a default and an expiry date, a safe value when the remote config is unavailable, and completed flags removed. Code behind a disabled flag still ships in the binary unless it is excluded at build time or loaded later through a supported mechanism. A remote flag must never be the only thing protecting an action the server should authorise.
The answer that scores: most tests on the logic, without Flutter: repositories with fake data sources, state holders driven by events with time controlled. Widget tests for screens with the state holder faked. A few integration tests for the flows that make money. If the architecture makes one of those hard, that is the architecture telling you something. Include the late result: a read that finishes after the user switched accounts must change nothing.
Prompt: “In an e-commerce app, the cart must be reachable from any screen and survive an app restart. Where does it live?”
Strong answer: “One owner: a CartRepository that holds the cart, writes every change to local storage, and exposes it as a stream or a listenable. It is created once at the app root and provided to the widget tree, so any screen can read it and nobody keeps a copy. Screens read it and send commands like add and remove. They never change a list they were handed. Restoring after a restart is loading from storage, not replaying UI state.”
Push: “The user signs out and another user signs in on the same phone.” Now the cart belongs to an account, not to the app. Storage is partitioned by user, sign-out clears or detaches the old cart according to a product decision, and the repository is recreated for the new session.
Push: “A response from the old account arrives after the switch.” This is the bug the first sentence is really guarding against. Give every async result a ticket and drop the ones that are stale:
abstract interface class CartStore {
Future<Map<String, int>> read(String accountID);
Future<void> write(String accountID, Map<String, int> items);
}
enum CartPhase { signedOut, loading, ready, saving, failed }
class CartSession {
CartSession(this._store);
final CartStore _store;
String? _accountID;
int _generation = 0;
Map<String, int> _items = const {};
CartPhase _phase = CartPhase.signedOut;
CartPhase get phase => _phase;
Map<String, int> get items => _items;
Future<void> selectAccount(String? id) async {
final ticket = ++_generation;
_accountID = id;
_items = const {};
_phase = id == null ? CartPhase.signedOut : CartPhase.loading;
if (id == null) return;
try {
final restored = await _store.read(id);
if (ticket != _generation) return;
_items = Map.unmodifiable(restored);
_phase = CartPhase.ready;
} on Exception {
if (ticket == _generation) _phase = CartPhase.failed;
}
}
Future<void> retry() async {
final id = _accountID;
if (id == null || _phase != CartPhase.failed) return;
await selectAccount(id);
}
Future<void> add(String productID, int quantity) async {
final owner = _accountID;
if (owner == null || _phase != CartPhase.ready) {
throw StateError('Cart is not ready for another mutation');
}
final ticket = _generation;
final next = Map<String, int>.from(_items);
next[productID] = (next[productID] ?? 0) + quantity;
_phase = CartPhase.saving;
try {
await _store.write(owner, Map.unmodifiable(next));
if (ticket != _generation) return;
_items = Map.unmodifiable(next);
_phase = CartPhase.ready;
} on Exception {
if (ticket == _generation) _phase = CartPhase.failed;
}
}
}Two decisions in there are worth saying out loud. A write that started under the old account finishes in the old account’s partition and never touches the new account’s cart. And add is rejected while a save is in flight, so rapid taps need a command queue, which costs more code. The store is injected, so a test can complete the old account’s read after the switch and check that nothing changes. failed is not a dead end: retry reloads the account’s stored cart through selectAccount, so a failed save or read recovers from what storage holds. The app root must still call selectAccount when the session changes, because nothing else will.
Push: “The server also has a cart, for the website.” Now there are two copies, and the question is which wins on conflict. Local first for responsiveness, the server as the authority on price and stock, and a merge rule stated before anyone writes the sync code.
Push: “Which local store?” A cart with line items, quantities and an owner is not a setting, so a key-value store is the wrong home for it. The storage choice has to answer a few questions: whether you need transactions and schema migrations, what the size bound is, and what happens when the stored data is corrupt. A cart that cannot be read back should fall back to empty and a server fetch, not crash the launch.
The answer is scored on the first sentence, “one owner”, and on whether the pushes change the design rather than the vocabulary.
Whatever the question, the next one is a version of: “What would make you change this?” A team that doubles, a second app sharing the code, a move to the web, a state management package that stops being maintained. Have one concrete answer for your own app: the condition, what you would change, and how you would migrate without stopping feature work. The same follow-up closes the Android and iOS architecture rounds, and the platform-neutral version is on Mobile architecture interview questions.
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.

Flutter · 15 min
Twelve senior Flutter interview questions on rendering, keys, state, isolates, platform channels and architecture, with the follow-up to each.

Senior and Staff loops · 11 min
Architecture interview questions that do not depend on the platform: modules, shared code, server-driven UI, flags, old app versions and who owns what.

Android · 14 min
Ten Android architecture interview questions and how senior answers are scored: layers, the domain layer, UDF, Hilt, modules, navigation and testing.