Flutter is one of the fastest-growing cross-platform frameworks, and Flutter developer interviews cover everything from Dart fundamentals to widget trees, state management, and performance tuning. This guide covers 50 common Flutter interview questions with detailed answers and code examples.
Table of Contents
- Dart Fundamentals
- Widgets & UI
- State Management
- Navigation & Routing
- Async & Streams
- Architecture Patterns
- Animations
- Platform Integration
- Testing
- Performance & Advanced
Dart Fundamentals
1. What is Dart and why does Flutter use it?
Dart is a statically typed, object-oriented language developed by Google. Flutter uses Dart because:
- AOT compilation — compiles to native ARM code for fast startup and runtime
- JIT compilation — enables hot reload during development
- Single language — one language for UI, business logic, and platform channels
- Sound null safety — catches null errors at compile time
// Null safety in Dart
String name = "Flutter"; // non-nullable, must have value
String? nickname; // nullable, can be null
// Safe access
int length = nickname?.length ?? 0; // Elvis operator
String upper = nickname!.toUpperCase(); // force unwrap (throws if null)
2. What is the difference between var, final, and const?
| Keyword | Type inferred | Assignable | Evaluated |
|---|---|---|---|
var |
Yes | Multiple times | Runtime |
final |
Yes | Once | Runtime |
const |
Yes | Once | Compile-time |
var name = "Alice"; // type inferred as String, reassignable
name = "Bob"; // OK
final now = DateTime.now(); // set once, at runtime
// now = DateTime.now(); // ERROR: final variable
const pi = 3.14159; // compile-time constant
const list = [1, 2, 3]; // deeply constant
// list.add(4); // ERROR: Unsupported operation
// const constructors
const Text("Hello"); // same instance reused — no rebuild
Rule of thumb: prefer const → final → var.
3. Explain Dart mixins.
Mixins allow adding behaviour to a class without inheritance. They enable code reuse across unrelated class hierarchies.
mixin Flyable {
String fly() => "I can fly!";
}
mixin Swimmable {
String swim() => "I can swim!";
}
class Duck extends Animal with Flyable, Swimmable {
// Duck now has fly() and swim() methods
}
// Restrict mixin usage with `on`
mixin Logger on Widget {
void log(String msg) => print("[${runtimeType}] $msg");
}
Key points:
- Mixins cannot be instantiated directly
- Use
onto restrict which types can use the mixin - Dart uses C3 linearization for mixin conflict resolution
4. What is the difference between == and identical()?
// == checks value equality (calls operator==)
// identical() checks reference equality (same object in memory)
const a = "hello";
const b = "hello";
print(a == b); // true (value equal)
print(identical(a, b)); // true (const canonicalization — same object)
var x = "hello";
var y = "hello";
print(x == y); // true (value equal)
print(identical(x, y)); // false (different objects at runtime)
// With custom classes
class Point {
final int x, y;
Point(this.x, this.y);
@override
bool operator ==(Object other) =>
other is Point && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(x, y);
}
5. What are Dart extensions?
Extensions add methods to existing types without subclassing or modifying the original class.
extension StringUtils on String {
String get capitalize =>
isEmpty ? this : "${this[0].toUpperCase()}${substring(1)}";
bool get isEmail => contains("@") && contains(".");
String repeat(int times) => List.filled(times, this).join();
}
// Usage
"hello".capitalize; // "Hello"
"user@example.com".isEmail; // true
"ab".repeat(3); // "ababab"
// Extensions on generic types
extension ListUtils<T> on List<T> {
T? firstOrNull() => isEmpty ? null : first;
}
Widgets & UI
6. What is the difference between StatelessWidget and StatefulWidget?
| StatelessWidget | StatefulWidget | |
|---|---|---|
| State | Immutable | Mutable via State<T> |
| Rebuild trigger | Parent rebuild | setState() or parent |
| Performance | Slightly lighter | Slightly heavier |
| Use case | Display-only UI | Interactive / animated UI |
// StatelessWidget — no internal state
class Greeting extends StatelessWidget {
final String name;
const Greeting({required this.name, super.key});
@override
Widget build(BuildContext context) {
return Text("Hello, $name!");
}
}
// StatefulWidget — has mutable state
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 Column(children: [
Text("$_count"),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text("Increment"),
),
]);
}
}
7. Explain the Flutter widget tree, element tree, and render tree.
Flutter has three parallel trees:
| Tree | Purpose | Examples |
|---|---|---|
| Widget tree | Configuration / immutable blueprints | Text, Column, Container |
| Element tree | Mutable lifecycle instances | ComponentElement, RenderObjectElement |
| Render tree | Layout and painting | RenderBox, RenderFlex |
Flow: Widget describes → Element manages lifecycle → RenderObject paints.
When setState() is called:
- Widget subtree is rebuilt (cheap — just config objects)
- Flutter diffs old vs new widget tree
- Only changed RenderObjects are repainted
8. What is the difference between Column/Row and Flex?
Column and Row are convenience widgets built on Flex:
// These are equivalent:
Column(children: [...])
Flex(direction: Axis.vertical, children: [...])
Row(children: [...])
Flex(direction: Axis.horizontal, children: [...])
Key Flex layout properties:
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // along main axis
crossAxisAlignment: CrossAxisAlignment.start, // perpendicular axis
mainAxisSize: MainAxisSize.min, // shrink-wrap
children: [
Expanded(flex: 2, child: WidgetA()), // takes 2/3 of space
Expanded(flex: 1, child: WidgetB()), // takes 1/3 of space
Flexible(child: WidgetC()), // can shrink below ideal size
],
)
9. What is const constructor in Flutter and why does it matter?
const constructors create compile-time constant widget instances that Flutter can skip rebuilding:
// Without const — new instance created on every build
Text("Hello") // rebuilt unnecessarily
// With const — same instance reused
const Text("Hello") // Flutter skips rebuild, O(1) check
// Deep const subtree
const Column(children: [
Text("Static A"),
Icon(Icons.star),
])
// The entire subtree is skipped during rebuild
Performance impact: Use const wherever the widget doesn't depend on runtime data. Flutter's lint rule prefer_const_constructors enforces this.
10. What is the Key in Flutter and when should you use it?
Keys help Flutter identify widgets across rebuilds, especially in lists:
// Without keys — Flutter matches by position, can swap state
ListView(children: [
MyWidget(color: Colors.red),
MyWidget(color: Colors.blue),
])
// With keys — Flutter matches by key, preserves state correctly
ListView(children: [
MyWidget(key: ValueKey("red"), color: Colors.red),
MyWidget(key: ValueKey("blue"), color: Colors.blue),
])
| Key type | Use case |
|---|---|
ValueKey(value) |
Unique value (ID, string) |
ObjectKey(object) |
Object reference |
UniqueKey() |
Force new state each rebuild |
GlobalKey |
Access widget state from outside its subtree |
Rule: Use keys when reordering stateful widgets in a list. Avoid UniqueKey() in build() — creates new widget every rebuild.
State Management
11. Explain the different state management approaches in Flutter.
| Approach | Complexity | Best for |
|---|---|---|
setState |
Low | Local, simple widget state |
InheritedWidget |
Medium | Propagate state down tree |
| Provider | Low-Medium | Small-medium apps |
| Riverpod | Medium | Testable, compile-safe Provider |
| Bloc/Cubit | Medium-High | Event-driven, large apps |
| GetX | Low | Rapid development (opinionated) |
| MobX | Medium | Reactive with code-gen |
| Redux | High | Predictable global state |
12. How does Provider work?
Provider uses InheritedWidget under the hood to propagate state:
// 1. Model
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // triggers rebuild of listening widgets
}
}
// 2. Provide at root
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
// 3. Consume anywhere in the tree
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final count = context.watch<CounterModel>().count; // rebuild on change
return Text("$count");
}
}
class IncrementButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
// context.read — no rebuild
onPressed: () => context.read<CounterModel>().increment(),
child: const Text("Increment"),
);
}
}
context.watch → subscribes, rebuilds on changecontext.read → reads once, no rebuildcontext.select → rebuilds only when selected value changes
13. What is Riverpod and how does it differ from Provider?
Riverpod is a rewrite of Provider that is compile-safe and doesn't rely on BuildContext:
// Riverpod provider definition (outside widget tree)
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
// Consuming in a widget
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(children: [
Text("$count"),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Text("+"),
),
]);
}
}
| Feature | Provider | Riverpod |
|---|---|---|
| Context required | Yes | No (WidgetRef) |
| Compile-time safety | Partial | Full |
| Testability | Good | Excellent |
| Provider override | Difficult | Easy |
| Auto-dispose | Manual | Built-in |
14. Explain BLoC pattern in Flutter.
BLoC (Business Logic Component) separates UI from business logic via events and states:
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
// States
class CounterState {
final int count;
const CounterState(this.count);
}
// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
}
}
// UI
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(children: [
Text("${state.count}"),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: const Text("+"),
),
]);
},
),
);
}
}
Cubit is a simpler BLoC variant — no events, just methods that emit states.
Navigation & Routing
15. What is the difference between Navigator 1.0 and Navigator 2.0?
| Navigator 1.0 | Navigator 2.0 | |
|---|---|---|
| API | Imperative | Declarative |
| Deep links | Difficult | First-class |
| URL sync | Manual | Automatic |
| Back button | Automatic | Manual control |
| Complexity | Low | High |
// Navigator 1.0 — imperative push/pop
Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage()));
Navigator.pop(context, result);
// Navigator 2.0 — declarative
Router(
routerDelegate: MyRouterDelegate(),
routeInformationParser: MyRouteInformationParser(),
)
Most teams use GoRouter (official package) which wraps Navigator 2.0 with a simple API:
final router = GoRouter(routes: [
GoRoute(path: "/", builder: (_, __) => const HomePage()),
GoRoute(
path: "/detail/:id",
builder: (context, state) => DetailPage(id: state.pathParameters["id"]!),
),
]);
// Navigation
context.go("/detail/42");
context.push("/detail/42"); // adds to stack
context.pop();
16. How do you pass data between screens?
// 1. Constructor arguments (simple)
Navigator.push(context, MaterialPageRoute(
builder: (_) => DetailPage(item: selectedItem),
));
// 2. Named routes with arguments
Navigator.pushNamed(context, "/detail", arguments: selectedItem);
// In target widget:
final item = ModalRoute.of(context)!.settings.arguments as Item;
// 3. GoRouter path/query parameters
context.go("/product/42?ref=home");
// state.pathParameters["id"] = "42"
// state.uri.queryParameters["ref"] = "home"
// 4. Return data on pop
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (_) => PickerPage()),
);
if (result != null) setState(() => _selected = result);
Async & Streams
17. Explain Future, async, and await in Dart.
// Future represents a value available in the future
Future<String> fetchUser(int id) async {
final response = await http.get(Uri.parse("/users/$id")); // non-blocking wait
if (response.statusCode != 200) throw Exception("Not found");
return jsonDecode(response.body)["name"] as String;
}
// Consuming a Future
void loadUser() async {
try {
final name = await fetchUser(1);
setState(() => _userName = name);
} catch (e) {
setState(() => _error = e.toString());
}
}
// FutureBuilder widget
FutureBuilder<String>(
future: fetchUser(1),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) return Text("Error: ${snapshot.error}");
return Text("Hello, ${snapshot.data}!");
},
)
18. What is a Stream in Dart?
A Stream is a sequence of asynchronous events over time:
// Single-subscription stream (like a file read)
Stream<int> countUp(int max) async* {
for (int i = 0; i <= max; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}
// Broadcast stream (multiple listeners, like UI events)
final controller = StreamController<String>.broadcast();
controller.sink.add("event1");
controller.stream.listen((event) => print(event));
// StreamBuilder widget
StreamBuilder<int>(
stream: countUp(10),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
return Text("Count: ${snapshot.data}");
},
)
| Future | Stream | |
|---|---|---|
| Values | One | Many over time |
| Completion | Once | Can be infinite |
| Listeners | Multiple | Single or broadcast |
| Error | One | Per-event |
19. What is the difference between Stream and StreamController?
// StreamController creates and controls a stream
class LocationService {
final _controller = StreamController<Position>.broadcast();
Stream<Position> get locationStream => _controller.stream;
void _onPositionUpdate(Position pos) => _controller.sink.add(pos);
void dispose() => _controller.close(); // ALWAYS close to prevent memory leaks
}
// Common mistake: not closing StreamController
// Use StreamSubscription to cancel
class _MyWidgetState extends State<MyWidget> {
late StreamSubscription<Position> _sub;
@override
void initState() {
super.initState();
_sub = locationService.locationStream.listen(_onLocation);
}
@override
void dispose() {
_sub.cancel(); // prevent memory leak
super.dispose();
}
}
Architecture Patterns
20. What is clean architecture in Flutter?
Clean architecture separates code into layers with one-way dependency:
Presentation (Widgets, BLoC/Cubit)
↓ depends on
Domain (Entities, Use Cases, Repository interfaces)
↓ depends on
Data (Repository implementations, Data Sources, DTOs)
// Domain layer — pure Dart, no Flutter imports
class User {
final String id;
final String name;
const User({required this.id, required this.name});
}
abstract class UserRepository {
Future<User> getUser(String id);
}
class GetUserUseCase {
final UserRepository repository;
GetUserUseCase(this.repository);
Future<User> call(String id) => repository.getUser(id);
}
// Data layer
class UserRepositoryImpl implements UserRepository {
final ApiClient apiClient;
UserRepositoryImpl(this.apiClient);
@override
Future<User> getUser(String id) async {
final dto = await apiClient.fetchUser(id);
return User(id: dto.id, name: dto.name);
}
}
// Presentation layer
class UserCubit extends Cubit<UserState> {
final GetUserUseCase getUserUseCase;
UserCubit(this.getUserUseCase) : super(UserInitial());
Future<void> loadUser(String id) async {
emit(UserLoading());
try {
final user = await getUserUseCase(id);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
21. What is dependency injection in Flutter?
DI provides dependencies to a class rather than creating them inside. Popular options:
// 1. get_it service locator
final sl = GetIt.instance;
void setupDependencies() {
sl.registerLazySingleton<ApiClient>(() => ApiClient());
sl.registerLazySingleton<UserRepository>(() => UserRepositoryImpl(sl()));
sl.registerFactory<UserCubit>(() => UserCubit(sl()));
}
// Usage
final cubit = sl<UserCubit>();
// 2. injectable (code-gen on top of get_it)
@injectable
class UserCubit extends Cubit<UserState> {
UserCubit(@injectable UserRepository repo) : super(UserInitial());
}
// 3. Riverpod providers (built-in DI)
final userRepoProvider = Provider<UserRepository>(
(ref) => UserRepositoryImpl(ref.read(apiClientProvider)),
);
Animations
22. What are the main animation types in Flutter?
| Type | API | Use case |
|---|---|---|
| Implicit | AnimatedContainer, AnimatedOpacity |
Simple value changes |
| Explicit | AnimationController + Tween |
Full control |
| Hero | Hero widget |
Shared element transitions |
| Rive | Rive package | Complex designer animations |
| Lottie | Lottie package | After Effects animations |
// Implicit — easiest
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
color: _expanded ? Colors.blue : Colors.red,
)
// Explicit — full control
class _FadeInState extends State<FadeIn>
with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
late Animation<double> _opacity;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_opacity = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _ctrl, curve: Curves.easeIn),
);
_ctrl.forward();
}
@override
void dispose() {
_ctrl.dispose(); // ALWAYS dispose AnimationController
super.dispose();
}
@override
Widget build(BuildContext context) =>
FadeTransition(opacity: _opacity, child: widget.child);
}
23. What is TickerProviderStateMixin vs SingleTickerProviderStateMixin?
// SingleTickerProviderStateMixin — one AnimationController
class _MyState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
// _ctrl = AnimationController(vsync: this, ...)
}
// TickerProviderStateMixin — multiple AnimationControllers
class _MyTabState extends State<MyTabWidget>
with TickerProviderStateMixin {
late AnimationController _ctrl1;
late AnimationController _ctrl2;
// Both use vsync: this
}
Using SingleTickerProviderStateMixin with multiple controllers throws a runtime error. Always use TickerProviderStateMixin when you need more than one controller.
Platform Integration
24. What is a Platform Channel?
Platform channels allow Flutter to communicate with native Android/iOS code:
// Flutter (Dart) side
class BatteryService {
static const _channel = MethodChannel("com.example/battery");
Future<int> getBatteryLevel() async {
try {
return await _channel.invokeMethod<int>("getBatteryLevel") ?? -1;
} on PlatformException catch (e) {
throw Exception("Failed to get battery: ${e.message}");
}
}
}
// Android (Kotlin) side
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
val level = getBatteryLevel()
if (level != -1) result.success(level)
else result.error("UNAVAILABLE", "Battery unavailable", null)
} else result.notImplemented()
}
}
}
| Channel type | Direction | Use case |
|---|---|---|
| MethodChannel | Bidirectional | One-off method calls |
| EventChannel | Native → Flutter | Continuous streams (sensors, BLE) |
| BasicMessageChannel | Bidirectional | Custom message codecs |
25. How do you handle permissions in Flutter?
// Using permission_handler package
import "package:permission_handler/permission_handler.dart";
Future<void> requestCamera() async {
final status = await Permission.camera.status;
if (status.isGranted) {
_openCamera();
} else if (status.isDenied) {
final result = await Permission.camera.request();
if (result.isGranted) _openCamera();
} else if (status.isPermanentlyDenied) {
// User denied with "Don't ask again" — open app settings
await openAppSettings();
}
}
// Multiple permissions at once
Future<void> requestMultiple() async {
final statuses = await [
Permission.camera,
Permission.microphone,
].request();
if (statuses[Permission.camera]!.isGranted &&
statuses[Permission.microphone]!.isGranted) {
_startVideoCall();
}
}
Also configure AndroidManifest.xml (Android) and Info.plist (iOS) with required permission strings.
Testing
26. What are the three types of tests in Flutter?
| Type | Speed | Scope | Command |
|---|---|---|---|
| Unit tests | Fast | Functions, classes, BLoC | flutter test |
| Widget tests | Medium | Single widget rendering | flutter test |
| Integration tests | Slow | Full app on device/emulator | flutter test integration_test/ |
// Unit test
test("counter increments", () {
final cubit = CounterCubit();
cubit.increment();
expect(cubit.state, 1);
});
// Widget test
testWidgets("shows user name", (tester) async {
await tester.pumpWidget(
const MaterialApp(home: UserCard(name: "Alice")),
);
expect(find.text("Alice"), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump(); // rebuild after state change
expect(find.text("Tapped"), findsOneWidget);
});
// Bloc test (bloc_test package)
blocTest<CounterBloc, CounterState>(
"emits [1] when Increment is added",
build: () => CounterBloc(),
act: (bloc) => bloc.add(Increment()),
expect: () => [const CounterState(1)],
);
27. How do you mock dependencies in Flutter tests?
// Using mockito
@GenerateMocks([UserRepository])
void main() {
late MockUserRepository mockRepo;
late UserCubit cubit;
setUp(() {
mockRepo = MockUserRepository();
cubit = UserCubit(GetUserUseCase(mockRepo));
});
tearDown(() => cubit.close());
test("loads user successfully", () async {
when(mockRepo.getUser("1"))
.thenAnswer((_) async => const User(id: "1", name: "Alice"));
await cubit.loadUser("1");
expect(cubit.state, isA<UserLoaded>());
expect((cubit.state as UserLoaded).user.name, "Alice");
});
test("emits error on failure", () async {
when(mockRepo.getUser("999"))
.thenThrow(Exception("Not found"));
await cubit.loadUser("999");
expect(cubit.state, isA<UserError>());
});
}
Performance & Advanced
28. What causes jank in Flutter and how do you fix it?
Jank occurs when frames take longer than 16.67ms (60fps) or 8.33ms (120fps):
Common causes and fixes:
| Cause | Fix |
|---|---|
Heavy build() method |
Extract widgets, use const |
| Rebuilding too much | RepaintBoundary, context.select |
| Expensive operations on main isolate | Move to Isolate or compute() |
| Loading images synchronously | cached_network_image, lazy loading |
Missing ListView.builder |
Use .builder for long lists |
| Opacity on large widgets | Offstage or Visibility instead |
// Bad — rebuilds entire list on any change
Column(children: items.map((e) => ItemWidget(e)).toList())
// Good — only builds visible items
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemWidget(items[index]),
)
// Move CPU work off main thread
final result = await compute(heavyComputation, data);
// RepaintBoundary — isolates repaint
RepaintBoundary(
child: ExpensiveAnimatedWidget(), // won't trigger parent repaint
)
29. What is Isolate in Dart?
Isolates are independent workers with their own memory heap — true parallelism in Dart:
// compute() — simple isolate for one-shot tasks
List<int> sortNumbers(List<int> numbers) {
return [...numbers]..sort();
}
final sorted = await compute(sortNumbers, [5, 2, 8, 1, 9]);
// Isolate.spawn — full control
void heavyTask(SendPort sendPort) {
// runs in separate isolate
final result = performExpensiveCalc();
sendPort.send(result);
}
final receivePort = ReceivePort();
await Isolate.spawn(heavyTask, receivePort.sendPort);
final result = await receivePort.first;
// Dart 2.15+: Isolate.run() (simpler API)
final sorted = await Isolate.run(() => [5, 2, 8, 1]..sort());
Important: Isolates cannot share memory. Communication is via SendPort/ReceivePort with message passing (objects are copied).
30. What is InheritedWidget and when would you use it directly?
InheritedWidget is the foundation for Provider, Theme, MediaQuery, and all Flutter state propagation:
class AppThemeData extends InheritedWidget {
final Color primaryColor;
const AppThemeData({
required this.primaryColor,
required super.child,
super.key,
});
static AppThemeData of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<AppThemeData>();
assert(result != null, "No AppThemeData found in context");
return result!;
}
@override
bool updateShouldNotify(AppThemeData oldWidget) =>
primaryColor != oldWidget.primaryColor;
}
// Usage
final color = AppThemeData.of(context).primaryColor;
updateShouldNotify controls whether dependent widgets rebuild when the InheritedWidget changes — return false to suppress unnecessary rebuilds.
31. How does Flutter handle hot reload vs hot restart?
| Hot Reload | Hot Restart | |
|---|---|---|
| Speed | ~1 second | ~3-5 seconds |
| State preserved | Yes | No |
| What updates | Widget tree | Full app restart |
| When to use | UI changes | Logic/state structure changes |
| What won't update | initState, main(), global vars |
Nothing |
Hot reload works by injecting updated source code into the Dart VM and rebuilding the widget tree while preserving the current state. Changes to initState(), constructors, or global variables require hot restart.
32. What is MediaQuery and LayoutBuilder?
// MediaQuery — device-level info
class ResponsiveWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final padding = MediaQuery.of(context).padding; // safe area
final isTablet = size.width > 600;
return isTablet ? TabletLayout() : MobileLayout();
}
}
// LayoutBuilder — parent constraint-based
class AdaptiveCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 400) {
return WideCard();
}
return NarrowCard();
},
);
}
}
Prefer LayoutBuilder when adapting to the parent widget's size. Use MediaQuery for device-level info (screen size, orientation, safe area).
33. What is GlobalKey and what are its risks?
// GlobalKey allows accessing widget state from outside its subtree
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: const Text("Submit"),
),
]),
)
Risks:
GlobalKeyforces a new element — can't be used in const widgets- Moving a widget with
GlobalKeyacross the tree is expensive (triggers full subtree rebuild) - Overuse leads to tight coupling and hard-to-test code
Use GlobalKey sparingly — mainly for Form, ScaffoldMessenger, or Navigator.
34. Explain the Flutter build modes.
| Mode | Command | Optimizations | Use case |
|---|---|---|---|
| Debug | flutter run |
JIT, asserts, DevTools | Development |
| Profile | flutter run --profile |
AOT, minimal debuggability | Performance profiling |
| Release | flutter build |
AOT, tree shaking, minification | Production |
// Conditional code by mode
import "package:flutter/foundation.dart";
if (kDebugMode) {
print("Debug only logging");
}
if (kReleaseMode) {
// Initialize production crash reporting
FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
}
if (kProfileMode) {
// Profile-specific setup
}
35. What is tree shaking in Flutter?
Tree shaking removes unused code from the final build. Flutter's AOT compiler:
- Starts from
main()entry point - Traces all reachable code
- Eliminates everything unreachable
// This entire class is removed if never used
class UnusedUtility {
static String format(String s) => s.toUpperCase();
}
// Icons are tree-shaken
// Only icons you actually use are included in the binary
Icons.home // included
Icons.settings // included only if referenced
// All other 1000+ icons — excluded
Use flutter build apk --analyze-size to inspect what's included in your binary.
Common Anti-Patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
setState in initState |
Throws error on first frame | Use addPostFrameCallback |
Not disposing AnimationController |
Memory leak | dispose() in State.dispose() |
context after async gap |
May be unmounted | Check mounted before using context |
Large build() methods |
Poor readability, hard to optimize | Extract private methods or widgets |
GlobalKey everywhere |
Performance + tight coupling | Use callbacks or state management |
ListView without .builder |
All items built upfront | Use ListView.builder |
| Blocking main isolate | UI jank | Use compute() or Isolate.run() |
Ignoring const lint |
Unnecessary rebuilds | Add const wherever possible |
// Dangerous: using context after async gap
void _save() async {
await saveToDatabase();
// Widget may have been unmounted!
Navigator.pop(context); // may throw
}
// Safe: check mounted
void _save() async {
await saveToDatabase();
if (!mounted) return; // guard
Navigator.pop(context);
}
Flutter vs React Native vs Native
| Feature | Flutter | React Native | Native (iOS/Android) |
|---|---|---|---|
| Language | Dart | JavaScript/TypeScript | Swift/Kotlin |
| UI engine | Own (Skia/Impeller) | Native components | Native |
| Performance | Near-native | Good (JSI bridge) | Best |
| Code sharing | ~95% | ~85% | 0% |
| Hot reload | Yes | Yes | Partial |
| Bundle size | Larger (~5MB) | Smaller | N/A |
| Animations | Excellent | Good | Excellent |
| Desktop/Web | Yes (experimental) | Limited | No |
Frequently Asked Questions
Q: Is Flutter production-ready?
Yes. Flutter powers apps at Google, BMW, Alibaba, eBay, and thousands of startups. Version 3.x has stable support for Android, iOS, Web, and Desktop.
Q: How does Flutter differ from React Native architecturally?
Flutter renders everything using its own Skia/Impeller graphics engine — no native UI components. React Native bridges to native components (UIKit/Views). Flutter's approach gives pixel-perfect consistency across platforms; React Native's gives native look and feel automatically.
Q: What's the best state management solution?
It depends on app size: setState for local state, Provider/Riverpod for small-medium apps, BLoC for large apps needing strict separation and testability.
Q: Can Flutter access native device APIs?
Yes, via Platform Channels (direct) or the extensive pub.dev package ecosystem. Most common APIs (camera, location, Bluetooth, push notifications, SQLite) have well-maintained packages.
Q: How do you handle background tasks in Flutter?
Use flutter_background_service or workmanager for background Dart code. For platform-native background processing, use Android WorkManager or iOS BGTaskScheduler via Platform Channels.
Q: What is pubspec.yaml?
The project manifest file for Flutter/Dart. It declares the app name, version, dependencies (dependencies:), dev dependencies (dev_dependencies:), assets, and fonts. Run flutter pub get after changes.