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
switchstatements 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
| Feature | Abstract Class | Sealed Class |
|---|---|---|
| Instantiable | No | No |
| Subclass location | Anywhere | Same file only |
| Compiler awareness | Partial | Full |
| Exhaustive switch | No (needs default) | Yes (enforced) |
| Best for | Open extension APIs | Closed 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
mixinkeyword - Cannot be instantiated directly
- Cannot have constructors (with parameters)
- Can use
onkeyword 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 valuesEnumClass.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/awaitinstead - 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:
- The variable will definitely be assigned before it is used, but you cannot assign at declaration time.
- 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
| Feature | Future | Stream |
|---|---|---|
| Delivery | One value, once | Multiple values over time |
| Completion | Completes after first value | Completes when closed |
| Use case | HTTP response, DB query | WebSocket, real-time data, file reading |
| Listen | await or .then() | await for or .listen() |
| Types | Future<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.
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.
