Designing for multiple screen sizes in Flutter

By Saki Yamada · 2 April 2026491 views
Designing for multiple screen sizes in Flutter

Flutter runs on phones, tablets, foldables, web, and desktop — all from one codebase. But a layout designed for a 375pt phone looks broken on a 1024pt tablet. Responsive design bridges that gap.

Reading screen dimensions

final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;

// Safer: LayoutBuilder gives you constraints from the parent, not the screen
LayoutBuilder(
  builder: (context, constraints) {
    final isWide = constraints.maxWidth > 600;
    return isWide ? WideLayout() : NarrowLayout();
  },
)

Breakpoints

Define breakpoints as constants so they're consistent across the app:

class Breakpoints {
  static const double mobile = 600;
  static const double tablet = 900;
  static const double desktop = 1200;
}

extension ScreenType on BuildContext {
  bool get isMobile => MediaQuery.of(this).size.width < Breakpoints.mobile;
  bool get isTablet =>
      MediaQuery.of(this).size.width >= Breakpoints.mobile &&
      MediaQuery.of(this).size.width < Breakpoints.tablet;
  bool get isDesktop => MediaQuery.of(this).size.width >= Breakpoints.tablet;
}

// Usage:
if (context.isMobile) return const MobileLayout();
if (context.isTablet) return const TabletLayout();
return const DesktopLayout();

Adaptive navigation

On mobile, a bottom navigation bar is standard. On tablet/desktop, a side rail or drawer fits better:

class AdaptiveScaffold extends StatelessWidget {
  const AdaptiveScaffold({super.key, required this.body, required this.destinations});
  final Widget body;
  final List<NavigationDestination> destinations;

  @override
  Widget build(BuildContext context) {
    if (context.isDesktop) {
      return Scaffold(
        body: Row(
          children: [
            NavigationRail(
              destinations: destinations
                  .map((d) => NavigationRailDestination(
                        icon: d.icon,
                        label: Text(d.label),
                      ))
                  .toList(),
              selectedIndex: 0,
            ),
            Expanded(child: body),
          ],
        ),
      );
    }
    return Scaffold(
      body: body,
      bottomNavigationBar: NavigationBar(destinations: destinations),
    );
  }
}

Flexible grid layouts

// Adapts column count based on available width
GridView.builder(
  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 300, // Each cell max 300px wide
    crossAxisSpacing: 16,
    mainAxisSpacing: 16,
    childAspectRatio: 0.75,
  ),
  itemCount: products.length,
  itemBuilder: (_, i) => ProductCard(product: products[i]),
)

Responsive typography

extension ResponsiveTextTheme on BuildContext {
  TextTheme get responsiveTextTheme {
    final base = Theme.of(this).textTheme;
    if (isDesktop) {
      return base.copyWith(
        displayLarge: base.displayLarge?.copyWith(fontSize: 96),
        headlineMedium: base.headlineMedium?.copyWith(fontSize: 48),
      );
    }
    return base;
  }
}

Testing multiple sizes

testWidgets('shows rail on wide screen', (tester) async {
  await tester.binding.setSurfaceSize(const Size(1200, 800));
  await tester.pumpWidget(const MaterialApp(home: HomeScreen()));
  expect(find.byType(NavigationRail), findsOneWidget);
  expect(find.byType(NavigationBar), findsNothing);
});

testWidgets('shows bottom nav on mobile', (tester) async {
  await tester.binding.setSurfaceSize(const Size(375, 812));
  await tester.pumpWidget(const MaterialApp(home: HomeScreen()));
  expect(find.byType(NavigationBar), findsOneWidget);
  expect(find.byType(NavigationRail), findsNothing);
});

Common pitfalls

Hard-coding pixel values. Container(width: 320) looks right on one phone and broken on every other screen. Use FractionallySizedBox, Expanded, or LayoutBuilder constraints instead.

Only testing on one device. During development, switch between a phone simulator and a tablet simulator regularly. Issues only visible on tablet are invisible on phone, and vice versa.

Forgetting safe areas. On notched and punch-hole screens, content can hide behind the camera cutout. Always wrap content in SafeArea or handle MediaQuery.of(context).padding explicitly.

Comments

No comments yet. Be the first!

Sign in to leave a comment.