Responsive Flutter Layouts: One App for Phone, Tablet and Web
Make your Flutter UI responsive with MediaQuery, LayoutBuilder, breakpoints and adaptive navigation. Build one layout that looks right on phones, tablets, desktop and the web.
By Yaqoob Developer · · 5 min read
- #Flutter
- #Responsive Design
- #LayoutBuilder
- #Flutter Web
- #Flutter UI
Flutter runs on phones, tablets, laptops and in the browser. But a screen designed for a 390-pixel-wide phone looks empty and stretched on a 1400-pixel laptop. Text lines become too long to read, buttons stretch across the whole screen, and grids show two giant cards.
A responsive layout fixes this. In this guide you will learn:
- The difference between MediaQuery and LayoutBuilder
- How to choose breakpoints
- How to cap content width so it never stretches too far
- Grids that add columns as the screen grows
- Switching between a bottom bar and a side rail
MediaQuery vs LayoutBuilder
Both tell you how much space you have, but they measure different things.
MediaQuery gives you the size of the whole screen (the app window):
final width = MediaQuery.sizeOf(context).width;
LayoutBuilder gives you the space available to this widget, set by its parent:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return const WideLayout();
}
return const NarrowLayout();
},
)
Which to use?
- Use MediaQuery for app-level decisions: which navigation to show, how many panes.
- Use LayoutBuilder for components: a card that changes layout when it is narrow, no matter how big the screen is.
Use MediaQuery.sizeOf(context) rather than MediaQuery.of(context).size. The sizeOf version only rebuilds your widget when the size changes, not when anything else in MediaQuery changes (like the keyboard opening).
Step 1: Breakpoints
A breakpoint is a width where your layout changes. Material 3 uses these, and they work well for most apps:
| Width | Window class | Typical device |
|---|---|---|
| under 600 | Compact | Phones |
| 600 to 839 | Medium | Small tablets, foldables |
| 840 and above | Expanded | Large tablets, laptops, desktop |
Put them in one place so the whole app agrees:
import 'package:flutter/material.dart';
enum ScreenSize { compact, medium, expanded }
ScreenSize screenSizeOf(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
if (width >= 840) return ScreenSize.expanded;
if (width >= 600) return ScreenSize.medium;
return ScreenSize.compact;
}
Step 2: Cap the content width
The quickest win for tablets and web: don't let content grow wider than a comfortable reading width. Wrap the page body:
class MaxWidth extends StatelessWidget {
const MaxWidth({super.key, this.maxWidth = 720, required this.child});
final double maxWidth;
final Widget child;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: child,
),
);
}
}
Use it on forms, settings pages and articles:
Scaffold(
body: MaxWidth(
child: ListView(
padding: const EdgeInsets.all(24),
children: const [/* ... */],
),
),
)
A login form that is 1400 pixels wide looks wrong. The same form capped at 420 pixels and centered looks designed. Try it on any of the login screen projects on this site.
Step 3: Grids that add columns
Don't hard-code crossAxisCount: 2. Tell the grid how wide each item may be, and let it calculate the number of columns:
GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 220,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 3 / 4,
),
itemCount: products.length,
itemBuilder: (context, i) => ProductCard(product: products[i]),
)
With maxCrossAxisExtent: 220 you get 2 columns on a phone, 3 or 4 on a tablet, and 6 or more on a desktop, with no if statements. If grids are new to you, start with the ListView and GridView guide.
Step 4: Adaptive navigation
On phones, navigation belongs at the bottom where thumbs reach. On tablets and desktop, a side NavigationRail uses the extra width better:
class AdaptiveShell extends StatefulWidget {
const AdaptiveShell({super.key});
@override
State<AdaptiveShell> createState() => _AdaptiveShellState();
}
class _AdaptiveShellState extends State<AdaptiveShell> {
int _index = 0;
static const _pages = [
Center(child: Text('Home')),
Center(child: Text('Search')),
Center(child: Text('Profile')),
];
@override
Widget build(BuildContext context) {
final wide = MediaQuery.sizeOf(context).width >= 600;
if (wide) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
labelType: NavigationRailLabelType.all,
destinations: const [
NavigationRailDestination(icon: Icon(Icons.home), label: Text('Home')),
NavigationRailDestination(icon: Icon(Icons.search), label: Text('Search')),
NavigationRailDestination(icon: Icon(Icons.person), label: Text('Profile')),
],
),
const VerticalDivider(width: 1),
Expanded(child: _pages[_index]),
],
),
);
}
return Scaffold(
body: _pages[_index],
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
The selected index lives in the state, so rotating a tablet from portrait to landscape keeps you on the same tab. For more on the bottom bar itself, read the NavigationBar guide.
Step 5: Two panes on large screens
Email and chat apps show the list and the selected item side by side on large screens. With the breakpoints from Step 1:
@override
Widget build(BuildContext context) {
if (screenSizeOf(context) == ScreenSize.expanded) {
return Row(
children: [
const SizedBox(width: 360, child: ChatList()),
const VerticalDivider(width: 1),
Expanded(child: ChatDetail(chat: _selected)),
],
);
}
return const ChatList(); // tapping a chat pushes ChatDetail
}
The same two widgets are reused. Only the arrangement changes. See the chat app messages UI for a design that works well this way.
Things that are not about width
Responsive design is also about:
- Text scaling. Users can make text larger in their phone settings. Don't put text in fixed-height boxes, and test with the largest text size.
- Orientation. Landscape phones are wide but very short. A layout that fits a tablet may overflow vertically on a landscape phone. Scrollable pages avoid this.
- Mouse and keyboard. On web and desktop, add hover effects and make sure Tab moves between fields.
Common mistakes
Using fixed widths everywhere. width: 360 looks fine on your test phone and broken everywhere else. Prefer Expanded, Flexible, percentages from constraints, and max widths.
Scaling everything by screen width. Packages that make every font and padding grow with the screen make tablets look like giant phones. Show more content on bigger screens, not bigger content.
Checking Platform.isAndroid to decide the layout. Platform tells you the operating system, not the screen size. An Android tablet and an Android phone need different layouts.
Summary
- MediaQuery.sizeOf for app-level decisions, LayoutBuilder for components.
- Use Material breakpoints: 600 and 840.
- Cap content with ConstrainedBox(maxWidth).
- Let grids pick their columns with SliverGridDelegateWithMaxCrossAxisExtent.
- Switch NavigationBar to NavigationRail on wide screens.
Keep reading
More articles
· 5 min read
Flutter Animations for Beginners: AnimatedContainer, AnimatedSwitcher and Hero
Learn Flutter animations the easy way. Use AnimatedContainer, AnimatedOpacity, AnimatedSwitcher, TweenAnimationBuilder and Hero to make your UI feel smooth, with no AnimationController needed.
· 5 min read
Flutter Dark Mode: Light and Dark Theme with a Toggle That Remembers
Add dark mode to a Flutter app with Material 3. Create light and dark themes from one seed color, follow the system setting, add a toggle and save the user's choice with shared_preferences.
· 5 min read
Flutter Form Validation: Build a Login Form with TextFormField
Validate user input in Flutter with Form, TextFormField and GlobalKey. Build a complete login form with email and password rules, a show/hide password button and a loading state.