← Articles

Designing for multiple screen sizes in Flutter

By Ann Tech · 2 April 2026

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.

Sign in to like, dislike, or report.

Designing for multiple screen sizes in Flutter — ANN Tech