If you’re preparing for a Flutter interview and not sure where to start, this series is for you.
In this blog series, I’ll be sharing a wide range of Flutter interview questions that are commonly asked from basic concepts to more advanced topics. The goal is simple to help you build confidence, strengthen your understanding, and get you fully prepared to crack your next interview.
This won’t be just a single post. The series will consist of multiple blogs, each focusing on different areas of Flutter, so make sure you follow along and read them carefully. Consistency is key, and by going through each part, you’ll gradually develop a solid grasp of what interviewers are really looking for.
Let’s get started and take one step closer to your Flutter job!
SECTION 1: What is Flutter, Its Architecture, How It Works
Q1. What is Flutter?
Answer: Flutter is Google's open-source UI toolkit for building beautiful, natively compiled applications for mobile (Android & iOS), web, desktop (Windows, macOS, Linux), and embedded devices -- all from a single codebase. It uses Dart as its programming language. Unlike other cross-platform frameworks like React Native that use a bridge to communicate with native components, Flutter has its own rendering engine called Skia (now Impeller) that draws every pixel on the screen directly, giving you full control over every pixel and consistent UI across platforms.
Q2. Explain Flutter's architecture in detail.
Answer: Flutter's architecture has three main layers:
-
Framework Layer (Dart) -- This is the topmost layer that we developers interact with. It includes:
- Material & Cupertino -- Pre-built widgets following Material Design and iOS design guidelines
- Widgets Layer -- The core building blocks (Text, Row, Column, Container, etc.)
- Rendering Layer -- Handles layout, painting, and hit testing
- Foundation Layer -- Basic utility classes and building blocks
-
Engine Layer (C/C++) -- This is the core of Flutter. It includes:
- Skia/Impeller -- The 2D rendering engine that draws UI onto a canvas
- Dart Runtime -- Executes Dart code
- Text Layout -- Handles text rendering
- Platform Channels -- For communicating with native code
Embedder Layer (Platform-specific) -- This is the platform-specific layer that embeds the Flutter engine into the host platform (Android, iOS, Windows, etc.). It handles things like surface creation, accessibility, and input events.
Q3. How does Flutter render UI? How is it different from React Native?
Answer: Flutter does NOT use native platform UI components at all. Instead, Flutter uses its own rendering engine (Skia, and now Impeller for iOS/Android) to paint every single pixel on a canvas. When you write a Text widget, Flutter is literally drawing that text pixel by pixel using its rendering engine, not delegating to a native TextView or UILabel.
In contrast, React Native uses a JavaScript bridge to communicate with native platform components. When you write <Text> in React Native, it gets converted to a native TextView on Android or UILabel on iOS.
This is why Flutter gives you pixel-perfect consistency across platforms -- because the same rendering engine is drawing the UI on every platform. It also means better performance because there's no bridge overhead.
Q4. What is the Dart VM and how does Flutter compile code?
Answer: Flutter uses two compilation modes:
During development (Debug mode): Flutter uses Dart's JIT (Just-In-Time) compilation. The Dart VM compiles code on the fly. This is what enables Hot Reload -- you can inject updated source code into the running Dart VM without restarting the app.
For release builds (Release mode): Flutter uses AOT (Ahead-Of-Time) compilation. Dart code is compiled directly into native ARM/x86 machine code. There's no Dart VM, no interpreter, no bridge. This gives near-native performance.
This dual compilation strategy gives us the best of both worlds -- fast development cycles and high release performance.
Q5. What is the Flutter rendering pipeline?
Answer: When Flutter renders a frame, it goes through these steps:
-
Build Phase -- The
build()method is called, creating/updating the Widget tree - Layout Phase -- The framework walks the Render tree, each RenderObject determines its size and position. Constraints go down, sizes go up.
- Paint Phase -- Each RenderObject paints itself onto a canvas (layers)
- Compositing Phase -- The layer tree is sent to the engine
- Rasterization -- Skia/Impeller converts the layer tree into actual GPU commands and pixels on screen
Flutter targets 60fps (or 120fps on supported devices), so this entire pipeline runs in under 16ms per frame.
Q6. What are Platform Channels in Flutter?
Answer: Platform Channels are Flutter's mechanism for communicating with native platform code (Java/Kotlin for Android, Swift/Objective-C for iOS). There are three types:
- MethodChannel -- For calling methods. Most commonly used. You invoke a method by name and get a result back. Example: calling native camera API.
- EventChannel -- For streaming data from native to Flutter. Example: listening to sensor data continuously.
- BasicMessageChannel -- For passing messages back and forth using a custom codec.
The communication is asynchronous and uses binary message passing with codecs (StandardMessageCodec, JSONMessageCodec, etc.) to serialize/deserialize data.
Q7. What is Impeller? How is it different from Skia?
Answer: Impeller is Flutter's new rendering engine that replaces Skia. The key differences:
- Skia compiles shaders at runtime, which can cause shader compilation jank -- the first time a particular visual effect is rendered, there's a stutter while the shader compiles.
- Impeller pre-compiles all shaders during the build process. This eliminates shader jank completely. It also uses Metal on iOS and Vulkan on Android for optimal GPU performance.
Impeller is now the default renderer on iOS and Android in recent Flutter versions.
Q8. What is the difference between Flutter SDK and Dart SDK?
Answer: The Dart SDK is the SDK for the Dart programming language. It includes the Dart compiler, Dart VM, core libraries (dart:core, dart:async, dart:io, etc.), and the dart command-line tool.
The Flutter SDK includes the Dart SDK within it plus the entire Flutter framework -- widgets, rendering engine, development tools (flutter doctor, flutter run), DevTools, and the embedder code for each platform. So when you install Flutter, you automatically get Dart. You don't need to install Dart separately.
Q9. What are the different build modes in Flutter?
Answer: Flutter has three build modes:
Debug Mode -- Uses JIT compilation. Hot reload enabled. Assertions enabled. DevTools enabled. Debug banner shown. Slower performance. Used during development.
Profile Mode -- Uses AOT compilation like release, but keeps some debugging tools enabled (like DevTools and Observatory). Used to analyze performance. Not available on emulators.
Release Mode -- Uses AOT compilation. No debugging, no assertions, no DevTools. Maximum optimization. Smallest app size. This is what goes to the app store.
Q10. What is "Everything is a Widget" philosophy in Flutter?
Answer: In Flutter, virtually everything you see on screen is a Widget. A button is a widget. Text is a widget. Padding is a widget. An alignment is a widget. Even the app itself is a widget. This is fundamentally different from other frameworks where you have views, layouts, and controllers.
Flutter uses composition over inheritance. Instead of having a "text with padding and alignment" superclass, you compose: Center( child: Padding( child: Text('Hello'))). Each concern is its own widget. This makes the framework highly flexible and modular.
SECTION 2: Dart Language Fundamentals
Q1. What is Dart? Why did Flutter choose Dart?
Answer: Dart is Google's programming language optimized for building UIs. Flutter chose Dart for several reasons:
- Both AOT and JIT compilation -- JIT enables hot reload during development; AOT gives native performance in production
- No bridge needed -- Dart compiles to native code directly, unlike JavaScript which needs a bridge
- Single-threaded event loop with Isolates -- Avoids race conditions common with shared-memory multithreading
- Garbage collection optimized for UI -- Dart's GC is designed for short-lived object allocation patterns common in UI frameworks (widgets are frequently created and destroyed)
- Strong typing with type inference -- Catches bugs at compile time while remaining concise
- Google controls both Dart and Flutter -- They can optimize Dart specifically for Flutter's needs
Q2. What is the difference between var, final, const, dynamic, and late in Dart?
Answer:
var-- Type is inferred at assignment and cannot change.var name = 'John';is inferred asString. You can reassign it (name = 'Jane';) but cannot change its type.dynamic-- Explicitly opts out of type checking. The variable can hold any type and can change types.dynamic x = 'hello'; x = 42;is valid. Avoid using it unless absolutely necessary because you lose type safety.final-- The variable can only be set once. It's a runtime constant.final now = DateTime.now();is valid because the value is determined at runtime. You cannot reassign it after initialization.const-- A compile-time constant. The value must be known at compile time.const pi = 3.14;is valid.const now = DateTime.now();is NOT valid becauseDateTime.now()can only be evaluated at runtime.constobjects are deeply immutable and canonicalized (only one instance in memory).late-- Defers initialization. The variable is non-nullable but doesn't have to be initialized at declaration.late String name;-- you promise the compiler you'll assign it before using it. If you access it before assignment, you get aLateInitializationErrorat runtime. Also used for lazy initialization:late final value = expensiveComputation();-- the computation runs only whenvalueis first accessed.
Q3. What is Null Safety in Dart? Explain sound null safety.
Answer: Null Safety, introduced in Dart 2.12, means that by default, variables cannot be null. This eliminates an entire class of null reference errors at compile time.
-
Non-nullable by default:
String name = 'John';-- cannot be null -
Nullable type:
String? name;-- the?suffix means it CAN be null -
Null assertion:
name!-- tells the compiler "I'm sure this isn't null." Throws at runtime if it is. -
Null-aware operators:
name?.length(returns null if name is null),name ?? 'default'(provides fallback),name ??= 'default'(assigns if null)
Sound null safety means the type system guarantees at compile time that a non-nullable variable will NEVER be null. The compiler statically verifies this through flow analysis. For example:
String? name;
if (name != null) {
print(name.length); // Dart promotes name to String (non-nullable) inside this block
}
This is called type promotion. The compiler is smart enough to know that inside the if block, name cannot be null.
Q4. What are the basic data types in Dart?
Answer: Dart has the following built-in types:
-
int-- 64-bit integers:int age = 25; -
double-- 64-bit floating point:double price = 9.99; -
num-- Supertype of both int and double:num value = 42; -
String-- UTF-16 strings. Single or double quotes. String interpolation with$variableor${expression}. -
bool--trueorfalse -
List-- Ordered collection (like arrays):List<int> nums = [1, 2, 3]; -
Map-- Key-value pairs:Map<String, int> ages = {'John': 25}; -
Set-- Unordered collection of unique items:Set<int> nums = {1, 2, 3}; -
Runes-- Unicode code points of a String -
Symbol-- Represents an operator or identifier -
Null-- The type ofnull -
Record-- (Dart 3.0+) Fixed-size, heterogeneous collection:(String, int) record = ('John', 25);
Everything in Dart is an object. Even int and bool are objects that inherit from Object.
Q5. What is the difference between final and const?
Answer: This is one of the most asked questions. The key differences:
| Feature | final |
const |
|---|---|---|
| When value is set | Runtime | Compile-time |
| Reassignment | Not allowed | Not allowed |
| Example |
final time = DateTime.now(); (valid) |
const time = DateTime.now(); (INVALID) |
| Object mutability | The reference is final, but object contents CAN change | Everything is deeply, transitively immutable |
| Canonicalization | No | Yes -- identical const objects share the same memory |
| Instance variables | Can be final in a class |
Cannot be const (use static const) |
A practical example:
final list1 = [1, 2, 3];
list1.add(4); // OK -- the list contents can change
// list1 = [5, 6]; // ERROR -- can't reassign
const list2 = [1, 2, 3];
// list2.add(4); // ERROR -- const list is unmodifiable
In Flutter, prefer const constructors for widgets whenever possible because const widgets are not rebuilt, improving performance.
Q6. What are type inference and type promotion in Dart?
Answer:
Type Inference: Dart can automatically infer the type of a variable from its value. When you write var name = 'John';, Dart infers the type as String. You don't need to explicitly write String name = 'John';. This works with var, final, and const.
Type Promotion: Dart's flow analysis automatically promotes a nullable type to non-nullable when it can prove the value isn't null:
void greet(String? name) {
if (name == null) return;
// Here, 'name' is automatically promoted to String (non-nullable)
print(name.length); // No error, no need for name!.length
}
Type promotion also works with type checks:
void process(Object obj) {
if (obj is String) {
print(obj.length); // obj is promoted to String
}
}
Note: Type promotion works with local variables but NOT with class fields or global variables, because another thread could theoretically change them between the null check and usage.
Q7. What are String interpolation and multi-line strings in Dart?
Answer:
String Interpolation: You can embed expressions inside strings using $ for simple variables and ${} for expressions:
String name = 'Flutter';
print('Hello $name'); // Hello Flutter
print('Length: ${name.length}'); // Length: 7
print('Upper: ${name.toUpperCase()}'); // Upper: FLUTTER
Multi-line Strings: Use triple quotes:
String text = '''
This is a
multi-line string
''';
Raw Strings: Prefix with r to avoid escape sequences:
String path = r'C:\Users\folder'; // backslashes treated literally
Q8. What is the required keyword in Dart?
Answer: The required keyword is used with named parameters to make them mandatory. Without required, named parameters are optional by default.
// Without required -- age is optional
void greet({String? name, int? age}) {}
// With required -- both must be provided
void greet({required String name, required int age}) {}
greet(name: 'John', age: 25); // Must provide both
In Flutter, you see this extensively in widget constructors:
class MyWidget extends StatelessWidget {
final String title;
const MyWidget({super.key, required this.title});
}
Q9. What are positional vs named parameters in Dart?
Answer:
Positional Parameters -- Order matters, no labels needed:
void greet(String name, int age) {}
greet('John', 25);
Optional Positional Parameters -- Wrapped in []:
void greet(String name, [int age = 0]) {}
greet('John'); // age defaults to 0
Named Parameters -- Wrapped in {}, order doesn't matter, must use labels:
void greet({required String name, int age = 0}) {}
greet(name: 'John', age: 25);
greet(age: 25, name: 'John'); // Order doesn't matter
Flutter almost exclusively uses named parameters in widget constructors for readability.
Q10. What are Dart's cascade operator (..) and spread operator (...)?
Answer:
Cascade Operator (..): Allows you to perform multiple operations on the same object without repeating the object reference:
var paint = Paint()
..color = Colors.red
..strokeWidth = 5.0
..style = PaintingStyle.stroke;
Without cascade, you'd need paint.color = ...; paint.strokeWidth = ...; on separate lines.
The null-aware cascade ?.. is used when the object might be null.
Spread Operator (...): Inserts all elements of a collection into another collection:
var list1 = [1, 2, 3];
var list2 = [0, ...list1, 4]; // [0, 1, 2, 3, 4]
The null-aware spread ...? handles null collections:
List<int>? maybeList;
var combined = [1, ...?maybeList, 2]; // [1, 2]
SECTION 3: Dart OOP - Classes, Abstract Classes, Mixins, Extensions, Generics
Q1. Explain classes and constructors in Dart.
Answer: Dart is a fully object-oriented language. Every class implicitly extends Object.
class Person {
String name;
int age;
// Default constructor with initializing formals
Person(this.name, this.age);
// Named constructor
Person.guest() : name = 'Guest', age = 0;
// Factory constructor -- can return existing instance or subtype
factory Person.fromJson(Map<String, dynamic> json) {
return Person(json['name'], json['age']);
}
// Redirecting constructor
Person.baby(String name) : this(name, 0);
}
Key constructor types:
-
Default constructor --
Person(this.name, this.age);uses initializing formals (shorthand) -
Named constructor --
Person.guest()-- Dart doesn't support method overloading, so named constructors serve that purpose -
Factory constructor -- Uses
factorykeyword. Doesn't always create a new instance. Can return cached objects or subtypes. Doesn't have access tothis. -
Const constructor --
const Person(this.name);where all fields arefinal. Enables compile-time constant objects.
Q2. What is the difference between an Abstract Class and an Interface in Dart?
Answer: In Dart, there is no interface keyword. Every class implicitly defines an interface. The distinction is how you use them:
Abstract Class:
abstract class Animal {
String name; // Can have fields with state
Animal(this.name); // Can have constructors
void breathe() { // Can have implemented methods
print('Breathing...');
}
void makeSound(); // Abstract method -- no body
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
void makeSound() => print('Bark!');
}
Class as Interface (using implements):
class Flyable {
void fly() => print('Flying');
}
class Bird implements Flyable {
@override
void fly() => print('Bird flying'); // MUST override ALL methods
}
Key differences:
-
extends(abstract class) -- You inherit implementation. You only override abstract methods. -
implements(interface) -- You promise to provide ALL methods. No implementation is inherited. - A class can
extendonly ONE class butimplementMULTIPLE classes.
Q3. What are Mixins in Dart? How are they different from abstract classes?
Answer: Mixins are a way to reuse code across multiple class hierarchies. They solve the problem that Dart only supports single inheritance.
mixin Swimming {
void swim() => print('Swimming');
}
mixin Flying {
void fly() => print('Flying');
}
class Duck extends Animal with Swimming, Flying {
// Duck now has swim() and fly() without inheriting from multiple classes
}
Key rules:
- Declare with
mixinkeyword (ormixin classif it should also work as a regular class) - Cannot be instantiated directly (
Swimming()is invalid for a pure mixin) - Can have implemented methods and fields
- Can use
onkeyword to restrict which classes can use the mixin:mixin Swimming on Animal {}-- only subclasses of Animal can use Swimming - A class can use multiple mixins with the
withkeyword - Order matters: If two mixins define the same method, the LAST one wins (linearization)
Difference from abstract class:
- Abstract class uses single inheritance (
extends). Mixin allows multiple (with). - Abstract class can have constructors. Mixins cannot (pure
mixin). - Use abstract classes for "is-a" relationships. Use mixins for "can-do" capabilities.
Q4. What are Extension Methods in Dart?
Answer: Extensions let you add new functionality to existing classes without modifying them or creating subclasses. Introduced in Dart 2.7.
extension StringExtension on String {
String capitalize() {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1)}';
}
bool get isEmail => contains('@') && contains('.');
}
// Usage
print('hello'.capitalize()); // Hello
print('test@email.com'.isEmail); // true
You can extend any type, including built-in types:
extension IntExtension on int {
Duration get seconds => Duration(seconds: this);
Duration get minutes => Duration(minutes: this);
}
// Usage
await Future.delayed(5.seconds);
In Flutter, extension methods are widely used to add utility methods to BuildContext, String, DateTime, etc.
Q5. What are Generics in Dart? Why are they important?
Answer: Generics allow you to write code that works with different types while maintaining type safety.
// Generic class
class Box<T> {
T value;
Box(this.value);
}
var intBox = Box<int>(42);
var stringBox = Box<String>('hello');
// Generic method
T getFirst<T>(List<T> items) => items.first;
// Bounded generics -- constrain the type
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
double get doubled => value * 2.0;
}
// NumberBox<String>('hi'); // ERROR -- String doesn't extend num
Generics are important because:
- Type safety -- Errors caught at compile time, not runtime
- Code reuse -- Write once, use with any type
-
Performance -- Dart's generics are reified, meaning type information is preserved at runtime (unlike Java's type erasure).
List<int>andList<String>are truly different types at runtime.
Flutter uses generics extensively: State<T>, ValueNotifier<T>, FutureBuilder<T>, StreamBuilder<T>, etc.
Q6. What is the difference between extends, implements, and with in Dart?
Answer:
| Keyword | Purpose | Inherits Implementation? | Multiple? |
|---|---|---|---|
extends |
Inherit from a class | Yes | No (single inheritance) |
implements |
Promise to implement an interface | No (must override everything) | Yes (multiple interfaces) |
with |
Use a mixin | Yes | Yes (multiple mixins) |
abstract class Animal {
void breathe() => print('Breathing');
void makeSound();
}
mixin CanSwim {
void swim() => print('Swimming');
}
class Printable {
void printInfo() => print('Info');
}
class Dog extends Animal with CanSwim implements Printable {
@override
void makeSound() => print('Bark');
@override
void printInfo() => print('I am a dog');
// breathe() is inherited from Animal
// swim() is inherited from CanSwim
}
The order must always be: extends -> with -> implements.
Q7. What are sealed classes in Dart 3?
Answer: Sealed classes, introduced in Dart 3.0, restrict which classes can extend or implement them. Only classes in the same library (same file) can extend a sealed class.
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Rectangle extends Shape {
final double width, height;
Rectangle(this.width, this.height);
}
The major benefit is exhaustive pattern matching in switch expressions:
double area(Shape shape) => switch (shape) {
Circle(radius: var r) => 3.14 * r * r,
Rectangle(width: var w, height: var h) => w * h,
// No default needed! Compiler knows all subtypes
};
If you add a new subtype to Shape, the compiler will warn you everywhere you have a switch that doesn't handle the new case. This is extremely useful for state management.
Q8. What are Enums in Dart? What are enhanced enums?
Answer:
Simple enum:
enum Color { red, green, blue }
var c = Color.red;
Enhanced enums (Dart 2.17+): Enums can have fields, methods, constructors, and implement interfaces:
enum Planet {
mercury(3.7),
earth(9.8),
mars(3.7);
final double gravity;
const Planet(this.gravity);
String get description => 'Planet $name has gravity $gravity m/s²';
}
print(Planet.earth.gravity); // 9.8
print(Planet.earth.description); // Planet earth has gravity 9.8 m/s²
Enhanced enums are very useful in Flutter for defining things like theme modes, app states, and navigation routes with associated data.
Q9. What is the difference between a Factory Constructor and a Named Constructor?
Answer:
Named Constructor:
- Always creates a new instance
- Has access to
this - Uses initializer lists
class Logger {
final String name;
Logger.named(this.name); // Named constructor
}
Factory Constructor:
- Does NOT always create a new instance -- can return a cached/existing instance
- Does NOT have access to
this - Can return a subtype
- Must explicitly return an instance
class Logger {
static final Map<String, Logger> _cache = {};
final String name;
Logger._internal(this.name); // Private constructor
factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}
}
var a = Logger('app');
var b = Logger('app');
print(identical(a, b)); // true -- same instance (Singleton pattern)
Factory constructors are commonly used for the Singleton pattern, caching, and returning subtypes from a base class.
Q10. What is operator overloading in Dart?
Answer: Dart allows you to define how operators work for your custom classes using the operator keyword:
class Vector {
final double x, y;
const Vector(this.x, this.y);
Vector operator +(Vector other) => Vector(x + other.x, y + other.y);
Vector operator -(Vector other) => Vector(x - other.x, y - other.y);
Vector operator *(double scalar) => Vector(x * scalar, y * scalar);
@override
bool operator ==(Object other) =>
other is Vector && x == other.x && y == other.y;
@override
int get hashCode => Object.hash(x, y);
}
var a = Vector(1, 2);
var b = Vector(3, 4);
var c = a + b; // Vector(4, 6)
When you override ==, always override hashCode as well. Dart supports overloading these operators: +, -, *, /, ~/, %, <, >, <=, >=, ==, [], []=, ~, <<, >>, |, &, ^.
SECTION 4: Dart Async - Future, Stream, async/await, Isolates
Q1. What is a Future in Dart?
Answer: A Future represents a value that will be available at some point in the future. It's Dart's version of a Promise (from JavaScript). A Future can be in one of three states:
- Uncompleted -- The async operation is still running
- Completed with a value -- Success
- Completed with an error -- Failure
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Data loaded';
}
// Using async/await
void main() async {
String data = await fetchData();
print(data);
}
// Using .then()
fetchData().then((data) => print(data)).catchError((e) => print(e));
In Flutter, Futures are used everywhere -- API calls, database queries, file I/O, SharedPreferences, etc.
Q2. What is the difference between async/await and .then()?
Answer: Both handle Futures, but their style differs:
.then() -- Callback style:
fetchUser()
.then((user) => fetchOrders(user.id))
.then((orders) => processOrders(orders))
.catchError((e) => handleError(e));
async/await -- Synchronous-looking style:
try {
var user = await fetchUser();
var orders = await fetchOrders(user.id);
processOrders(orders);
} catch (e) {
handleError(e);
}
async/await is syntactic sugar over .then(). Under the hood, they do the same thing. But async/await is preferred because:
- More readable, especially with multiple sequential async operations
- Error handling with
try/catchis cleaner than.catchError() - Easier to debug -- stack traces are more meaningful
Q3. What is a Stream in Dart? How is it different from a Future?
Answer:
- A Future delivers a single value (or error) asynchronously.
- A Stream delivers a sequence of values (or errors) asynchronously over time.
Think of a Future as ordering one pizza and waiting. A Stream is like a subscription -- data keeps arriving over time.
// Creating a Stream
Stream<int> countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // yield sends a value on the stream
}
}
// Listening to a Stream
countStream().listen(
(value) => print(value), // onData
onError: (e) => print(e), // onError
onDone: () => print('Done'), // onDone
);
There are two types of Streams:
- Single-subscription Stream -- Can only be listened to once. Default. Example: reading a file.
- Broadcast Stream -- Can be listened to by multiple listeners. Example: button clicks.
var controller = StreamController<int>.broadcast();
controller.stream.listen((v) => print('Listener 1: $v'));
controller.stream.listen((v) => print('Listener 2: $v'));
In Flutter, Streams are used with StreamBuilder, StreamController, BLoC pattern, Firebase Realtime Database, WebSockets, etc.
Q4. What are async* and yield in Dart?
Answer:
-
asyncmakes a function return aFuture -
async*makes a function return aStream(asynchronous generator) -
sync*makes a function return anIterable(synchronous generator) -
yieldemits a single value -
yield*delegates to another generator (forwards all its values)
// Async generator -- returns Stream
Stream<int> numbers() async* {
yield 1;
await Future.delayed(Duration(seconds: 1));
yield 2;
yield* moreNumbers(); // delegates to another stream
}
// Sync generator -- returns Iterable
Iterable<int> range(int start, int end) sync* {
for (int i = start; i <= end; i++) {
yield i;
}
}
for (var n in range(1, 5)) {
print(n); // 1, 2, 3, 4, 5
}
Q5. What is an Isolate in Dart? How is it different from a Thread?
Answer: An Isolate is Dart's model for concurrency. Unlike threads in languages like Java or C++, Isolates do NOT share memory. Each Isolate has its own memory heap and event loop.
Key differences from Threads:
- No shared memory -- Isolates communicate by passing messages (via ports), not by accessing shared variables
- No locks/mutexes needed -- Since there's no shared state, there are no race conditions
- Heavier than threads -- Each Isolate has its own memory space
// Simple Isolate usage with compute()
Future<int> heavyComputation(int input) async {
// This runs in a separate isolate
return await Isolate.run(() {
int sum = 0;
for (int i = 0; i < input; i++) {
sum += i;
}
return sum;
});
}
In Flutter, use Isolates for:
- Heavy JSON parsing
- Image processing
- Complex mathematical computations
- Anything that takes more than ~16ms (to avoid dropping frames)
Flutter's compute() function is a convenience wrapper that spawns an Isolate, runs a function, returns the result, and kills the Isolate.
Q6. What is the Event Loop in Dart?
Answer: Dart is single-threaded and uses an event loop to handle asynchronous operations. The event loop has two queues:
Microtask Queue -- Higher priority. Tasks scheduled with
scheduleMicrotask()orFuture.microtask(). Processed FIRST before moving to the event queue.Event Queue -- Lower priority. I/O events, timers, UI events, Future callbacks (
.then()). Processed after all microtasks are done.
The execution order:
- Synchronous code runs first (to completion)
- All microtasks are processed
- One event from the event queue is processed
- All microtasks again (if any new ones were added)
- Next event, and so on...
void main() {
print('1 - Main');
Future(() => print('4 - Event Queue'));
scheduleMicrotask(() => print('3 - Microtask'));
print('2 - Main');
}
// Output: 1, 2, 3, 4
Understanding this is crucial for Flutter because the UI thread runs on this event loop. If you block it with synchronous computation, the UI freezes.
Q7. What is Future.wait, Future.any, and Future.forEach?
Answer:
Future.wait -- Waits for ALL futures to complete. Returns a list of results. If any future fails, the whole thing fails.
var results = await Future.wait([
fetchUser(),
fetchOrders(),
fetchSettings(),
]);
// results[0] = user, results[1] = orders, results[2] = settings
Future.any -- Returns the result of the FIRST future to complete. Others are ignored.
var fastest = await Future.any([
fetchFromServer1(),
fetchFromServer2(),
]);
Future.forEach -- Executes an async operation sequentially for each element:
await Future.forEach(urls, (url) async {
await downloadFile(url);
});
Future.delayed -- Creates a future that completes after a delay:
var result = await Future.delayed(Duration(seconds: 2), () => 'Done');
Q8. How do you handle errors in async Dart code?
Answer: There are multiple approaches:
1. try/catch/finally with async/await:
try {
var data = await fetchData();
} on SocketException catch (e) {
// Specific exception
print('Network error: $e');
} on FormatException {
// Another specific exception
print('Bad format');
} catch (e, stackTrace) {
// Catch all others
print('Error: $e');
print('Stack: $stackTrace');
} finally {
// Always runs
print('Cleanup');
}
2. .catchError() with Futures:
fetchData()
.then((data) => process(data))
.catchError((e) => handleError(e))
.whenComplete(() => cleanup());
3. Stream error handling:
stream.listen(
(data) => process(data),
onError: (e) => handleError(e),
cancelOnError: false, // continue listening after error
);
Best practice: Always handle errors in async code. Unhandled Future errors can crash the app.
Q9. What is a StreamController? When would you use one?
Answer: A StreamController lets you create and manage a Stream programmatically. You can add data, errors, and close the stream.
class CounterBloc {
final _controller = StreamController<int>();
int _count = 0;
Stream<int> get stream => _controller.stream;
void increment() {
_count++;
_controller.sink.add(_count);
}
void dispose() {
_controller.close(); // Always close to prevent memory leaks!
}
}
Use a broadcast StreamController when multiple listeners need the same stream:
final _controller = StreamController<int>.broadcast();
StreamControllers are the foundation of the BLoC pattern in Flutter and are used whenever you need to manually push data into a stream.
Q10. What is the difference between then, whenComplete, and catchError?
Answer:
-
.then(onValue)-- Called when the Future completes successfully. Receives the result. -
.catchError(onError)-- Called when the Future completes with an error. Receives the error. -
.whenComplete(action)-- Called when the Future completes either way (success or error). Likefinally. Doesn't receive the result or error.
fetchData()
.then((data) {
print('Success: $data');
})
.catchError((error) {
print('Error: $error');
})
.whenComplete(() {
print('This always runs, like finally');
});
They can be chained because each returns a new Future. The async/await equivalent is:
try {
var data = await fetchData(); // .then
print('Success: $data');
} catch (error) { // .catchError
print('Error: $error');
} finally { // .whenComplete
print('This always runs');
}
SECTION 5: Dart Collections - List, Map, Set, Iterable
Q1. What is the difference between List, Set, and Map in Dart?
Answer:
| Feature | List | Set | Map |
|---|---|---|---|
| Order | Ordered (by index) | Unordered (LinkedHashSet preserves insertion order) | Keys are unordered (LinkedHashMap preserves insertion order) |
| Duplicates | Allowed | NOT allowed | Keys unique, values can duplicate |
| Access | By index list[0]
|
By value/contains | By key map['name']
|
| Use case | Ordered collection | Unique items, membership testing | Key-value associations |
var list = [1, 2, 2, 3]; // [1, 2, 2, 3] -- duplicates kept
var set = {1, 2, 2, 3}; // {1, 2, 3} -- duplicates removed
var map = {'a': 1, 'b': 2}; // key-value pairs
By default, Dart uses List (growable array), LinkedHashSet (insertion-ordered Set), and LinkedHashMap (insertion-ordered Map).
Q2. What are the common List operations in Dart?
Answer:
var list = [3, 1, 4, 1, 5];
// Adding
list.add(9); // [3, 1, 4, 1, 5, 9]
list.addAll([2, 6]); // [..., 2, 6]
list.insert(0, 0); // [0, 3, 1, 4, 1, 5, 9, 2, 6]
// Removing
list.remove(1); // Removes first occurrence of 1
list.removeAt(0); // Removes element at index 0
list.removeLast(); // Removes last element
list.removeWhere((e) => e > 5); // Removes all > 5
// Accessing
list.first; // First element
list.last; // Last element
list.length; // Size
list.isEmpty; // Is empty?
list.contains(4); // true
// Transforming
list.map((e) => e * 2); // Iterable of doubled values
list.where((e) => e > 2); // Filter
list.any((e) => e > 4); // true if any element matches
list.every((e) => e > 0); // true if all elements match
list.reduce((a, b) => a + b); // Sum all elements
list.fold(0, (sum, e) => sum + e); // Sum with initial value
// Sorting
list.sort(); // In-place sort
list.sort((a, b) => b.compareTo(a)); // Descending
// Sublist
list.sublist(1, 3); // Elements from index 1 to 2
// Collection-if and collection-for
var newList = [
1,
if (true) 2, // Conditional inclusion
for (var i in [3, 4]) i, // Loop inclusion
];
Q3. What are collection-if and collection-for in Dart?
Answer: These are Dart's collection literals features that let you use if and for inside collection definitions:
Collection-if:
bool isLoggedIn = true;
var nav = [
'Home',
'Products',
if (isLoggedIn) 'Profile', // Only included if true
if (isLoggedIn) 'Logout' else 'Login',
];
Collection-for:
var numbers = [1, 2, 3];
var doubled = [
for (var n in numbers) n * 2, // [2, 4, 6]
];
These are very useful in Flutter for building widget lists conditionally:
Column(
children: [
Text('Always shown'),
if (hasError) Text('Error message'),
for (var item in items) ListTile(title: Text(item)),
],
)
This is much cleaner than using ternary operators or separate builder methods.
Q4. What is the difference between Iterable and List in Dart?
Answer:
Iterable is the base class for all collections that can be iterated. List, Set, and the results of .map(), .where() are all Iterable.
Key difference: Iterable is lazy, List is eager.
var list = [1, 2, 3, 4, 5];
// .where() returns an Iterable (lazy)
var filtered = list.where((e) => e > 2);
// Nothing is computed yet!
// .toList() forces evaluation
var filteredList = filtered.toList(); // [3, 4, 5]
Iterable does NOT support index access (iterable[0] is invalid). You must use .elementAt(0) or convert to a List.
Iterable<int> iterable = [1, 2, 3];
// iterable[0]; // ERROR
iterable.elementAt(0); // OK, returns 1
iterable.first; // OK, returns 1
Lazy evaluation is efficient when you chain multiple operations:
// Only iterates once, not three times
var result = list
.where((e) => e > 2)
.map((e) => e * 10)
.take(2)
.toList(); // [30, 40]
Q5. How do you create an unmodifiable/immutable collection?
Answer:
// Unmodifiable List
var list = List.unmodifiable([1, 2, 3]);
// list.add(4); // Throws UnsupportedError at runtime
// Using const
const constList = [1, 2, 3];
// constList.add(4); // Throws UnsupportedError at runtime
// Unmodifiable Map
var map = Map.unmodifiable({'a': 1, 'b': 2});
// Unmodifiable view (wraps existing collection)
var original = [1, 2, 3];
var view = List.unmodifiable(original);
In Flutter, using const collections and const constructors is a best practice because:
- They are created at compile time (zero runtime cost)
- They are canonicalized (same
const= same object in memory) - Flutter skips rebuilding widgets with
constconstructors
Q6. What is the Map class and its common operations?
Answer:
// Creating Maps
var map = {'name': 'John', 'age': 25};
var map2 = Map<String, int>();
var map3 = Map.fromIterables(['a', 'b'], [1, 2]);
// Adding/Updating
map['email'] = 'john@mail.com'; // Add
map.addAll({'city': 'NYC'}); // Add multiple
map.update('age', (v) => v + 1); // Update existing
map.putIfAbsent('role', () => 'user'); // Add only if key doesn't exist
// Accessing
map['name']; // 'John' (null if key doesn't exist)
map.keys; // Iterable of keys
map.values; // Iterable of values
map.entries; // Iterable of MapEntry
map.containsKey('name'); // true
map.containsValue(25); // true
map.length; // Number of entries
// Removing
map.remove('age'); // Remove by key
map.removeWhere((k, v) => v == 25);
// Iterating
map.forEach((key, value) => print('$key: $value'));
for (var entry in map.entries) {
print('${entry.key}: ${entry.value}');
}
// Transforming
var mapped = map.map((k, v) => MapEntry(k.toUpperCase(), v));
Q7. How does Set work in Dart and when should you use it?
Answer: A Set is an unordered collection of unique elements. Dart's default Set is LinkedHashSet, which preserves insertion order.
var set = {1, 2, 3, 4, 5};
// Adding
set.add(6); // {1, 2, 3, 4, 5, 6}
set.add(3); // {1, 2, 3, 4, 5, 6} -- no duplicate added
// Set operations
var a = {1, 2, 3, 4};
var b = {3, 4, 5, 6};
a.union(b); // {1, 2, 3, 4, 5, 6}
a.intersection(b); // {3, 4}
a.difference(b); // {1, 2}
// Checking membership
set.contains(3); // true -- O(1) lookup!
set.containsAll({1, 2}); // true
// Converting
var list = [1, 2, 2, 3, 3, 3];
var unique = list.toSet().toList(); // [1, 2, 3] -- remove duplicates
Use Set when:
- You need to ensure uniqueness
- You need fast
contains()lookups (O(1) vs O(n) for List) - You need set operations (union, intersection, difference)
Q8. What is the whereType, expand, and fold method?
Answer:
whereType<T>() -- Filters elements by type:
var mixed = [1, 'hello', 2, 'world', 3];
var strings = mixed.whereType<String>(); // ('hello', 'world')
var ints = mixed.whereType<int>(); // (1, 2, 3)
expand() -- Flattens nested collections (like flatMap):
var nested = [[1, 2], [3, 4], [5]];
var flat = nested.expand((list) => list).toList(); // [1, 2, 3, 4, 5]
// Also useful for duplicating
var list = [1, 2, 3];
var doubled = list.expand((e) => [e, e]).toList(); // [1, 1, 2, 2, 3, 3]
fold() -- Reduces a collection to a single value with an initial value:
var nums = [1, 2, 3, 4, 5];
var sum = nums.fold(0, (prev, element) => prev + element); // 15
var product = nums.fold(1, (prev, element) => prev * element); // 120
// More flexible than reduce() because you can change the type
var sentence = ['Hello', 'World'];
String result = sentence.fold('', (prev, word) => '$prev $word'.trim());
// 'Hello World'
Q9. What are Records in Dart 3 and how do they relate to collections?
Answer: Records are anonymous, immutable, aggregate types introduced in Dart 3.0. They let you bundle multiple values without creating a class.
// Positional record
(String, int) person = ('John', 25);
print(person.$1); // John
print(person.$2); // 25
// Named fields
({String name, int age}) person = (name: 'John', age: 25);
print(person.name); // John
// Great for returning multiple values from functions
(String, int) getUserInfo() {
return ('John', 25);
}
var (name, age) = getUserInfo(); // Destructuring
Records work well with collections:
var people = [
(name: 'John', age: 25),
(name: 'Jane', age: 30),
];
people.sort((a, b) => a.age.compareTo(b.age));
Records are value types -- two records with the same values are equal:
print((1, 2) == (1, 2)); // true
Q10. What is pattern matching with collections in Dart 3?
Answer: Dart 3 introduced destructuring patterns that work beautifully with collections:
// List destructuring
var list = [1, 2, 3];
var [a, b, c] = list; // a=1, b=2, c=3
// Rest patterns
var [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
// Map destructuring
var map = {'name': 'John', 'age': 25};
var {'name': name, 'age': age} = map;
// Switch with patterns
var list = [1, 2, 3];
switch (list) {
case [1, 2, 3]:
print('Exact match');
case [1, ...]:
print('Starts with 1');
case [_, _, _]:
print('Any 3 elements');
}
// If-case
if (json case {'name': String name, 'age': int age}) {
print('$name is $age years old');
}
This is extremely useful for JSON parsing and API response handling in Flutter.
SECTION 6: Flutter Widget Basics - StatelessWidget vs StatefulWidget, Widget Lifecycle
Q1. What is a Widget in Flutter?
Answer: A Widget is the fundamental building block of Flutter's UI. Everything in Flutter is a widget -- text, buttons, padding, layout, even the app itself. A widget is an immutable description of part of the UI.
Key points:
- Widgets are immutable -- once created, they cannot change. If you want the UI to change, you create a new widget.
- Widgets are lightweight -- they are just configuration objects (blueprints). They are cheap to create and destroy.
- Widgets form a tree -- every widget has a parent (except the root) and can have children.
- Widgets are declarative -- you describe WHAT the UI should look like, not HOW to update it.
Text('Hello World') // This is a widget
Container(
padding: EdgeInsets.all(8),
child: Text('Hello'),
) // This is also a widget, containing another widget
Q2. What is the difference between StatelessWidget and StatefulWidget?
Answer:
StatelessWidget:
- Has no mutable state
- The
build()method depends only on the constructor parameters - Once built, it cannot change itself (parent must rebuild it with new parameters)
- More performant because Flutter can optimize it better
- Use for static/presentational UI
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('Hello, $name');
}
}
StatefulWidget:
- Has mutable state that can change during the widget's lifetime
- Consists of TWO classes: the widget class (immutable) and the State class (mutable)
- Calling
setState()triggers a rebuild - Use when the UI needs to change dynamically (user interaction, animations, data loading)
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => setState(() => _count++),
child: Text('Count: $_count'),
);
}
}
Why are they split into two classes? Because widgets are immutable and recreated frequently. The State object is long-lived and persists across rebuilds, preserving your data.
Q3. Explain the complete lifecycle of a StatefulWidget.
Answer: The lifecycle methods are called in this order:
createState()-- Called once when the widget is inserted into the tree. Creates the State object.initState()-- Called once after the State object is created. Use it for one-time initialization: subscribing to streams, initializing controllers, fetching initial data. Always callsuper.initState()first.didChangeDependencies()-- Called immediately afterinitState()and whenever anInheritedWidgetthat this widget depends on changes. Use it for things that depend onBuildContext(likeMediaQuery.of(context),Theme.of(context)).build()-- Called every time the widget needs to render. Must return a Widget. Called afterinitState,didChangeDependencies,setState, anddidUpdateWidget. Should be pure (no side effects) and fast.didUpdateWidget(oldWidget)-- Called when the parent rebuilds and provides a new widget of the same type with different parameters. Use it to respond to parameter changes (e.g., restart an animation if a parameter changed).setState()-- Not a lifecycle method, but calling it marks the State as dirty and schedules a rebuild (callsbuild()again).deactivate()-- Called when the State is removed from the tree temporarily (e.g., moved to a different location using GlobalKey).dispose()-- Called when the State is permanently removed from the tree. Use it for cleanup: cancel subscriptions, dispose controllers, close streams. Always callsuper.dispose()last.
class _MyWidgetState extends State<MyWidget> {
late StreamSubscription _sub;
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_sub = myStream.listen((data) => setState(() {}));
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Safe to use context here
final theme = Theme.of(context);
}
@override
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.id != oldWidget.id) {
// Parent passed a different id, reload data
_loadData();
}
}
@override
void dispose() {
_sub.cancel();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Container();
}
Q4. When should you use StatelessWidget vs StatefulWidget?
Answer:
Use StatelessWidget when:
- The widget displays static content that depends only on its constructor parameters
- The UI doesn't change after it's built
- Examples:
Text,Icon, a custom card that displays data passed to it
Use StatefulWidget when:
- The widget has internal mutable state
- The UI changes in response to user interaction, timers, or data updates
- You need lifecycle methods (initState, dispose) for resource management
- Examples: Forms, checkboxes, animations, screens that fetch data
Best practice: Start with StatelessWidget. Convert to StatefulWidget only when you need mutable state. Better yet, consider state management solutions (Provider, Riverpod, BLoC) that keep your widgets stateless while managing state externally.
Q5. What is setState() and how does it work?
Answer: setState() is a method available in State<T> that tells the Flutter framework "my state has changed, please rebuild this widget."
setState(() {
_counter++;
});
What happens when you call setState():
- The callback runs synchronously (mutates state)
- The State object is marked as "dirty"
- The framework schedules a rebuild for the next frame
- On the next frame,
build()is called again - Flutter diffs the old and new widget trees and updates only what changed
Important rules:
- Never call setState() in
build()-- causes infinite loop - Never call setState() after
dispose()-- causes error. Guard withif (mounted) setState(() {}); - The callback must be synchronous. Don't make it async:
// WRONG
setState(() async {
_data = await fetchData();
});
// RIGHT
_data = await fetchData();
setState(() {});
- setState() only affects the current widget and its subtree, not the parent or siblings
Q6. What is the difference between const constructors and regular constructors for widgets?
Answer:
Regular constructor:
Text('Hello') // New instance created every rebuild
Const constructor:
const Text('Hello') // Same instance reused across rebuilds
When a widget uses a const constructor:
- It's created at compile time (zero runtime cost)
- Flutter skips rebuilding it entirely -- even if the parent rebuilds
- Only one instance exists in memory for identical const widgets
This is a significant performance optimization. Example:
Column(
children: [
const Text('Static Title'), // Never rebuilds
Text('Count: $_count'), // Rebuilds when _count changes
const SizedBox(height: 16), // Never rebuilds
const Icon(Icons.star), // Never rebuilds
],
)
Best practice: Always use const wherever possible. The Dart analyzer can warn you when you can add const but haven't. Enable the prefer_const_constructors lint rule.
Q7. What is a GlobalKey? When would you use it?
Answer: A GlobalKey uniquely identifies a widget across the entire app (not just within a parent). It provides access to the widget's State and BuildContext from anywhere.
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(children: [
TextFormField(validator: (v) => v!.isEmpty ? 'Required' : null),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
},
child: Text('Submit'),
),
]),
)
Common use cases:
-
Form validation -- Accessing
FormStateto validate/save -
Navigating without context --
navigatorKey.currentState!.pushNamed('/home') - Moving a widget between different parts of the tree while preserving its state
- Accessing State from outside the widget
Caution: GlobalKeys are expensive because they require global bookkeeping. Don't use them unnecessarily. Prefer ValueKey, ObjectKey, or UniqueKey for list items.
Q8. What are Keys in Flutter? Why are they important?
Answer: Keys control how Flutter matches widgets between rebuilds. Without keys, Flutter matches widgets by their type and position in the tree.
// Without key -- Flutter matches by position
// If items reorder, state goes to the wrong widget!
Column(children: items.map((item) => ItemWidget(item)).toList())
// With key -- Flutter matches by key
Column(children: items.map((item) => ItemWidget(key: ValueKey(item.id), item)).toList())
Types of keys:
-
ValueKey(value)-- Uses a specific value for identity (ID, name, etc.) -
ObjectKey(object)-- Uses object identity -
UniqueKey()-- Always unique, forces rebuild -
GlobalKey()-- Globally unique, accesses State/context -
PageStorageKey-- Preserves scroll position
When to use keys:
- When reordering, adding, or removing items in a list
- When you have multiple widgets of the same type and they have state
- In
ListView,AnimatedList,ReorderableListView
Rule of thumb: If your list items are StatefulWidgets or have animations, always use keys.
Q9. What is the widget property in State? What is mounted?
Answer:
widget property: Inside a State class, widget refers to the associated StatefulWidget instance. It gives you access to the widget's parameters.
class MyWidget extends StatefulWidget {
final String title;
const MyWidget({super.key, required this.title});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return Text(widget.title); // Access widget's parameters via widget.title
}
}
When the parent rebuilds with new parameters, widget is updated to point to the new widget instance, and didUpdateWidget is called.
mounted property: A boolean that indicates whether the State object is currently in the widget tree.
Future<void> _loadData() async {
var data = await api.fetchData();
if (mounted) { // Check before calling setState
setState(() => _data = data);
}
}
After dispose() is called, mounted becomes false. Calling setState() on an unmounted State throws an error. Always check mounted before calling setState() after an async operation.
Q10. What is InheritedWidget?
Answer: InheritedWidget is a special widget that efficiently propagates data down the widget tree. It's the mechanism behind Theme.of(context), MediaQuery.of(context), and state management solutions like Provider.
class MyTheme extends InheritedWidget {
final Color primaryColor;
const MyTheme({
super.key,
required this.primaryColor,
required super.child,
});
static MyTheme of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyTheme>()!;
}
@override
bool updateShouldNotify(MyTheme oldWidget) {
return primaryColor != oldWidget.primaryColor;
}
}
// Usage anywhere in the subtree:
var color = MyTheme.of(context).primaryColor;
How it works:
- Data flows DOWN the tree (parent to descendants)
- When
updateShouldNotifyreturns true, all dependents are rebuilt -
context.dependOnInheritedWidgetOfExactType<T>()registers the widget as a dependent -- it will be rebuilt when the InheritedWidget changes - It's O(1) lookup, not O(n) tree traversal -- the framework maintains a map of InheritedWidgets
Provider, Riverpod, and BLoC all build on top of InheritedWidget internally.
SECTION 7: BuildContext, Element Tree, Widget Tree, RenderObject Tree
Q1. What is BuildContext?
Answer: BuildContext is a reference to the location of a widget in the widget tree. Technically, every BuildContext IS an Element object -- it's the Element's handle.
It's used to:
-
Find ancestor widgets:
Theme.of(context),Navigator.of(context),MediaQuery.of(context) -
Find inherited data:
context.dependOnInheritedWidgetOfExactType<T>() -
Get the size after layout:
context.size -
Show overlays:
showDialog(context: context, ...)
Important rules:
-
Context refers to THIS widget's position, not its children. So
Theme.of(context)inside aThemewidget won't find that Theme -- it finds the one ABOVE. - Don't use context in
initState()for things that depend on InheritedWidgets. UsedidChangeDependencies()instead orWidgetsBinding.instance.addPostFrameCallback. - Each widget has its own BuildContext.
@override
Widget build(BuildContext context) {
// This context is for this widget, looking UP the tree
var theme = Theme.of(context); // Finds Theme ancestor
var screenWidth = MediaQuery.of(context).size.width;
return ElevatedButton(
onPressed: () {
Navigator.of(context).push(...); // Uses context to find Navigator
ScaffoldMessenger.of(context).showSnackBar(...);
},
child: Text('Click me'),
);
}
Q2. What are the three trees in Flutter?
Answer: Flutter maintains three parallel trees:
1. Widget Tree:
- The tree of Widget objects you write in your
build()method - Widgets are immutable and lightweight (just configuration/blueprints)
- Recreated frequently (every rebuild)
- Acts as a blueprint for the other two trees
2. Element Tree:
- One Element per Widget
- Elements are mutable and long-lived -- they persist across rebuilds
- Each Element holds a reference to its Widget and its RenderObject
- The Element is what
BuildContextactually is - Responsible for managing the lifecycle and connecting widgets to render objects
- When a widget rebuilds, the Element checks if the new widget can update the existing one (same type & key) or needs to create a new Element
3. RenderObject Tree:
- Handles layout, painting, and hit testing
- Each RenderObject knows its size, position, and how to paint itself
- Not every widget has a RenderObject -- only
RenderObjectWidgetsubclasses (likePadding,Container,SizedBox). Layout-free widgets likeStatelessWidgetandStatefulWidgetdon't create RenderObjects directly. - The actual pixels on screen come from this tree
The flow: Widget tree (blueprint) -> Element tree (manager) -> RenderObject tree (renderer).
Q3. How does Flutter's reconciliation (diffing) algorithm work?
Answer: When a widget rebuilds, Flutter walks the Element tree and compares old and new widgets at each position:
Same runtimeType AND same key? -> Update the existing Element. Call
update()on the Element, which updates the RenderObject. This is cheap.Different runtimeType OR different key? -> Unmount the old Element (and its entire subtree) and create a new Element from the new Widget. This is expensive.
// Example: Swapping two widgets WITHOUT keys
// Flutter sees same type at same position -> updates (WRONG behavior for stateful)
Column(children: [
WidgetA(), // position 0
WidgetB(), // position 1
])
// After swap:
Column(children: [
WidgetB(), // position 0 -- Flutter tries to update old WidgetA with WidgetB
WidgetA(), // position 1 -- Flutter tries to update old WidgetB with WidgetA
])
// With keys -- Flutter correctly matches and moves elements
Column(children: [
WidgetA(key: ValueKey('a')),
WidgetB(key: ValueKey('b')),
])
This is why keys matter for lists with stateful widgets.
Q4. What is the difference between createElement() and createRenderObject()?
Answer:
createElement()-- Defined on the Widget class. Creates an Element that manages this widget's position in the tree. Every widget has this. StatelessWidget creates aStatelessElement, StatefulWidget creates aStatefulElement.createRenderObject()-- Defined onRenderObjectWidgetsubclasses. Creates the RenderObject that handles layout and painting. Only widgets that directly affect rendering implement this (likePaddingcreates aRenderPadding,SizedBoxcreates aRenderConstrainedBox).
The hierarchy:
Widget
├── StatelessWidget (has StatelessElement, no RenderObject)
├── StatefulWidget (has StatefulElement, no RenderObject)
├── InheritedWidget (has InheritedElement, no RenderObject)
└── RenderObjectWidget (has RenderObjectElement + RenderObject)
├── SingleChildRenderObjectWidget (e.g., Padding, Align, SizedBox)
├── MultiChildRenderObjectWidget (e.g., Column, Row, Stack)
└── LeafRenderObjectWidget (e.g., RichText, RawImage)
Q5. How does the layout algorithm work in Flutter?
Answer: Flutter uses a single-pass layout algorithm with the rule: Constraints go down, Sizes go up, Parent sets position.
Constraints go down: A parent tells its child the minimum and maximum width/height it can be. This is a
BoxConstraintsobject withminWidth,maxWidth,minHeight,maxHeight.Sizes go up: The child chooses its own size within those constraints and reports back.
Parent sets position: The parent decides where to place the child using an offset.
Parent: "You can be 0-300px wide and 0-600px tall" (constraints DOWN)
Child: "I'll be 200px wide and 100px tall" (size UP)
Parent: "I'll place you at offset (50, 50)" (position by PARENT)
This is why you sometimes get overflow errors -- when a child can't fit within the constraints given by its parent.
Example: If a Row gives its children unconstrained width (0 to infinity) and a child tries to be infinitely wide, you get a layout error. This is a common mistake when putting ListView inside Column without wrapping it in Expanded or SizedBox.
Q6. What is RenderObject and how does painting work?
Answer: RenderObject is responsible for layout (determining size and position) and painting (drawing pixels to the screen).
Key methods:
-
performLayout()-- Calculates size and lays out children -
paint(PaintingContext context, Offset offset)-- Draws the widget onto a canvas -
hitTest()-- Determines if a touch/click hits this render object
Most developers don't interact with RenderObjects directly. Instead, they use widgets like CustomPaint for custom drawing:
CustomPaint(
painter: MyPainter(),
child: Container(),
)
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.blue
..strokeWidth = 4;
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
The painting happens in layers. Each layer can be independently composited by the GPU, which is how Flutter achieves smooth animations.
Q7. What is context.findRenderObject() and when would you use it?
Answer: context.findRenderObject() returns the RenderObject associated with a widget. You can use it to get the widget's actual size and position after layout.
final key = GlobalKey();
Container(key: key, child: Text('Hello'))
// After build, in a callback:
WidgetsBinding.instance.addPostFrameCallback((_) {
final renderBox = key.currentContext!.findRenderObject() as RenderBox;
final size = renderBox.size; // Actual rendered size
final position = renderBox.localToGlobal(Offset.zero); // Position on screen
print('Size: $size, Position: $position');
});
Important: This only works AFTER the widget has been laid out. Don't call it in build() or initState(). Use it in addPostFrameCallback or in response to user interactions.
Common use cases:
- Getting widget position for custom overlays/tooltips
- Measuring widget size for adaptive layouts
- Custom hit testing
Q8. What is context.dependOnInheritedWidgetOfExactType vs context.findAncestorWidgetOfExactType?
Answer:
dependOnInheritedWidgetOfExactType<T>():
- Finds the nearest InheritedWidget of type T
- Registers a dependency -- the widget will rebuild when the InheritedWidget changes
- Used by
Theme.of(context),MediaQuery.of(context), etc. - Only works with InheritedWidget subclasses
findAncestorWidgetOfExactType<T>():
- Finds the nearest ancestor of ANY widget type T
- Does NOT register a dependency -- won't rebuild when the ancestor changes
- O(n) tree walk -- can be slow
- Use sparingly
findAncestorStateOfType<T>():
- Finds the State object of an ancestor StatefulWidget
- Also does NOT register a dependency
// Registers dependency -- rebuilds when Theme changes
var theme = Theme.of(context); // uses dependOnInheritedWidgetOfExactType internally
// No dependency -- just a one-time lookup
var scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
Rule: Use dependOn... for data you need to stay in sync with. Use findAncestor... for one-time lookups where you don't need reactivity.
Q9. What happens when you call setState() internally? Walk through the full process.
Answer: Here's the complete internal flow:
setState(fn)is called -- The callbackfnruns synchronously, mutating stateElement marked dirty --
_element!.markNeedsBuild()is called, adding this Element to a "dirty elements" listFrame scheduled -- If not already scheduled,
SchedulerBinding.scheduleFrame()requests a new frame from the platform-
On next vsync (frame):
-
Build phase: The framework calls
buildScope(), which rebuilds all dirty elements by calling theirbuild()method. New widget tree is created. - Reconciliation: The Element tree is walked. For each position, old widget is compared to new widget (by type and key). Elements are updated, created, or removed.
-
Layout phase: Dirty RenderObjects run
performLayout(). Constraints flow down, sizes flow up. -
Paint phase: Dirty RenderObjects run
paint(). Layers are created/updated. - Compositing phase: The layer tree is sent to the engine for GPU compositing.
- Rasterization: Skia/Impeller turns layers into pixels on screen.
-
Build phase: The framework calls
Frame displayed -- The GPU presents the frame. Total time must be under 16ms for 60fps.
Only the dirty subtree is rebuilt, not the entire app. This is why Flutter can be efficient despite rebuilding widget trees.
Q10. What is RepaintBoundary?
Answer: RepaintBoundary is a widget that creates a separate painting layer for its subtree. When something inside the boundary needs repainting, ONLY that layer is repainted -- not the rest of the tree.
RepaintBoundary(
child: MyExpensiveWidget(), // Repaints independently
)
Without RepaintBoundary, when any widget in a layer needs to repaint, the entire layer repaints. RepaintBoundary isolates repainting.
Use cases:
- Animations -- Wrap animated widgets so they don't cause the entire screen to repaint
- Scrolling lists -- ListView automatically adds RepaintBoundary to each item
- Static content next to dynamic content -- wrap the static part
You can see repaint regions by enabling debugRepaintRainbowEnabled = true in DevTools. Each repaint flashes a different color.
Don't overuse RepaintBoundary -- each one adds a layer, consuming GPU memory. Use it when profiling shows excessive repainting.
SECTION 8: MaterialApp, Scaffold, AppBar Basics
Q1. What is MaterialApp in Flutter?
Answer: MaterialApp is the top-level widget for a Material Design app. It wraps your app with several essential widgets and configurations.
MaterialApp(
title: 'My App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
home: HomeScreen(),
routes: {
'/settings': (context) => SettingsScreen(),
},
debugShowCheckedModeBanner: false,
)
What MaterialApp provides:
- Navigator -- For routing and navigation (push/pop screens)
- Theme -- Material Design theming (colors, fonts, shapes)
- Localization -- Internationalization support
- MediaQuery -- Screen size, orientation, text scale factor
- Title -- App title shown in task switchers
- Route management -- Named routes and route generation
Without MaterialApp, you'd have no Navigator, no Theme, and many Material widgets wouldn't work. For Cupertino (iOS-style) apps, use CupertinoApp instead.
Q2. What is the difference between MaterialApp and WidgetsApp?
Answer: WidgetsApp is the base class. MaterialApp extends it with Material Design features.
WidgetsApp:
- Provides basic app infrastructure: Navigator, routes, localization
- No Material Design theme or widgets
- Use it for fully custom-designed apps that don't follow Material Design
MaterialApp:
- Everything WidgetsApp provides, PLUS
- Material Design theme (
ThemeData) - Material-specific widgets (AppBar, FloatingActionButton, etc.)
-
Scaffold,Drawer,BottomNavigationBarsupport -
ScaffoldMessengerfor SnackBars
CupertinoApp:
- Everything WidgetsApp provides, PLUS
- iOS-style Cupertino theme
- iOS-specific widgets
Most Flutter apps use MaterialApp even if they have a custom design because it provides convenient infrastructure.
Q3. What is Scaffold in Flutter?
Answer: Scaffold provides the basic Material Design visual layout structure. It gives you slots for the most common app UI elements.
Scaffold(
appBar: AppBar(
title: Text('My App'),
actions: [IconButton(icon: Icon(Icons.search), onPressed: () {})],
),
body: Center(child: Text('Hello World')),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
drawer: Drawer(child: ListView(children: [...])),
bottomNavigationBar: BottomNavigationBar(items: [...]),
bottomSheet: Container(...),
backgroundColor: Colors.white,
)
Scaffold provides:
- appBar -- Top app bar
- body -- Main content area
- floatingActionButton -- FAB
- drawer / endDrawer -- Side navigation panels
- bottomNavigationBar -- Bottom navigation
- bottomSheet -- Persistent bottom sheet
-
snackBar -- Via
ScaffoldMessenger.of(context).showSnackBar()
Each screen in your app typically has its own Scaffold.
Q4. What is AppBar and how do you customize it?
Answer: AppBar is a Material Design toolbar displayed at the top of a Scaffold.
AppBar(
leading: IconButton( // Left widget (back button by default)
icon: Icon(Icons.menu),
onPressed: () {},
),
title: Text('Page Title'), // Title widget
centerTitle: true, // Center the title (default varies by platform)
actions: [ // Right-side action buttons
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
],
elevation: 4, // Shadow
backgroundColor: Colors.blue,
foregroundColor: Colors.white, // Icon/text color
bottom: TabBar( // Widget below the AppBar (usually TabBar)
tabs: [Tab(text: 'Tab 1'), Tab(text: 'Tab 2')],
),
flexibleSpace: FlexibleSpaceBar(...), // Collapsible space (for SliverAppBar)
)
For scrolling/collapsing effects, use SliverAppBar inside a CustomScrollView:
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('Title'),
background: Image.network('url', fit: BoxFit.cover),
),
),
SliverList(delegate: SliverChildListDelegate([...]))
],
)
Q5. How do you show a SnackBar, Dialog, and BottomSheet?
Answer:
SnackBar:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Item deleted'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () => undoDelete(),
),
duration: Duration(seconds: 3),
),
);
AlertDialog:
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('Confirm'),
content: Text('Are you sure?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')),
TextButton(onPressed: () { doAction(); Navigator.pop(context); }, child: Text('OK')),
],
),
);
BottomSheet:
showModalBottomSheet(
context: context,
isScrollControlled: true, // For full-height sheets
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (context) => Container(
padding: EdgeInsets.all(16),
child: Column(mainAxisSize: MainAxisSize.min, children: [...]),
),
);
Note: Use ScaffoldMessenger.of(context) (not the old Scaffold.of(context)) for SnackBars. It works even if the context changes (e.g., after navigation).
Q6. What is SafeArea and why is it important?
Answer: SafeArea is a widget that adds padding to avoid system UI intrusions like the status bar, notch, home indicator, and rounded screen corners.
Scaffold(
body: SafeArea(
child: Column(
children: [
Text('This text avoids the notch and status bar'),
],
),
),
)
Without SafeArea, your content would be hidden behind the status bar on Android or the notch on iPhone. SafeArea uses MediaQuery.of(context).padding to determine the safe insets.
You can customize which sides to pad:
SafeArea(
top: true, // Avoid status bar/notch
bottom: true, // Avoid home indicator
left: false, // Don't pad left
right: false, // Don't pad right
child: ...,
)
Scaffold's AppBar already handles the top safe area, so you typically use SafeArea on the body content, or on screens without an AppBar.
Q7. What is MediaQuery and how is it used?
Answer: MediaQuery provides information about the device's screen and user preferences. It's an InheritedWidget, so accessing it registers a dependency (your widget rebuilds when values change).
@override
Widget build(BuildContext context) {
var mq = MediaQuery.of(context);
var screenWidth = mq.size.width; // Screen width
var screenHeight = mq.size.height; // Screen height
var orientation = mq.orientation; // Portrait or Landscape
var padding = mq.padding; // System UI insets (status bar, etc.)
var textScale = mq.textScaleFactor; // User's text size preference
var brightness = mq.platformBrightness; // Light or Dark mode
var viewInsets = mq.viewInsets; // Keyboard height (bottom)
var devicePixelRatio = mq.devicePixelRatio;
return screenWidth > 600
? TabletLayout()
: PhoneLayout();
}
Performance tip: MediaQuery.of(context) causes a rebuild whenever ANY MediaQuery property changes (including keyboard appearing). Use specific methods to only depend on what you need:
var size = MediaQuery.sizeOf(context); // Only rebuilds on size change
var padding = MediaQuery.paddingOf(context); // Only rebuilds on padding change
Q8. What is Theme and ThemeData in Flutter?
Answer: Theme applies a visual theme to the entire app or a subtree. ThemeData holds all the theme properties.
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
),
useMaterial3: true,
textTheme: TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
bodyMedium: TextStyle(fontSize: 14),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system, // Follows device setting
)
Access theme anywhere:
var colorScheme = Theme.of(context).colorScheme;
var textTheme = Theme.of(context).textTheme;
Text('Hello', style: textTheme.headlineLarge);
Container(color: colorScheme.primary);
Override theme for a subtree:
Theme(
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme.copyWith(primary: Colors.red),
),
child: ElevatedButton(...), // This button uses red as primary
)
Q9. What is the difference between Navigator.push and Navigator.pushNamed?
Answer:
Navigator.push -- Direct routing, pass the route object directly:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailScreen(id: 42)),
);
Navigator.pushNamed -- Named routing, use a string route name defined in MaterialApp:
// Define routes in MaterialApp
MaterialApp(
routes: {
'/': (context) => HomeScreen(),
'/detail': (context) => DetailScreen(),
},
)
// Navigate
Navigator.pushNamed(context, '/detail', arguments: 42);
// Receive arguments
var id = ModalRoute.of(context)!.settings.arguments as int;
Other Navigator methods:
-
Navigator.pop(context)-- Go back -
Navigator.pushReplacement(...)-- Replace current screen -
Navigator.pushAndRemoveUntil(...)-- Push and remove all previous screens (useful after login) -
Navigator.popUntil(...)-- Pop multiple screens
For production apps, consider using GoRouter or AutoRoute for declarative, type-safe routing.
Q10. What is MaterialApp.router and declarative routing?
Answer: Flutter supports two navigation approaches:
Imperative (Navigator 1.0):
Navigator.push(context, MaterialPageRoute(builder: (_) => Screen()));
You imperatively tell the navigator "push this screen." Simple but limited for deep linking and complex navigation.
Declarative (Navigator 2.0 / Router API):
MaterialApp.router(
routerConfig: GoRouter(
routes: [
GoRoute(path: '/', builder: (_, __) => HomeScreen()),
GoRoute(path: '/user/:id', builder: (_, state) => UserScreen(id: state.pathParameters['id']!)),
],
),
)
Declarative routing describes the navigation state as data. The framework handles transitions. Benefits:
- Deep linking works automatically
- Browser back/forward works on web
- Complex navigation patterns (nested navigation, redirects)
- Type-safe parameters
The most popular declarative routing package is GoRouter (maintained by the Flutter team).
SECTION 9: Hot Reload vs Hot Restart
Q1. What is Hot Reload in Flutter?
Answer: Hot Reload injects updated Dart source code into the running Dart VM without restarting the app. It preserves the app's current state (variables, navigation stack, scroll position).
How it works:
- You make a code change
- Press
rin the terminal (or save, if auto-reload is enabled) - The Dart VM loads the updated source files
- The Flutter framework rebuilds the widget tree
- You see the changes instantly -- state is preserved
What Hot Reload CAN do:
- Change widget build methods
- Add/remove widgets
- Change styles, colors, text
- Modify method implementations
What Hot Reload CANNOT do (requires Hot Restart):
- Changes to
initState()(already ran) - Changes to global/static variables (already initialized)
- Changes to
main()function - Changes to enum values
- Changing a StatelessWidget to StatefulWidget
- Changes to generic type arguments
Q2. What is Hot Restart?
Answer: Hot Restart recompiles the entire app from scratch and restarts it. All state is lost -- the app goes back to its initial state.
How it works:
- Press
R(capital R) in the terminal - The Dart VM destroys all existing state
- The app's
main()function runs again - Everything starts fresh
When to use Hot Restart instead of Hot Reload:
- You changed
main()or initialization code - You modified global variables or static fields
- You changed enum definitions
- Hot Reload didn't pick up your changes
- You changed something in
initState() - You added a new dependency in pubspec.yaml (actually requires full restart)
Q3. What is the difference between Hot Reload, Hot Restart, and Full Restart?
Answer:
| Feature | Hot Reload | Hot Restart | Full Restart |
|---|---|---|---|
| Speed | ~1 second | ~3-5 seconds | 30-60+ seconds |
| State preserved? | Yes | No | No |
| Runs main()? | No | Yes | Yes |
| Recompiles? | Incrementally | Full recompile in VM | Full native build |
| Shortcut | r |
R |
Stop + Run |
| Use case | UI tweaks | State/init changes | Native code changes, pubspec changes |
Full Restart is needed when:
- You change native code (Android/iOS)
- You add a new plugin in
pubspec.yaml - You modify
AndroidManifest.xmlorInfo.plist - You change build configuration
Q4. Why does Hot Reload work so fast?
Answer: Hot Reload is fast because of Dart's JIT (Just-In-Time) compilation in debug mode. Here's the process:
- Only the changed Dart source files are recompiled (incremental compilation)
- The updated code is injected into the already running Dart VM
- The VM replaces the old method implementations with new ones
- Flutter then calls
reassemble()on the root widget, which triggers the widget tree to rebuild using the new code - The existing State objects are preserved, so
initState()doesn't run again
This entire process happens in under a second because:
- No native code rebuild needed
- No app restart needed
- The Dart VM supports code hot-swapping
- Only the UI (widget tree) is rebuilt, not the entire app state
This is one of Flutter's biggest advantages for developer productivity.
Q5. What happens internally when Hot Reload is triggered?
Answer:
- File change detected -- The IDE or CLI detects saved file changes
- Incremental compilation -- Only changed files are recompiled to Dart kernel format
- Code injection -- The new kernel is sent to the Dart VM over a service protocol
- VM updates -- The Dart VM replaces changed classes and functions in memory
-
reassemble()called -- Flutter's binding callsreassemble()on the root Element -
Widget tree rebuilt -- Each Element calls its widget's
build()method with the new code - Reconciliation -- The framework diffs old and new widget trees, updating RenderObjects as needed
- Frame rendered -- The updated UI is painted and displayed
If any step fails (e.g., compile error), the error is shown in the console and the app continues running with the old code.
Q6. Can Hot Reload work with state management solutions like Provider or BLoC?
Answer: Yes, Hot Reload works well with state management solutions because:
- Provider/Riverpod -- State objects (ChangeNotifier, StateNotifier) are preserved. UI rebuilds with new code but existing state persists.
- BLoC -- BLoC instances are preserved. New event handlers apply on next event, but existing state remains.
- GetX -- Controllers persist across hot reload.
However, there are caveats:
- If you change the initial value of a state variable, Hot Reload won't apply it (the old value persists). Use Hot Restart.
- If you change the structure of a state class (add/remove fields), Hot Reload may fail or show stale data. Use Hot Restart.
- If you change stream transformations in a BLoC, you need Hot Restart for the new transformations to take effect.
Q7. Does Hot Reload work on release builds?
Answer: No. Hot Reload only works in debug mode because it depends on Dart's JIT compiler and the Dart VM's ability to hot-swap code at runtime.
In release mode, Dart code is compiled AOT (Ahead-Of-Time) to native machine code. There is no Dart VM, no JIT compiler, and no ability to inject new code. This gives better performance but loses hot reload capability.
Profile mode also does NOT support Hot Reload (it uses AOT compilation).
Q8. What are the common pitfalls of Hot Reload?
Answer:
Changed
initState()but state seems stale -- initState runs only once. Hot Reload doesn't re-run it. Solution: Hot Restart.Changed a global variable but old value persists -- Global/static variables are initialized once. Hot Reload doesn't reinitialize them. Solution: Hot Restart.
Changed enum values but app shows old ones -- Enums are compile-time constants. Solution: Hot Restart.
Added a new field to a class but it's null -- Existing instances don't get the new field initialized. Solution: Hot Restart.
Compile error breaks hot reload -- Fix the error and hot reload again. The app continues with old code until a successful reload.
Font/asset changes not appearing -- Asset changes sometimes need Hot Restart or even Full Restart.
SECTION 10: Flutter SDK Structure, pubspec.yaml
Q1. What is the structure of a Flutter project?
Answer:
my_app/
├── android/ # Android-specific native code and configuration
├── ios/ # iOS-specific native code and configuration
├── linux/ # Linux desktop configuration
├── macos/ # macOS desktop configuration
├── windows/ # Windows desktop configuration
├── web/ # Web-specific files (index.html)
├── lib/ # YOUR DART CODE LIVES HERE
│ └── main.dart # Entry point of the app
├── test/ # Unit and widget tests
├── integration_test/ # Integration tests
├── assets/ # Images, fonts, JSON files, etc.
├── pubspec.yaml # Project configuration and dependencies
├── pubspec.lock # Locked dependency versions
├── analysis_options.yaml # Lint rules
├── .metadata # Flutter project metadata
└── README.md # Documentation
The lib/ folder is where all your Dart code goes. A common structure inside lib/:
lib/
├── main.dart
├── app.dart
├── models/
├── screens/ (or pages/)
├── widgets/
├── services/
├── providers/ (or blocs/)
├── utils/
├── constants/
└── routes/
Q2. What is pubspec.yaml? Explain its sections.
Answer: pubspec.yaml is the configuration file for a Dart/Flutter project. It defines metadata, dependencies, assets, and fonts.
name: my_app # Package name (must be lowercase_with_underscores)
description: A sample app # Project description
version: 1.0.0+1 # Version (1.0.0) + build number (+1)
publish_to: 'none' # Don't publish to pub.dev
environment: # SDK version constraints
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.10.0'
dependencies: # Packages needed at runtime
flutter:
sdk: flutter
http: ^1.1.0 # From pub.dev
provider: ^6.0.0
shared_preferences: ^2.2.0
my_package: # From Git
git:
url: https://github.com/user/repo.git
ref: main
local_package: # Local package
path: ../local_package
dev_dependencies: # Packages needed only during development
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
build_runner: ^2.4.0
mockito: ^5.4.0
flutter: # Flutter-specific configuration
uses-material-design: true # Include Material icons
assets: # Asset files
- assets/images/
- assets/data/config.json
fonts: # Custom fonts
- family: Roboto
fonts:
- asset: fonts/Roboto-Regular.ttf
- asset: fonts/Roboto-Bold.ttf
weight: 700
- asset: fonts/Roboto-Italic.ttf
style: italic
Q3. What is the difference between dependencies and dev_dependencies?
Answer:
dependencies:
- Packages needed at runtime -- they are compiled into your app
- Examples:
http(API calls),provider(state management),shared_preferences(storage) - These are included in the final APK/IPA
dev_dependencies:
- Packages needed only during development and testing
- NOT included in the final app build
- Examples:
flutter_test(testing),build_runner(code generation),mockito(mocking),flutter_lints(linting)
dependencies:
http: ^1.1.0 # Included in release build
dev_dependencies:
mockito: ^5.4.0 # NOT included in release build
If you put a testing package in dependencies, it would unnecessarily increase your app size. If you put a runtime package in dev_dependencies, your app would crash in release mode.
Q4. What does the caret ^ symbol mean in version constraints?
Answer: The ^ (caret) syntax means "compatible with" -- it allows updates that don't change the leftmost non-zero digit.
^1.2.3 # Means >=1.2.3 and <2.0.0 (any 1.x.x where x >= 2.3)
^0.2.3 # Means >=0.2.3 and <0.3.0 (more restrictive for 0.x versions)
^0.0.3 # Means >=0.0.3 and <0.0.4
Other version constraints:
any # Any version (not recommended)
1.2.3 # Exactly this version
>=1.2.3 # This version or higher
>=1.2.3 <2.0.0 # Range (same as ^1.2.3)
The caret is the recommended approach. It follows semantic versioning: minor and patch updates should be backward compatible, major updates might break things.
Q5. What is pubspec.lock? Should it be committed to version control?
Answer: pubspec.lock is automatically generated by flutter pub get. It locks the exact versions of all dependencies (direct and transitive).
# pubspec.yaml says:
http: ^1.1.0 # Any 1.x.x
# pubspec.lock pins:
http:
version: "1.2.1" # Exact version resolved
Should you commit it?
-
For apps: YES. Commit
pubspec.lock. This ensures every developer and CI server uses the exact same dependency versions, preventing "works on my machine" issues. - For packages (libraries): NO. Don't commit it. Package consumers should resolve their own dependency versions.
To update locked dependencies:
flutter pub upgrade # Update all to latest compatible
flutter pub upgrade http # Update specific package
flutter pub outdated # Show which packages have newer versions
Q6. How do you add assets (images, fonts, JSON) to a Flutter project?
Answer:
Step 1: Place files in your project (commonly in an assets/ folder):
my_app/
├── assets/
│ ├── images/
│ │ ├── logo.png
│ │ ├── 2.0x/logo.png # 2x resolution
│ │ └── 3.0x/logo.png # 3x resolution
│ └── data/
│ └── config.json
Step 2: Declare in pubspec.yaml:
flutter:
assets:
- assets/images/ # All files in this directory
- assets/data/config.json # Specific file
Step 3: Use in code:
// Images
Image.asset('assets/images/logo.png')
// JSON
String json = await rootBundle.loadString('assets/data/config.json');
Map data = jsonDecode(json);
// Fonts
Text('Hello', style: TextStyle(fontFamily: 'Roboto'))
Flutter automatically selects the right resolution image (1x, 2x, 3x) based on the device's pixel density. The base image goes in the main folder, and higher-resolution variants go in 2.0x/ and 3.0x/ subdirectories.
Q7. What is flutter doctor and what does it check?
Answer: flutter doctor is a command-line tool that checks your development environment and reports any issues.
$ flutter doctor
Doctor summary:
[✓] Flutter (Channel stable, 3.x.x)
[✓] Android toolchain - develop for Android devices
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] VS Code
[✓] Connected device (2 available)
[✓] Network resources
It checks:
- Flutter SDK -- Installed correctly, correct channel, up to date
- Android toolchain -- Android SDK, build tools, platform tools, licenses
- Xcode (macOS only) -- Xcode installed, CocoaPods, command-line tools
- Chrome -- For web development
- IDE -- Android Studio and/or VS Code with Flutter plugin
- Connected devices -- Emulators or physical devices available
Use flutter doctor -v for verbose output with detailed information about each check.
Q8. What are Flutter channels (stable, beta, dev, master)?
Answer: Flutter channels represent different release stages:
- Stable -- Production-ready. Thoroughly tested. Updated roughly every quarter. Use this for production apps.
- Beta -- Preview of the next stable release. Updated monthly. May have some bugs. Good for trying upcoming features.
- Master -- The bleeding edge. Updated continuously with every commit. May be unstable. For contributors and early adopters.
flutter channel # Show current channel
flutter channel stable # Switch to stable
flutter channel beta # Switch to beta
flutter upgrade # Update to latest version of current channel
flutter downgrade # Go back to previous version
For most developers and all production apps, always use stable.
Q9. What is analysis_options.yaml?
Answer: analysis_options.yaml configures Dart's static analysis -- the lint rules that catch potential bugs and enforce code style.
include: package:flutter_lints/flutter.yaml # Include recommended rules
analyzer:
exclude:
- '**/*.g.dart' # Exclude generated files
- '**/*.freezed.dart'
errors:
missing_return: error # Treat as error
todo: ignore # Ignore TODOs
language:
strict-casts: true # Strict type casting
strict-raw-types: true # Disallow raw generic types
linter:
rules:
- prefer_const_constructors # Encourage const
- prefer_const_declarations
- avoid_print # Use logger instead
- prefer_single_quotes
- sort_constructors_first
- always_declare_return_types
- annotate_overrides
- avoid_unnecessary_containers
Popular lint packages:
-
flutter_lints-- Flutter team's recommended rules (included by default) -
very_good_analysis-- Stricter rules by Very Good Ventures -
lint-- Community-driven comprehensive rules
Q10. What are the important flutter CLI commands every developer should know?
Answer:
# Project creation
flutter create my_app # Create new project
flutter create --org com.example my_app # With custom organization
flutter create --template=package my_pkg # Create a package
# Running
flutter run # Run on connected device
flutter run -d chrome # Run on Chrome (web)
flutter run -d macos # Run on macOS desktop
flutter run --release # Run in release mode
flutter run --profile # Run in profile mode
# Building
flutter build apk # Build Android APK
flutter build appbundle # Build Android App Bundle (for Play Store)
flutter build ios # Build iOS
flutter build web # Build web
flutter build windows # Build Windows desktop
# Dependencies
flutter pub get # Install dependencies
flutter pub upgrade # Upgrade dependencies
flutter pub outdated # Check for outdated packages
flutter pub add http # Add a dependency
flutter pub remove http # Remove a dependency
# Testing
flutter test # Run all tests
flutter test test/widget_test.dart # Run specific test file
flutter test --coverage # Run tests with coverage
# Code quality
flutter analyze # Run static analysis
flutter format . # Format all Dart files (or dart format .)
# Cleaning
flutter clean # Delete build files (fixes many issues)
# Info
flutter doctor # Check environment
flutter doctor -v # Verbose check
flutter devices # List connected devices
flutter --version # Show Flutter version
BONUS: Quick-Fire Questions Often Asked in Interviews
What is the difference between mainAxisAlignment and crossAxisAlignment?
Answer: In a Row, mainAxis is horizontal and crossAxis is vertical. In a Column, mainAxis is vertical and crossAxis is horizontal. mainAxisAlignment distributes children along the primary axis (e.g., spaceEvenly, center). crossAxisAlignment aligns children perpendicular to the primary axis (e.g., start, stretch).
What is Expanded vs Flexible?
Answer: Both are used inside Row/Column/Flex. Expanded forces the child to fill all available space (FlexFit.tight). Flexible allows the child to be smaller than the available space (FlexFit.loose). Both accept a flex factor to control proportional sizing.
What is the difference between Container and SizedBox?
Answer: SizedBox is simpler -- it only sets width/height. Container is a convenience widget combining decoration, padding, margin, alignment, constraints, and transformation. Use SizedBox when you only need to set size or add fixed spacing. Use Container when you need decoration (background color, border, gradient, shadow).
What is ValueKey vs ObjectKey vs UniqueKey?
Answer: ValueKey compares by value (ValueKey(1) == ValueKey(1)). ObjectKey compares by object identity (ObjectKey(obj1) != ObjectKey(obj2) even if equal). UniqueKey is always unique -- every instance is different. Use ValueKey with IDs, ObjectKey when you have object references, UniqueKey when you need guaranteed uniqueness.
What is WidgetsBinding.instance.addPostFrameCallback?
Answer: It schedules a callback to run after the current frame is rendered. Useful when you need to do something after build() and layout are complete, like reading the size of a widget or scrolling to a position. It runs exactly once, after the next frame.
What is the @override annotation?
Answer: It's a metadata annotation indicating that a method is intentionally overriding a method from a superclass or interface. It's not required but strongly recommended because the analyzer will warn you if the method doesn't actually override anything (e.g., typo in the method name).
Can you nest Scaffolds?
Answer: Yes, but be careful. Each Scaffold creates its own visual scope (AppBar, FAB, SnackBar). Nested Scaffolds can cause unexpected behavior with SnackBars and Drawers. Typically you have one Scaffold per screen/route, and use regular widgets for nested layouts.
Top comments (0)