80 Flutter interview questions with detailed answers, sorted from basic to advanced. Free to practice, with progress saved automatically.
Flutter is a UI framework that uses the Dart language to build mobile apps, and other platforms, from a shared codebase. Questions cover Dart basics such as var, dynamic, and final, the difference between stateless and stateful widgets, state management, navigation and routing, isolates, and applying clean architecture to larger apps.
var lets Dart infer the type once at declaration and it cannot change thereafter. dynamic allows the type to change at runtime, deferring type checking. final creates a variable that cannot be reassigned after initialization. const can't be reassigned either, but its value must be…
Null safety means variables cannot contain null unless explicitly marked as nullable with ?. This prevents null access errors at runtime (in Dart, errors like "Null check operator used on a null value" or NoSuchMethodError) by catching them at compile time instead. - ? on…
The ! operator tells Dart to treat a nullable variable as non-nullable without checking at compile time. The check moves to runtime: if the value really is null, the app throws right on that line. Use ! when you know the value is not null…
Null-aware operators safely handle nullable values without writing if (x != null) everywhere. The three most common: - ?. accesses a property or calls a method only if the object is not null; if it's null, the whole expression is null. - ?? provides a…
These three collections differ in how you get elements out: a List by position (index), a Set keeps only unique values, a Map by key. scores keeps both 9s because a List allows duplicates and is indexed from 0. tags.add('dart') adds nothing because a Set…
A Future is a value that arrives once in the future (like a JS Promise); a Stream is a sequence of values arriving over time, like a conveyor belt. In Flutter, FutureBuilder builds UI from a Future and StreamBuilder builds UI from a Stream. Streams…
async marks a function as asynchronous, and await pauses that function until a Future completes. That makes async code read like synchronous code. await can only be used inside an async function. An async function always returns a Future, even if you return a plain…
Type promotion is when Dart narrows a variable's type based on control flow. If you have a nullable String? and check if (name != null), inside that block Dart treats name as non-nullable String without needing !. This happens automatically with null checks, is checks,…
Positional parameters are passed in order. Named parameters use curly braces and are called by name. Named parameters can be optional; combine with the required keyword to make them mandatory. Positional parameters can also be optional when wrapped in square brackets []. Both kinds of…
A closure is a function that remembers the variables of the scope where it was created, and can still use them after the outer function has returned. makeAdder(2) finishes and returns, but the function (int y) = x + y it returned still holds x…
All three relate classes, but they differ in whether you get code: - extends inherits from a parent class for code reuse. A class can extend only one class. - implements treats a class as a contract/interface and you override all its methods with no…
A StatelessWidget depends only on its constructor arguments (and any InheritedWidgets it reads); a StatefulWidget adds a long-lived State object that holds mutable data while the widget stays in the tree. - StatelessWidget: build() runs again when the parent rebuilds it with new arguments, or…
BuildContext is a widget's "address" in the tree — under the hood it is that widget's Element. Every "look up the tree" operation goes through a context: Theme.of(context), MediaQuery.of(context), Navigator.of(context), ScaffoldMessenger.of(context). Because of(context) searches upward from the context's position, which context you use decides the…
Flutter splits the UI into three trees with different jobs: the widget is the description, the element is the "live instance" that holds position and state, and the render object does layout and painting. - Widget tree: immutable, cheap objects recreated on every build(). A…
A Key lets Flutter recognise "this is still the same widget" when its position among its siblings changes. An element compares the new and old widgets with Widget.canUpdate: same runtimeType and same key. Without keys, Flutter matches only by type + position. A common bug:…
ValueKey identifies widgets by a specific value; two widgets with equal values are treated as the same. ObjectKey uses an object's reference identity; each distinct object gets its own key. UniqueKey always creates a unique identity, useful when every instance should be different. When to…
A const constructor creates the object at compile time, and two identical const expressions return the same instance (canonicalization). That is where the Flutter benefit comes from: when a parent rebuilds, the element compares the new widget with the old one. If the child is…
Flutter layout follows one rule: constraints go down, sizes go up, the parent sets the position. Row lays children out horizontally and Column vertically; that direction is the main axis, and the other one is the cross axis. In the first Row, Flutter lays out…
setState() tells the framework that data inside a State changed: it runs the callback synchronously, then marks the element dirty (markNeedsBuild). Flutter calls build() again on the next frame rather than immediately, and several setState calls in the same frame cause only one build. Rules…
The lifecycle belongs to the State object, not the widget: widgets are recreated constantly, while State lives from insertion into the tree until it is removed for good. The methods in order: - initState() — called once on insert (create controllers, listeners). - didChangeDependencies() —…
dispose() is the last hook in the lifecycle of a State: the framework calls it when the widget is permanently removed from the tree, and that State never builds again. Anything you create or register in initState and do not clean up here outlives the…
RepaintBoundary isolates a subtree into its own paint layer. When a widget needs to repaint, Flutter repaints from the nearest RepaintBoundary above it; if no boundary is in between, the whole region sharing that layer is repainted, including parts that did not change. The spinner…
State management is how you handle data that changes in your app (user input, API results) and how you tell the UI to redraw when that data changes. In Flutter, UI is a function of state: when state changes, widgets rebuild. The Flutter docs split…
setState() works well for a single widget's state. In complex apps it runs into four problems: - Wide rebuilds: setState() rebuilds that widget's entire subtree. Put state high up (the whole page) and a small change rebuilds the whole page. - Logic mixed with UI:…
Provider is a Flutter state management package that wraps InheritedWidget to pass an object down the widget tree and tell descendants when it changes. The most common pair is ChangeNotifier (holds the state, calls notifyListeners()) and ChangeNotifierProvider (puts that object in the tree). CartModel is…
Riverpod (Provider's spiritual successor, by the same author) is fully type-safe, doesn't depend on BuildContext, and declares providers as global variables instead of placing them in the widget tree. Main differences from Provider: - Errors at compile time instead of runtime. With Provider, context.read<Counter() with…
BLoC (Business Logic Component) separates logic from UI in one direction: the UI sends events in, the BLoC processes them and emits new states, and the UI simply renders the state. The usual packages are bloc + flutterbloc. On the UI side: BlocProvider provides the…
The three most common options differ mainly in how much architecture they impose: Provider Riverpod BLoC / Cubit ------------ What it is Wrapper around InheritedWidget + ChangeNotifier A rewrite of Provider, independent of BuildContext Event → state flow built on Stream Missing provider ProviderNotFoundException at…
ChangeNotifier is a Flutter class that lets an object notify whoever is listening when its data changes. You extend it, change state, then call notifyListeners(); every registered listener runs. increment() changes value, then notifyListeners() wakes every listener. A widget using ListenableBuilder, or Provider through ChangeNotifierProvider,…
Consumer watches an entire provider and rebuilds whenever it changes (every notifyListeners()). Selector lets you watch only a specific part of the state: Each time the provider notifies, Selector re-runs selector and compares the new result with the previous one; it only calls builder when…
GetIt is a service locator for Dart and Flutter: you register objects once at app startup, then retrieve them anywhere by their type. registerSingleton always returns the same instance; registerFactory creates a new instance on each call. GetIt is not state management. It does not…
The general rule: state logic should be plain Dart classes that do not need a BuildContext, so you can unit test them without building widgets. Each library has its own tool: - Provider (the provider package): a ChangeNotifier is just a regular Dart class. Create…
Navigator 1.0 is an imperative API: you push and pop each screen on the stack yourself. Navigator 2.0 (the Router API) is declarative: the screen stack is derived from app state, usually the URL. gorouter is the Flutter team's package that wraps the Router API…
GoRouter (the gorouter package) is a routing package maintained by the Flutter team and suggested by the Flutter docs over named routes. Instead of managing the Navigator stack by hand with push/pop, you declare every route as a URL once, then navigate by path, e.g.…
There are three main ways; pick based on whether the data needs to live in the URL: - Constructor (old Navigator): simple and type-safe, but the target screen has no URL, so it cannot be deep linked. - Path parameters (GoRouter): the data lives in…
A deep link opens the app directly on a specific screen: tapping https://shop.example.com/product/42 in an email opens product 42 instead of the home screen. There are two kinds: custom schemes (myapp://product/42, which any app can register) and domain-verified https links: App Links on Android, Universal…
GoRouter keeps navigation state in one place: the current URL is the state. Each time it builds a screen it passes a GoRouterState with the path, path parameters, query string and extra. Conditional navigation (login, permissions) lives in redirect. When the user opens /orders/42?tab=items, GoRouter…
ShellRoute wraps a group of routes in a shared UI frame, e.g. a Scaffold with a bottom navigation bar. The inner screen changes, the frame stays. Child routes are shown on one navigator owned by the shell, separate from the root navigator. The common confusion:…
With GoRouter, an auth guard is the redirect callback: GoRouter calls it before every navigation. Return a path and the user is sent there; return null and navigation continues as normal. Put it on the GoRouter constructor and one place protects every route. Reading top…
Named routes in Navigator 1.0 means declaring a routes: {'/product': ...} map on MaterialApp and calling Navigator.pushNamed(context, '/product'). That is fine for a small app. In a large app three problems show up, and Flutter's own docs now don't recommend named routes for most apps.…
Clean architecture separates code into layers: Presentation (UI), Domain (business logic, entities, use cases), and Data (repositories, data sources). What makes it clean architecture is the dependency rule: outer layers may depend on inner layers, never the other way around. Presentation calls Domain, Data implements…
A repository is a layer between the app's logic and its data sources (API, cache, database). The rest of the app just calls repository.getUser(id) and does not know where the data comes from. The interface lives in the domain layer, the implementation in the data…
Dependency injection (DI) means passing dependencies (services, repositories) into a class from the outside, instead of letting the class create them inside. The first version is hard-wired to ApiUserRepository, so a test calls the real API. The second only knows the UserRepository interface, so the…
A singleton guarantees the whole app has one instance of a class; everyone who asks gets that same object. A factory is the opposite: every request gets a new instance. In Flutter you meet both most often when registering dependencies with getit. getIt<UserRepository() returns the…
MVVM (Model–View–ViewModel) splits a screen into three parts: the Model is data and business logic (repositories, APIs), the View is widgets that only render, and the ViewModel holds the screen's state and exposes functions such as load() and toggle(). The View listens to the ViewModel…
A use case (the Flutter docs also call it an interactor) is a class that encapsulates one business action of the app, e.g. LoginUseCase, PlaceOrderUseCase. It lives in the domain layer, between the UI (a ViewModel or BLoC calls it) and repositories (where data comes…
Common structure: presentation/ for UI and BLoCs/ViewModels, domain/ for entities, repository interfaces and use cases, data/ for API clients, local DB and repository implementations, plus a shared folder for app config and constants. As the app grows, group by feature first, layer second: each feature…
An entity is a business concept in the domain layer, for example a User with an id and a name. It is a plain Dart class that knows nothing about JSON or the API. A model is the shape of the data in the data…
Records are lightweight, immutable aggregates bundling multiple values (positional or named fields) without needing a class. Example: (String, int) fetchUser() = ('Alice', 30); then destructure with var (name, age) = fetchUser();. They replace Map/List workarounds for multi-return functions with type-safe, concise syntax.
Pattern matching (Dart 3) lets you check whether a value has the expected "shape" and pull its data out into variables (destructuring) in one step. It pairs with sealed classes: when the parent is declared sealed, the compiler knows the full list of subclasses. Circle(radius:…
A switch expression returns a value directly and must be exhaustive (cover every case). Each case is written as pattern = value, cases are separated by commas, and is the default case: A switch statement does not return a value, so it fits side effects…
Impeller is Flutter's rendering engine that replaces Skia: the part that turns widget paint commands into GPU commands. Impeller is the only engine on iOS (the default since Flutter 3.10), and the default on Android API 29+ since Flutter 3.27. The problem with Skia is…
By default Flutter Web uses dart2js: Dart is compiled to JavaScript, which the browser must parse and then JIT-compile. With WebAssembly (Wasm), the dart2wasm compiler emits a .wasm file, a binary format the browser decodes and compiles straight to machine code, with no JS parsing…
Tree-shaking is a build step that automatically removes the parts of the app that are never used. For Dart code, the compiler starts from main() and follows the calls to learn which classes and functions can run; anything it cannot reach is left out of…
Flutter Web compiles Dart to JS (or WebAssembly when building with --wasm) and paints the whole UI onto a canvas through the CanvasKit or Skwasm renderer (using WebGL), instead of building a DOM tree per element like React or Vue. The old HTML renderer was…
Most UI code carries over, but desktop differs from mobile in input, resizable windows and packaging and distribution. This handles two things mobile never needs. Shortcuts change per OS, because Mac users expect Cmd and Windows users expect Ctrl. The layout uses LayoutBuilder (the real…
The http package is a simple HTTP client for basic requests. Dio is a more feature-rich client: - Interceptors: handle every request, response and error in one place for logging, adding auth tokens automatically, and retries. - Shared config: BaseOptions with baseUrl, headers and fine-grained…
Use the firebaseauth package (after Firebase.initializeApp()): call signInWithEmailAndPassword to sign in, and listen to auth state via the authStateChanges() stream — the correct way to know if a user is signed in. Common pitfalls: 1. Checking currentUser right at startup. Firebase needs a moment to…
InheritedWidget is a special widget that lets descendant widgets read data from an ancestor without threading it manually through every constructor. It is the underlying mechanism that Provider, Theme, and MediaQuery are all built on. Picture it as a "broadcast station" placed high in the…
Both are widgets that "rebuild themselves as async data arrives"; they differ in how many values the source emits: - FutureBuilder listens to a Future — a source that emits exactly one result then completes. Fits one-shot operations: an API call for product detail, reading…
Hero produces the effect of a single shared element "flying" seamlessly between two screens during navigation — for example a small list thumbnail expanding into a large image on the detail page. Users perceive it as the same object moving, not two different widgets. Usage:…
MediaQuery exposes information about the current display environment: screen size (size), orientation, devicePixelRatio, safe-area padding (notch, status bar via padding/viewPadding), and text scaling (textScaler). It is the data source for layouts that adapt to the device. Distinguish two often-conflated ideas: - Responsive: the same layout…