All Guides
    Flutter Basics
    Flutter Basics
    Layouts
    Responsive Design

    Building Responsive Layouts

    Published April 2026

    Introduction

    In the early days of mobile development, application designers typically targeted a small, standardized list of display sizes. Today, the platform landscape has expanded dramatically. A modern Flutter application can run on compact mobile screens, folding devices, high-resolution tablets, wide desktop monitors, and web browser windows of any shape and size.

    Building responsive layouts in Flutter means designing user interfaces that adapt to this variety of screen dimensions, orientations, and platform constraints. Rather than scaling or stretching static layouts, you restructure the layout flow dynamically based on the current window size (for example, showing a navigation drawer on mobile but switching to a persistent sidebar on desktop).

    To achieve this, Flutter provides powerful tools like MediaQuery and LayoutBuilder. While they may seem similar at first glance, they serve different purposes: one provides device-level screen metrics, while the other gives you the layout constraints of the parent widget. In this guide, you will learn the theory of constraints, compare responsive APIs, and build a reusable responsive container helper.


    Prerequisites

    Before building responsive layouts, ensure you have:


    Core Explanation: Constraints & Sizing Tools

    In Flutter, layouts are governed by a simple rule: Constraints go down. Sizes go up. Parents set positions.

    A parent widget passes constraints (minimum and maximum width and height) down to its child. The child widget decides its own size within those limits and reports it back up. The parent widget then positions the child.

    To make layouts responsive, you can inspect these constraints at runtime using two key APIs:

    ┌────────────────────────────────────────────────────────┐
    │                      VIEWPORT                          │
    │                                                        │
    │  MediaQuery.of(context).size   (Device-Level Metrics)  │
    │  ┌──────────────────────────────────────────────────┐  │
    │  │ Parent Widget Container (e.g. Card, CardBody)    │  │
    │  │                                                  │  │
    │  │  LayoutBuilder constraints  (Widget-Level Limits) │  │
    │  │  ┌────────────────────────────────────────────┐  │  │
    │  │  │ Child Widget                               │  │  │
    │  │  └────────────────────────────────────────────┘  │  │
    │  └──────────────────────────────────────────────────┘  │
    └────────────────────────────────────────────────────────┘
    

    1. MediaQuery (Device-Level Metrics)

    MediaQuery.of(context) returns metadata about the device screen and system settings, such as size, orientation, and text scale factors.

    • When to Use: Use MediaQuery to inspect screen size or orientation (e.g., checking if the device width is less than 600 pixels to toggle between portrait and landscape modes).
    • Caveat: Calling MediaQuery.of(context) causes the widget to rebuild whenever any screen property changes (like the keyboard opening or the window resizing).

    2. LayoutBuilder (Widget-Level Constraints)

    LayoutBuilder provides a builder function containing the layout constraints of its parent widget (BoxConstraints).

    • When to Use: Use LayoutBuilder to adapt a widget's layout based on the space available to it, regardless of the screen size (e.g. rendering a grid with two columns if the parent card is wide, or one column if the card is narrow).
    • Advantage: It is highly modular and doesn't depend on global screen size metadata.

    3. Breakpoints Configuration

    Standard breakpoint widths used by professional Flutter developers to adapt layouts include:

    • Mobile: Width less than 600 pixels.
    • Tablet: Width between 600 and 1200 pixels.
    • Desktop: Width greater than 1200 pixels.

    Practical Example: A Reusable Responsive Layout Widget

    Let's build a clean, compiler-ready ResponsiveLayout widget that dynamically switches between mobile, tablet, and desktop layouts based on the available width.

    import 'package:flutter/material.dart';
    
    /// A helper widget that builds different layouts based on parent constraints.
    class ResponsiveLayout extends StatelessWidget {
      final Widget mobileBody;
      final Widget tabletBody;
      final Widget desktopBody;
    
      const ResponsiveLayout({
        super.key,
        required this.mobileBody,
        required this.tabletBody,
        required this.desktopBody,
      });
    
      @override
      Widget build(BuildContext context) {
        // We use LayoutBuilder to inspect the maximum width available from the parent.
        return LayoutBuilder(
          builder: (context, constraints) {
            if (constraints.maxWidth >= 1200) {
              return desktopBody;
            } else if (constraints.maxWidth >= 600) {
              return tabletBody;
            } else {
              return mobileBody;
            }
          },
        );
      }
    }
    
    /// A responsive page showing LayoutBuilder and grid layouts.
    class ResponsiveCatalogPage extends StatelessWidget {
      const ResponsiveCatalogPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Responsive Catalog'),
            centerTitle: true,
          ),
          body: ResponsiveLayout(
            // Mobile Layout: Single column list view
            mobileBody: ListView.builder(
              padding: const EdgeInsets.all(16.0),
              itemCount: 8,
              itemBuilder: (context, index) => _buildItemCard(index),
            ),
            
            // Tablet Layout: Two-column grid view
            tabletBody: GridView.builder(
              padding: const EdgeInsets.all(16.0),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 16.0,
                mainAxisSpacing: 16.0,
                childAspectRatio: 1.5,
              ),
              itemCount: 8,
              itemBuilder: (context, index) => _buildItemCard(index),
            ),
            
            // Desktop Layout: Three-column grid view with padding
            desktopBody: GridView.builder(
              padding: const EdgeInsets.symmetric(horizontal: 48.0, vertical: 24.0),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                crossAxisSpacing: 24.0,
                mainAxisSpacing: 24.0,
                childAspectRatio: 1.8,
              ),
              itemCount: 8,
              itemBuilder: (context, index) => _buildItemCard(index),
            ),
          ),
        );
      }
    
      Widget _buildItemCard(int index) {
        return Card(
          elevation: 2,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Catalog Item #${index + 1}',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 6.0),
                    Text(
                      'Responsive cards adapt to fit your display size.',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey[600],
                      ),
                    ),
                  ],
                ),
                TextButton(
                  onPressed: () {},
                  child: const Text('Learn More'),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Code Explanation:

    1. LayoutBuilder: Inspects the parent's available constraints rather than the global screen size.
    2. SliverGridDelegateWithFixedCrossAxisCount: Dynamically adjusts the number of columns (crossAxisCount) and spacing based on the device width.
    3. Breakpoints Logic: Automatically switches layouts at the 600px and 1200px thresholds.

    Common Mistakes

    1. Hardcoding Screen Sizes

    Using hardcoded screen size numbers (e.g. setting widget width directly to 375 pixels for a mobile layout) causes layout breaking issues when run on different devices.

    • The Fix: Use percentage ratios, flex boxes, or wrap elements in an Expanded widget to let them size dynamically.

    2. Confusing MediaQuery with LayoutBuilder

    Using MediaQuery to size child cards inside parent containers (like a split view column) is a common mistake. Since MediaQuery returns the global device size, it won't reflect the actual width available to child widgets inside nested containers.

    • The Fix: Use LayoutBuilder to inspect the actual constraints of the parent widget rather than global screen dimensions.

    3. Unconstrained Layout Crashes inside GridViews

    Nesting grid builders inside unbounded vertical parents (like vertical scroll views) will throw "Vertical viewport was given unbounded height" exceptions.

    • The Fix: Set shrinkWrap: true and physics: const NeverScrollableScrollPhysics() on the grid, or wrap it in an Expanded widget inside a Column container.

    Best Practices

    • Define Consistent Breakpoints: Maintain a unified breakpoints configuration class (Breakpoints.mobile = 600, etc.) to keep layout rules consistent across the application.
    • Design Mobile-First: Build layout structures for mobile screens first, then expand and organize layout sections as screen real estate increases.
    • Test on Different Screen Sizes: Test layouts on multiple simulator configurations and resize browser windows during development to verify responsive behaviors.

    Related Concepts

    Continue mastering layout mechanics by exploring these guides:


    Summary

    • Responsive designs restructures layout sections based on the available width of the screen or container.
    • MediaQuery provides global screen metrics, while LayoutBuilder provides the layout constraints of the parent widget.
    • Standard breakpoints divide viewports into Mobile (under 600px), Tablet (600px - 1200px), and Desktop (over 1200px).
    • Always use flex layouts (Expanded, Flexible) to let child widgets resize dynamically.

    Next Steps

    1. Previous Guide: Stack and Positioned Widgets
    2. Next Guide: Managing State with Provider
    3. Related Guide: Row vs. Column Layout Guide

    Ready to take the next step?

    Start our free Flutter course and build real apps today.

    Start Learning Now