DEV Community

Cover image for Flutter & Dart Basic Interview Questions
Anurag Dubey
Anurag Dubey

Posted on

Flutter & Dart Basic Interview Questions

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:

  1. 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
  2. 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
  3. 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:

  1. Build Phase -- The build() method is called, creating/updating the Widget tree
  2. Layout Phase -- The framework walks the Render tree, each RenderObject determines its size and position. Constraints go down, sizes go up.
  3. Paint Phase -- Each RenderObject paints itself onto a canvas (layers)
  4. Compositing Phase -- The layer tree is sent to the engine
  5. 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:

  1. MethodChannel -- For calling methods. Most commonly used. You invoke a method by name and get a result back. Example: calling native camera API.
  2. EventChannel -- For streaming data from native to Flutter. Example: listening to sensor data continuously.
  3. 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:

  1. Debug Mode -- Uses JIT compilation. Hot reload enabled. Assertions enabled. DevTools enabled. Debug banner shown. Slower performance. Used during development.

  2. 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.

  3. 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:

  1. Both AOT and JIT compilation -- JIT enables hot reload during development; AOT gives native performance in production
  2. No bridge needed -- Dart compiles to native code directly, unlike JavaScript which needs a bridge
  3. Single-threaded event loop with Isolates -- Avoids race conditions common with shared-memory multithreading
  4. 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)
  5. Strong typing with type inference -- Catches bugs at compile time while remaining concise
  6. 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 as String. 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 because DateTime.now() can only be evaluated at runtime. const objects 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 a LateInitializationError at runtime. Also used for lazy initialization: late final value = expensiveComputation(); -- the computation runs only when value is 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
}
Enter fullscreen mode Exit fullscreen mode

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 $variable or ${expression}.
  • bool -- true or false
  • 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 of null
  • 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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Type promotion also works with type checks:

void process(Object obj) {
  if (obj is String) {
    print(obj.length); // obj is promoted to String
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Multi-line Strings: Use triple quotes:

String text = '''
This is a
multi-line string
''';
Enter fullscreen mode Exit fullscreen mode

Raw Strings: Prefix with r to avoid escape sequences:

String path = r'C:\Users\folder'; // backslashes treated literally
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In Flutter, you see this extensively in widget constructors:

class MyWidget extends StatelessWidget {
  final String title;
  const MyWidget({super.key, required this.title});
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Optional Positional Parameters -- Wrapped in []:

void greet(String name, [int age = 0]) {}
greet('John'); // age defaults to 0
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

The null-aware spread ...? handles null collections:

List<int>? maybeList;
var combined = [1, ...?maybeList, 2]; // [1, 2]
Enter fullscreen mode Exit fullscreen mode


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);
}
Enter fullscreen mode Exit fullscreen mode

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 factory keyword. Doesn't always create a new instance. Can return cached objects or subtypes. Doesn't have access to this.
  • Const constructor -- const Person(this.name); where all fields are final. 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!');
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 extend only ONE class but implement MULTIPLE 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
}
Enter fullscreen mode Exit fullscreen mode

Key rules:

  • Declare with mixin keyword (or mixin class if 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 on keyword 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 with keyword
  • 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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Generics are important because:

  1. Type safety -- Errors caught at compile time, not runtime
  2. Code reuse -- Write once, use with any type
  3. Performance -- Dart's generics are reified, meaning type information is preserved at runtime (unlike Java's type erasure). List<int> and List<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
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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²
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Uncompleted -- The async operation is still running
  2. Completed with a value -- Success
  3. 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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

async/await -- Synchronous-looking style:

try {
  var user = await fetchUser();
  var orders = await fetchOrders(user.id);
  processOrders(orders);
} catch (e) {
  handleError(e);
}
Enter fullscreen mode Exit fullscreen mode

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/catch is 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
);
Enter fullscreen mode Exit fullscreen mode

There are two types of Streams:

  1. Single-subscription Stream -- Can only be listened to once. Default. Example: reading a file.
  2. 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'));
Enter fullscreen mode Exit fullscreen mode

In Flutter, Streams are used with StreamBuilder, StreamController, BLoC pattern, Firebase Realtime Database, WebSockets, etc.


Q4. What are async* and yield in Dart?

Answer:

  • async makes a function return a Future
  • async* makes a function return a Stream (asynchronous generator)
  • sync* makes a function return an Iterable (synchronous generator)
  • yield emits 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
}
Enter fullscreen mode Exit fullscreen mode

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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Microtask Queue -- Higher priority. Tasks scheduled with scheduleMicrotask() or Future.microtask(). Processed FIRST before moving to the event queue.

  2. Event Queue -- Lower priority. I/O events, timers, UI events, Future callbacks (.then()). Processed after all microtasks are done.

The execution order:

  1. Synchronous code runs first (to completion)
  2. All microtasks are processed
  3. One event from the event queue is processed
  4. All microtasks again (if any new ones were added)
  5. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Future.any -- Returns the result of the FIRST future to complete. Others are ignored.

var fastest = await Future.any([
  fetchFromServer1(),
  fetchFromServer2(),
]);
Enter fullscreen mode Exit fullscreen mode

Future.forEach -- Executes an async operation sequentially for each element:

await Future.forEach(urls, (url) async {
  await downloadFile(url);
});
Enter fullscreen mode Exit fullscreen mode

Future.delayed -- Creates a future that completes after a delay:

var result = await Future.delayed(Duration(seconds: 2), () => 'Done');
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

2. .catchError() with Futures:

fetchData()
  .then((data) => process(data))
  .catchError((e) => handleError(e))
  .whenComplete(() => cleanup());
Enter fullscreen mode Exit fullscreen mode

3. Stream error handling:

stream.listen(
  (data) => process(data),
  onError: (e) => handleError(e),
  cancelOnError: false, // continue listening after error
);
Enter fullscreen mode Exit fullscreen mode

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!
  }
}
Enter fullscreen mode Exit fullscreen mode

Use a broadcast StreamController when multiple listeners need the same stream:

final _controller = StreamController<int>.broadcast();
Enter fullscreen mode Exit fullscreen mode

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). Like finally. 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');
  });
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode


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
Enter fullscreen mode Exit fullscreen mode

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
];
Enter fullscreen mode Exit fullscreen mode

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',
];
Enter fullscreen mode Exit fullscreen mode

Collection-for:

var numbers = [1, 2, 3];
var doubled = [
  for (var n in numbers) n * 2, // [2, 4, 6]
];
Enter fullscreen mode Exit fullscreen mode

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)),
  ],
)
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

In Flutter, using const collections and const constructors is a best practice because:

  1. They are created at compile time (zero runtime cost)
  2. They are canonicalized (same const = same object in memory)
  3. Flutter skips rebuilding widgets with const constructors

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));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Records work well with collections:

var people = [
  (name: 'John', age: 25),
  (name: 'Jane', age: 30),
];

people.sort((a, b) => a.age.compareTo(b.age));
Enter fullscreen mode Exit fullscreen mode

Records are value types -- two records with the same values are equal:

print((1, 2) == (1, 2)); // true
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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'),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. createState() -- Called once when the widget is inserted into the tree. Creates the State object.

  2. initState() -- Called once after the State object is created. Use it for one-time initialization: subscribing to streams, initializing controllers, fetching initial data. Always call super.initState() first.

  3. didChangeDependencies() -- Called immediately after initState() and whenever an InheritedWidget that this widget depends on changes. Use it for things that depend on BuildContext (like MediaQuery.of(context), Theme.of(context)).

  4. build() -- Called every time the widget needs to render. Must return a Widget. Called after initState, didChangeDependencies, setState, and didUpdateWidget. Should be pure (no side effects) and fast.

  5. 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).

  6. setState() -- Not a lifecycle method, but calling it marks the State as dirty and schedules a rebuild (calls build() again).

  7. deactivate() -- Called when the State is removed from the tree temporarily (e.g., moved to a different location using GlobalKey).

  8. dispose() -- Called when the State is permanently removed from the tree. Use it for cleanup: cancel subscriptions, dispose controllers, close streams. Always call super.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();
}
Enter fullscreen mode Exit fullscreen mode

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++;
});
Enter fullscreen mode Exit fullscreen mode

What happens when you call setState():

  1. The callback runs synchronously (mutates state)
  2. The State object is marked as "dirty"
  3. The framework schedules a rebuild for the next frame
  4. On the next frame, build() is called again
  5. 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 with if (mounted) setState(() {});
  • The callback must be synchronous. Don't make it async:
  // WRONG
  setState(() async {
    _data = await fetchData();
  });

  // RIGHT
  _data = await fetchData();
  setState(() {});
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

Const constructor:

const Text('Hello') // Same instance reused across rebuilds
Enter fullscreen mode Exit fullscreen mode

When a widget uses a const constructor:

  1. It's created at compile time (zero runtime cost)
  2. Flutter skips rebuilding it entirely -- even if the parent rebuilds
  3. 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
  ],
)
Enter fullscreen mode Exit fullscreen mode

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'),
    ),
  ]),
)
Enter fullscreen mode Exit fullscreen mode

Common use cases:

  1. Form validation -- Accessing FormState to validate/save
  2. Navigating without context -- navigatorKey.currentState!.pushNamed('/home')
  3. Moving a widget between different parts of the tree while preserving its state
  4. 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())
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

How it works:

  • Data flows DOWN the tree (parent to descendants)
  • When updateShouldNotify returns 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:

  1. Find ancestor widgets: Theme.of(context), Navigator.of(context), MediaQuery.of(context)
  2. Find inherited data: context.dependOnInheritedWidgetOfExactType<T>()
  3. Get the size after layout: context.size
  4. Show overlays: showDialog(context: context, ...)

Important rules:

  • Context refers to THIS widget's position, not its children. So Theme.of(context) inside a Theme widget won't find that Theme -- it finds the one ABOVE.
  • Don't use context in initState() for things that depend on InheritedWidgets. Use didChangeDependencies() instead or WidgetsBinding.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'),
  );
}
Enter fullscreen mode Exit fullscreen mode

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 BuildContext actually 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 RenderObjectWidget subclasses (like Padding, Container, SizedBox). Layout-free widgets like StatelessWidget and StatefulWidget don'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:

  1. Same runtimeType AND same key? -> Update the existing Element. Call update() on the Element, which updates the RenderObject. This is cheap.

  2. 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')),
])
Enter fullscreen mode Exit fullscreen mode

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 a StatelessElement, StatefulWidget creates a StatefulElement.

  • createRenderObject() -- Defined on RenderObjectWidget subclasses. Creates the RenderObject that handles layout and painting. Only widgets that directly affect rendering implement this (like Padding creates a RenderPadding, SizedBox creates a RenderConstrainedBox).

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)
Enter fullscreen mode Exit fullscreen mode

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.

  1. Constraints go down: A parent tells its child the minimum and maximum width/height it can be. This is a BoxConstraints object with minWidth, maxWidth, minHeight, maxHeight.

  2. Sizes go up: The child chooses its own size within those constraints and reports back.

  3. 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)
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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:

  1. setState(fn) is called -- The callback fn runs synchronously, mutating state

  2. Element marked dirty -- _element!.markNeedsBuild() is called, adding this Element to a "dirty elements" list

  3. Frame scheduled -- If not already scheduled, SchedulerBinding.scheduleFrame() requests a new frame from the platform

  4. On next vsync (frame):

    • Build phase: The framework calls buildScope(), which rebuilds all dirty elements by calling their build() 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.
  5. 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
)
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

What MaterialApp provides:

  1. Navigator -- For routing and navigation (push/pop screens)
  2. Theme -- Material Design theming (colors, fonts, shapes)
  3. Localization -- Internationalization support
  4. MediaQuery -- Screen size, orientation, text scale factor
  5. Title -- App title shown in task switchers
  6. 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, BottomNavigationBar support
  • ScaffoldMessenger for 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,
)
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

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([...]))
  ],
)
Enter fullscreen mode Exit fullscreen mode

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),
  ),
);
Enter fullscreen mode Exit fullscreen mode

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')),
    ],
  ),
);
Enter fullscreen mode Exit fullscreen mode

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: [...]),
  ),
);
Enter fullscreen mode Exit fullscreen mode

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'),
      ],
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

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: ...,
)
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

Access theme anywhere:

var colorScheme = Theme.of(context).colorScheme;
var textTheme = Theme.of(context).textTheme;

Text('Hello', style: textTheme.headlineLarge);
Container(color: colorScheme.primary);
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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)),
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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()));
Enter fullscreen mode Exit fullscreen mode

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']!)),
    ],
  ),
)
Enter fullscreen mode Exit fullscreen mode

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:

  1. You make a code change
  2. Press r in the terminal (or save, if auto-reload is enabled)
  3. The Dart VM loads the updated source files
  4. The Flutter framework rebuilds the widget tree
  5. 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:

  1. Press R (capital R) in the terminal
  2. The Dart VM destroys all existing state
  3. The app's main() function runs again
  4. 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.xml or Info.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:

  1. Only the changed Dart source files are recompiled (incremental compilation)
  2. The updated code is injected into the already running Dart VM
  3. The VM replaces the old method implementations with new ones
  4. Flutter then calls reassemble() on the root widget, which triggers the widget tree to rebuild using the new code
  5. 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:

  1. File change detected -- The IDE or CLI detects saved file changes
  2. Incremental compilation -- Only changed files are recompiled to Dart kernel format
  3. Code injection -- The new kernel is sent to the Dart VM over a service protocol
  4. VM updates -- The Dart VM replaces changed classes and functions in memory
  5. reassemble() called -- Flutter's binding calls reassemble() on the root Element
  6. Widget tree rebuilt -- Each Element calls its widget's build() method with the new code
  7. Reconciliation -- The framework diffs old and new widget trees, updating RenderObjects as needed
  8. 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:

  1. Changed initState() but state seems stale -- initState runs only once. Hot Reload doesn't re-run it. Solution: Hot Restart.

  2. Changed a global variable but old value persists -- Global/static variables are initialized once. Hot Reload doesn't reinitialize them. Solution: Hot Restart.

  3. Changed enum values but app shows old ones -- Enums are compile-time constants. Solution: Hot Restart.

  4. Added a new field to a class but it's null -- Existing instances don't get the new field initialized. Solution: Hot Restart.

  5. Compile error breaks hot reload -- Fix the error and hot reload again. The app continues with old code until a successful reload.

  6. 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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 2: Declare in pubspec.yaml:

flutter:
  assets:
    - assets/images/          # All files in this directory
    - assets/data/config.json # Specific file
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It checks:

  1. Flutter SDK -- Installed correctly, correct channel, up to date
  2. Android toolchain -- Android SDK, build tools, platform tools, licenses
  3. Xcode (macOS only) -- Xcode installed, CocoaPods, command-line tools
  4. Chrome -- For web development
  5. IDE -- Android Studio and/or VS Code with Flutter plugin
  6. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode


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)