All Posts
August 27, 202618 min read

Dart Interview Questions Every Flutter Developer Must Know

DartFlutterInterviewSealed ClassesMixins
Dart programming language interview questions and answers for Flutter developers

Introduction

Whether you are preparing for a senior Flutter role or conducting technical interviews yourself, having a deep understanding of Dart's core language features is non-negotiable. Frameworks and libraries change, but the language fundamentals remain the same across every project you will ever work on.

This guide covers the most frequently asked Dart interview questions with thorough explanations and real code examples. Not surface-level answers, but the kind that demonstrate genuine understanding.


1. Sealed Class vs Abstract Class: What is the Difference?

This is one of the most commonly asked questions in senior Flutter interviews, especially since Dart 3.0 introduced sealed classes.

Abstract Class

An abstract class is a class that cannot be instantiated directly. It defines a contract (methods and properties) that subclasses must implement. Any file anywhere in your codebase can extend or implement an abstract class.

abstract class Shape {
  double area(); // Contract - must be implemented by subclasses
  double perimeter(); // Contract - must be implemented by subclasses

  void describe() {
    // Can also have concrete methods
    print('This shape has area: ${area()}');
  }
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double area() => 3.14159 * radius * radius;

  @override
  double perimeter() => 2 * 3.14159 * radius;
}

class Rectangle extends Shape {
  final double width, height;
  Rectangle(this.width, this.height);

  @override
  double area() => width * height;

  @override
  double perimeter() => 2 * (width + height);
}

Key characteristics of abstract classes:

  • Cannot be instantiated directly
  • Subclasses can be defined in any file, any package
  • The compiler does NOT know all possible subclasses at compile time
  • switch statements on abstract types require a default case

Sealed Class

A sealed class (introduced in Dart 3.0) is an abstract class with one additional critical constraint: all direct subclasses must be defined in the same library file. The compiler knows every possible subtype at compile time, enabling exhaustive pattern matching in switch expressions.

// All subtypes MUST be in the same file as the sealed class
sealed class ApiResult {
  const ApiResult();
}

class ApiSuccess extends ApiResult {
  final Map<String, dynamic> data;
  const ApiSuccess(this.data);
}

class ApiError extends ApiResult {
  final String message;
  final int statusCode;
  const ApiError(this.message, this.statusCode);
}

class ApiLoading extends ApiResult {
  const ApiLoading();
}

// Exhaustive switch - compiler verifies ALL cases are handled
// No default case needed, and no case can be missed
String handleResult(ApiResult result) => switch (result) {
  ApiSuccess(:final data) => 'Success: ${data['message']}',
  ApiError(:final message, :final statusCode) => 'Error $statusCode: $message',
  ApiLoading() => 'Loading...',
};

If you add a new subtype (e.g., ApiTimeout) to the sealed class without updating the switch, the compiler throws a warning at compile time - not at runtime. This makes sealed classes incredibly powerful for modeling state in Flutter apps.

Comparison Table

FeatureAbstract ClassSealed Class
InstantiableNoNo
Subclass locationAnywhereSame file only
Compiler awarenessPartialFull
Exhaustive switchNo (needs default)Yes (enforced)
Best forOpen extension APIsClosed state modeling

2. Mixins: What Are They and When Do You Use Them?

Dart does not support multiple inheritance - a class can only extend one parent class. Mixins solve the problem of wanting to share behavior across multiple unrelated class hierarchies without deep inheritance chains.

A mixin is a class-like structure whose methods can be "mixed into" any class. It is not a parent-child relationship. Think of it as "borrowing" capabilities.

mixin Loggable {
  void log(String message) {
    print('[${runtimeType}] $message');
  }

  void logError(String message) {
    print('[${runtimeType}] ERROR: $message');
  }
}

mixin Cacheable {
  final Map<String, dynamic> _cache = {};

  void saveToCache(String key, dynamic value) {
    _cache[key] = value;
  }

  dynamic readFromCache(String key) => _cache[key];

  void clearCache() => _cache.clear();
}

mixin Retryable {
  Future<T> withRetry<T>(Future<T> Function() operation, {int maxAttempts = 3}) async {
    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await operation();
      } catch (e) {
        if (attempt == maxAttempts) rethrow;
        await Future.delayed(Duration(seconds: attempt));
      }
    }
    throw Exception('Should not reach here');
  }
}

// A class can use multiple mixins
class UserRepository with Loggable, Cacheable, Retryable {
  Future<User> getUser(String id) async {
    final cached = readFromCache(id);
    if (cached != null) {
      log('Returning cached user for $id');
      return cached as User;
    }

    log('Fetching user from API...');
    final user = await withRetry(() => _fetchFromApi(id));
    saveToCache(id, user);
    return user;
  }

  Future<User> _fetchFromApi(String id) async {
    // API call implementation
    throw UnimplementedError();
  }
}

Key rules for mixins:

  • Declared with the mixin keyword
  • Cannot be instantiated directly
  • Cannot have constructors (with parameters)
  • Can use on keyword to restrict which classes they can be applied to
// This mixin can only be applied to classes that extend or implement Widget
mixin ResponsiveMixin on Widget {
  bool get isTablet => false; // access Widget properties via 'on'
}

3. Enums in Dart: Basic and Enhanced

Dart has two forms of enums. Most developers only use the basic form.

Basic Enum

enum OrderStatus { pending, processing, shipped, delivered, cancelled }

void printStatus(OrderStatus status) {
  switch (status) {
    case OrderStatus.pending:
      print('Order is pending');
    case OrderStatus.shipped:
      print('Order has been shipped');
    default:
      print('Other status: ${status.name}');
  }
}

Enhanced Enum (Dart 2.17+)

Enhanced enums can have fields, constructors, and methods - making them much more powerful:

enum PaymentMethod {
  creditCard(displayName: 'Credit Card', icon: 'credit_card', fee: 0.029),
  paypal(displayName: 'PayPal', icon: 'paypal', fee: 0.034),
  bankTransfer(displayName: 'Bank Transfer', icon: 'bank', fee: 0.0);

  const PaymentMethod({
    required this.displayName,
    required this.icon,
    required this.fee,
  });

  final String displayName;
  final String icon;
  final double fee;

  double calculateFee(double amount) => amount * fee;

  bool get isFree => fee == 0.0;
}

// Usage
final method = PaymentMethod.creditCard;
print(method.displayName); // Credit Card
print(method.calculateFee(100)); // 2.9
print(method.isFree); // false

Useful built-in enum properties:

  • .name - returns the string name of the enum value
  • .index - returns the position (0, 1, 2...)
  • EnumClass.values - returns a list of all values
  • EnumClass.values.byName('creditCard') - find by name string

4. Isolates: Dart's Concurrency Model

Dart is single-threaded. Isolates are Dart's mechanism for true parallel execution across multiple CPU cores, without shared memory.

Unlike threads in Java/Kotlin, Isolates are completely isolated - each has its own memory heap and its own Event Loop. They communicate by passing messages (copies of data), which eliminates race conditions.

import 'dart:isolate';

// Heavy computation that would freeze the UI if run on the main isolate
List<int> computePrimes(int limit) {
  final primes = <int>[];
  for (int i = 2; i <= limit; i++) {
    bool isPrime = true;
    for (int j = 2; j * j <= i; j++) {
      if (i % j == 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) primes.add(i);
  }
  return primes;
}

// Run the heavy computation in a background Isolate
Future<List<int>> getPrimesInBackground(int limit) async {
  // Dart 2.19+ simple API
  return await Isolate.run(() => computePrimes(limit));
}

// Flutter-specific simpler API
import 'package:flutter/foundation.dart';

Future<List<int>> getPrimesWithCompute(int limit) async {
  return await compute(computePrimes, limit);
}

When to use Isolates:

  • Parsing large JSON responses (>5MB)
  • Image compression or processing
  • Cryptographic operations
  • Sorting or filtering large in-memory datasets
  • Any computation that takes more than a few milliseconds

When NOT to use Isolates:

  • Short async operations (HTTP calls, DB queries) - use async/await instead
  • Anything that requires Flutter widget access (Isolates cannot access Flutter)

5. Method Overriding vs Method Overloading

This trips up many candidates because Java supports both, but Dart is different.

Method Overriding

Overriding means replacing a parent class method in a subclass with a new implementation. Dart supports this fully.

class Animal {
  void speak() => print('...');
  String get name => 'Animal';
}

class Dog extends Animal {
  @override // Good practice - compiler will warn if no parent method exists
  void speak() => print('Woof!');

  @override
  String get name => 'Dog';
}

class Cat extends Animal {
  @override
  void speak() => print('Meow!');
}

// Polymorphism in action
void main() {
  final animals = <Animal>[Dog(), Cat()];
  for (final animal in animals) {
    animal.speak(); // Calls the correct overridden method at runtime
  }
}

Method Overloading

Overloading means defining multiple methods with the same name but different parameter signatures. Dart does NOT support method overloading.

// THIS IS NOT VALID IN DART:
class Calculator {
  int add(int a, int b) => a + b;
  double add(double a, double b) => a + b; // COMPILE ERROR - duplicate definition
}

Dart's solution is to use optional parameters and named parameters instead:

class Calculator {
  num add(num a, num b, {num? c}) {
    if (c != null) return a + b + c;
    return a + b;
  }
}

// Or use factory constructors for overloading-like patterns
class Connection {
  final String host;
  final int port;

  Connection(this.host, this.port);

  factory Connection.fromUrl(String url) {
    final uri = Uri.parse(url);
    return Connection(uri.host, uri.port);
  }

  factory Connection.local({int port = 8080}) {
    return Connection('localhost', port);
  }
}

6. const vs final vs static

class Config {
  // static: belongs to the class, not an instance
  static String appName = 'MyApp';

  // static const: compile-time constant belonging to the class
  static const String version = '1.0.0';

  // final: runtime constant - set once, never changed
  final String userId;

  Config(this.userId);
}

void main() {
  // const: compile-time constant - value must be known at compile time
  const pi = 3.14159;
  const greeting = 'Hello'; // Only primitive types and const constructors

  // final: runtime constant - value is determined at runtime
  final timestamp = DateTime.now(); // Runtime value - not const-able
  final name = getUserName(); // Evaluated once when reached

  // const widget optimization in Flutter
  // Flutter re-renders widgets when their parent rebuilds.
  // const widgets are instantiated only ONCE and reused.
  // Use const wherever possible for performance.
}

7. late Keyword: When and Why

The late keyword defers null-safety initialization to after declaration. Use it when:

  1. The variable will definitely be assigned before it is used, but you cannot assign at declaration time.
  2. For expensive initializations you want to defer (lazy initialization).
class UserProfile {
  // Without late: requires immediate initialization or nullable type
  // String _displayName; // Error: must be initialized
  // String? _displayName; // Works but loses null-safety guarantee

  // With late: promise to Dart that this WILL be set before use
  late String _displayName;
  late final DatabaseService _db; // late final = assigned once, lazily

  // Lazy initialization - only computed when first accessed
  late final String initials = _computeInitials();

  void initialize(String name) {
    _displayName = name; // Set before use
  }

  String _computeInitials() {
    return _displayName.split(' ').map((w) => w[0]).join();
  }
}

Warning: If a late variable is accessed before being assigned, Dart throws LateInitializationError at runtime. Use late only when you are certain initialization will happen before access.


8. Streams vs Futures

FeatureFutureStream
DeliveryOne value, onceMultiple values over time
CompletionCompletes after first valueCompletes when closed
Use caseHTTP response, DB queryWebSocket, real-time data, file reading
Listenawait or .then()await for or .listen()
TypesFuture<T>Stream<T>
// Future: one result
Future<User> getUser(String id) async {
  return await apiClient.fetchUser(id);
}

// Stream: multiple results over time
Stream<Message> listenToChat(String roomId) async* {
  await for (final event in websocket.events) {
    if (event.roomId == roomId) {
      yield Message.fromEvent(event);
    }
  }
}

// StreamController: manually control a stream
final controller = StreamController<int>.broadcast();
controller.sink.add(1);
controller.sink.add(2);
controller.stream.listen((value) => print(value));
controller.close();

9. Extension Methods

Extension methods let you add new methods to existing classes without modifying them or subclassing them.

extension StringHelpers on String {
  bool get isValidEmail => RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
  String get capitalize => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
  String truncate(int maxLength) => length <= maxLength ? this : '${substring(0, maxLength)}...';
}

extension DateHelpers on DateTime {
  bool get isToday {
    final now = DateTime.now();
    return day == now.day && month == now.month && year == now.year;
  }

  String get timeAgo {
    final diff = DateTime.now().difference(this);
    if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
    if (diff.inHours < 24) return '${diff.inHours}h ago';
    return '${diff.inDays}d ago';
  }
}

// Usage - reads like built-in methods
void main() {
  print('test@email.com'.isValidEmail); // true
  print('hello world'.capitalize); // Hello world
  print('Long text here'.truncate(8)); // Long tex...
  print(DateTime.now().isToday); // true
}

10. Null Safety: The Key Operators

// ? - Nullable type declaration
String? name; // Can be null
int? age = null; // Explicitly null

// ! - Null assertion: "I guarantee this is not null"
String nonNull = name!; // Throws if name is null at runtime

// ?? - Null coalescing: use fallback if null
String displayName = name ?? 'Anonymous';

// ??= - Assign only if null
name ??= 'Default Name';

// ?. - Null-aware member access
int? length = name?.length; // Returns null if name is null, not an error

// ?[] - Null-aware index operator
final list = <String>?;
final first = list?[0]; // null if list is null

// ...? - Null-aware spread in collections
List<int>? extra;
final combined = [1, 2, ...?extra, 3]; // [1, 2, 3] - safely ignores null

Conclusion

Mastering these Dart concepts puts you in the top tier of Flutter candidates. The key insight is that Dart is a carefully designed language where every feature has a specific purpose:

  • Sealed classes for exhaustive state modeling
  • Mixins for composable behavior without inheritance chains
  • Enhanced enums for rich type-safe constants
  • Isolates for true parallel execution without shared memory
  • Overriding (not overloading) through optional and named parameters
  • Null safety operators for compile-time correctness guarantees

Understanding not just what these features do, but when and why to use them, is what separates engineers who write correct code from engineers who write maintainable code.


Written by Moeen Ahmad, Senior Software Engineer working across mobile apps, backend systems, cloud deployments, and AI-powered products. I write about practical engineering, real project lessons, and building software that actually ships.

Share

Written by Moeen Ahmad, Senior Software Engineer working across mobile apps, backend systems, cloud deployments, and AI-powered products. I write about practical engineering, real project lessons, and building software that actually ships.

Interested in working together?

Let's discuss your project and explore how I can help bring it to life.