Flutter apps run on phones, tablets, foldables, desktops and the web, so one fixed layout isn't enough.
Responsive means the layout adjusts to available space; adaptive means it adjusts to the device's input and conventions (mouse vs touch, iOS vs Android).
Tools for size:
MediaQuery.sizeOf(context)— the window size. Use the specificsizeOf,paddingOf,orientationOfmethods rather thanMediaQuery.of(context), so the widget only rebuilds when that aspect changes.- `LayoutBuilder` — gives the constraints from the parent, which is better for reusable components that may be placed in a side panel rather than full screen.
- Breakpoints, following Material 3 window size classes: compact below 600 dp wide, medium 600–839, expanded 840 and up. A compact layout might use
NavigationBarat the bottom; wider ones aNavigationRailand two panes.
Other essentials:
- `SafeArea` keeps content clear of notches, status bars and home indicators.
- Flexible widgets (
Expanded,Wrap,GridViewwithmaxCrossAxisExtent) adapt better than fixed sizes. OrientationBuilderor the size itself for rotation.- Text scaling: test with large fonts; don't fix heights around text.
Adaptive touches: Theme.of(context).platform or defaultTargetPlatform to choose platform-specific behavior, .adaptive constructors (Switch.adaptive, CircularProgressIndicator.adaptive), hover and keyboard shortcuts on desktop and web.
Don't lock the app to portrait unless there's a strong reason; Android large-screen guidance discourages it.
Example
import 'package:flutter/material.dart';
class AdaptiveShell extends StatelessWidget {
const AdaptiveShell({super.key, required this.index, required this.onSelect, required this.body});
final int index;
final ValueChanged<int> onSelect;
final Widget body;
static const _dests = [(Icons.inbox, 'Inbox'), (Icons.settings, 'Settings')];
@override
Widget build(BuildContext context) {
final wide = MediaQuery.sizeOf(context).width >= 600; // compact < 600
if (!wide) {
return Scaffold(body: SafeArea(child: body), bottomNavigationBar: NavigationBar(
selectedIndex: index, onDestinationSelected: onSelect,
destinations: [for (final (i, l) in _dests) NavigationDestination(icon: Icon(i), label: l)]));
}
return Scaffold(body: SafeArea(child: Row(children: [
NavigationRail(selectedIndex: index, onDestinationSelected: onSelect,
labelType: NavigationRailLabelType.all,
destinations: [for (final (i, l) in _dests) NavigationRailDestination(icon: Icon(i), label: Text(l))]),
const VerticalDivider(width: 1),
Expanded(child: body),
])));
}
}Remember
- Use
MediaQuery.sizeOffor window size and LayoutBuilder for local space - Switch layouts at breakpoints (e.g. 600 and 840 dp)
- Wrap content in SafeArea and test large text
Watch out for
- Using
MediaQuery.of(context).sizeeverywhere, causing rebuilds on any media change such as the keyboard opening.