daries.
enum ScreenBreakpoint {
phone(maxWidth: 600),
tablet(maxWidth: 1024),
desktop(maxWidth: double.infinity);
final double maxWidth;
const ScreenBreakpoint({required this.maxWidth});
}
Step 2: Create a Reactive Breakpoint Provider
Avoid calling MediaQuery.of(context) inside build methods. Instead, compute the breakpoint once and expose it via a ValueNotifier or state management solution.
class ResponsiveContext extends ValueNotifier<ScreenBreakpoint> {
ResponsiveContext() : super(ScreenBreakpoint.phone);
void updateFromConstraints(BoxConstraints constraints) {
final breakpoint = constraints.maxWidth <= ScreenBreakpoint.phone.maxWidth
? ScreenBreakpoint.phone
: constraints.maxWidth <= ScreenBreakpoint.tablet.maxWidth
? ScreenBreakpoint.tablet
: ScreenBreakpoint.desktop;
if (value != breakpoint) value = breakpoint;
}
}
Step 3: Wrap with LayoutBuilder for Local Adaptation
LayoutBuilder provides parent constraints without triggering global rebuilds. Use it at layout boundaries, not leaf widgets.
class AdaptiveLayout extends StatelessWidget {
final WidgetBuilder mobile;
final WidgetBuilder tablet;
final WidgetBuilder desktop;
const AdaptiveLayout({
required this.mobile,
required this.tablet,
required this.desktop,
super.key,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final breakpoint = constraints.maxWidth <= ScreenBreakpoint.phone.maxWidth
? ScreenBreakpoint.phone
: constraints.maxWidth <= ScreenBreakpoint.tablet.maxWidth
? ScreenBreakpoint.tablet
: ScreenBreakpoint.desktop;
switch (breakpoint) {
case ScreenBreakpoint.phone:
return mobile(context);
case ScreenBreakpoint.tablet:
return tablet(context);
case ScreenBreakpoint.desktop:
return desktop(context);
}
},
);
}
}
Step 4: Implement Adaptive Navigation and Components
Navigation patterns must shift based on screen real estate. Use a unified Scaffold wrapper that injects the correct navigation widget.
class AdaptiveScaffold extends StatelessWidget {
final Widget body;
final List<NavigationDestination> destinations;
const AdaptiveScaffold({
required this.body,
required this.destinations,
super.key,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isNarrow = constraints.maxWidth <= ScreenBreakpoint.phone.maxWidth;
return Scaffold(
body: Row(
children: [
if (!isNarrow) NavigationRail(destinations: destinations),
Expanded(child: body),
],
),
bottomNavigationBar: isNarrow
? NavigationBar(destinations: destinations)
: null,
);
},
);
}
}
Architecture Decisions and Rationale
- Why
LayoutBuilder over MediaQuery? LayoutBuilder rebuilds only when parent constraints change, not on every orientation or padding shift. It isolates layout recomputation to the widget subtree that actually needs adaptation.
- Why separate breakpoint logic? Decoupling breakpoint calculation from UI rendering enables unit testing, preview mocking, and consistent behavior across platforms. Business logic never touches screen dimensions.
- Why avoid
MediaQuery.of(context) in build? It registers a dependency on the nearest MediaQuery, forcing the entire subtree to rebuild on any device metric change, including keyboard visibility and system UI chrome. This is a primary source of unnecessary frame drops.
Pitfall Guide
-
Hardcoding Dimensions
Using SizedBox(width: 300) or Container(height: 50) locks layouts to specific densities. Flutter's logical pixels scale with device pixel ratio, but fixed sizes ignore aspect ratio and available space. Replace with Flexible, Expanded, FractionallySizedBox, or AspectRatio.
-
Overusing MediaQuery.of(context)
Calling MediaQuery.of(context) inside build creates a hidden dependency. Every window resize, orientation change, or padding update triggers a full subtree rebuild. Use LayoutBuilder for constraint-driven adaptation or extract metrics once at the route level.
-
Ignoring SafeArea and System Padding
Notch, status bar, navigation bar, and foldable hinge regions consume layout space. Wrapping content in SafeArea or manually applying MediaQuery.viewPadding prevents content clipping. Always account for viewInsets when the keyboard appears.
-
Rebuilding Entire Trees on Orientation Change
Conditional rendering based on MediaQuery.orientation without constraint isolation forces Flutter to tear down and reconstruct unrelated widgets. Use LayoutBuilder to scope rebuilds, and prefer const constructors where possible to skip rebuilds entirely.
-
Skipping MediaQueryData Override Testing
Relying on emulator rotation is insufficient. Use MediaQuery widget overrides in widget tests to simulate arbitrary screen sizes, densities, and padding configurations. This catches overflow and alignment bugs before production.
-
Forgetting Foldable and Hinge Constraints
Foldables introduce dual-screen and hinge zones. MediaQuery.displayFeatures provides hinge bounds and screen separation. Layouts must avoid placing interactive elements across hinge regions. Use DisplayFeature filtering to split content or add padding.
-
Mixing Responsive Logic with Business State
Embedding MediaQuery or LayoutBuilder inside view models or controllers couples UI adaptation to domain logic. Keep responsive decisions in the presentation layer. Pass computed breakpoints or constraints down as immutable parameters.
Best Practices from Production:
- Profile layout passes with the Flutter DevTools Performance overlay. Look for yellow/red rebuild indicators.
- Use
const widgets extensively. Immutable widgets skip rebuilds even when parent constraints change.
- Prefer
Sliver-based scrolling for dynamic lists. ListView and GridView adapt to viewport changes without manual constraint math.
- Test on physical devices with varying DPIs. Emulators mask rasterization and constraint propagation quirks.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single-screen mobile app | LayoutBuilder + breakpoint enum | Minimal overhead, predictable rebuild scope | Low |
| Tablet/desktop parity | Breakpoint Architecture + adaptive navigation | Maintains UX consistency across form factors | Medium |
| Foldable/hinge support | MediaQuery.displayFeatures + constraint splitting | Prevents UI occlusion and touch target loss | High |
| Legacy codebase migration | Incremental LayoutBuilder wrapping | Avoids full rewrite, isolates risk per screen | Low-Medium |
| Performance-critical app | const widgets + isolated responsive context | Eliminates unnecessary subtree rebuilds | Low |
Configuration Template
// responsive_config.dart
import 'package:flutter/material.dart';
enum ScreenBreakpoint {
phone(maxWidth: 600),
tablet(maxWidth: 1024),
desktop(maxWidth: double.infinity);
final double maxWidth;
const ScreenBreakpoint({required this.maxWidth});
}
class ResponsiveContext extends ValueNotifier<ScreenBreakpoint> {
ResponsiveContext() : super(ScreenBreakpoint.phone);
void updateFromConstraints(BoxConstraints constraints) {
final next = constraints.maxWidth <= ScreenBreakpoint.phone.maxWidth
? ScreenBreakpoint.phone
: constraints.maxWidth <= ScreenBreakpoint.tablet.maxWidth
? ScreenBreakpoint.tablet
: ScreenBreakpoint.desktop;
if (value != next) value = next;
}
}
class AdaptiveLayout extends StatelessWidget {
final WidgetBuilder mobile;
final WidgetBuilder tablet;
final WidgetBuilder desktop;
const AdaptiveLayout({
required this.mobile,
required this.tablet,
required this.desktop,
super.key,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final bp = constraints.maxWidth <= ScreenBreakpoint.phone.maxWidth
? ScreenBreakpoint.phone
: constraints.maxWidth <= ScreenBreakpoint.tablet.maxWidth
? ScreenBreakpoint.tablet
: ScreenBreakpoint.desktop;
switch (bp) {
case ScreenBreakpoint.phone: return mobile(context);
case ScreenBreakpoint.tablet: return tablet(context);
case ScreenBreakpoint.desktop: return desktop(context);
}
},
);
}
}
Quick Start Guide
- Create the breakpoint enum and
ResponsiveContext: Copy the template into lib/core/responsive/. The enum defines thresholds; the notifier tracks computed state.
- Wrap adaptive screens: Replace root
build methods with AdaptiveLayout, passing distinct mobile, tablet, and desktop builders.
- Replace fixed dimensions: Scan for
SizedBox, Container with hardcoded width/height, or AspectRatio mismatches. Convert to Flexible, Expanded, or FractionallySizedBox.
- Add safe area handling: Wrap top-level content in
SafeArea(child: ...). For forms, listen to MediaQuery.viewInsets to adjust padding when the keyboard appears.
- Validate with DevTools: Run
flutter run --profile, open Performance overlay, and rotate the device or resize the window. Confirm layout latency stays under 16ms and rebuild counts remain localized.