Skip to content
ExponaBLOG
FLUTTER · DAY 017 / 100

LayoutBuilder and MediaQuery.sizeOf: a step-by-step guide

Know how much space you actually have.

Expona Admin8 min read

LayoutBuilder and MediaQuery.sizeOf: Know How Much Space You Actually Have

One of the hardest parts of building responsive Flutter apps is knowing exactly how much screen real estate you have to work with. I've spent countless hours debugging layouts that worked on my phone but broke on tablets, only to realize I was making assumptions about available space rather than asking the widget tree. LayoutBuilder and MediaQuery.sizeOf are your two most powerful tools for solving this problem, and understanding when to use each one will save you from brittle, hard-coded layouts.

Why Space Constraints Matter in Flutter

When you're building a widget, you rarely exist in isolation. You're nested inside rows, columns, scrollviews, and other containers, each one imposing constraints on how much space you can actually claim. Hard-coding a widget to be 400 pixels wide might work on your development device, but it'll overflow on smaller phones or look tiny on tablets. The real challenge is that your widget doesn't inherently know what constraints it will receive until the layout phase runs.

This is where LayoutBuilder comes in. It gives you access to the actual constraints imposed by your parent widget, letting you make intelligent decisions about how to render your content.

LayoutBuilder: Getting Parent Constraints

LayoutBuilder is a widget that rebuilds whenever its constraints change. Its builder function receives a BoxConstraints object that tells you the minimum and maximum width and height your parent allows. This is the ground truth about available space.

dart
LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth > 700) {
      return const TwoPaneLayout();
    } else {
      return const SinglePaneLayout();
    }
  },
)

In this example, I'm checking if I have more than 700 pixels of width. If I do, I render a side-by-side two-pane design. If not, I stack the content vertically. The beauty is that this logic responds automatically to any size change, whether it's a device rotation, a window resize (on desktop), or being placed inside a different parent container.

Let me show you a more realistic scenario from my work on a SaaS dashboard. We had a data table that needed to be responsive:

dart
class DashboardTable extends StatelessWidget {
  const DashboardTable({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isMobile = constraints.maxWidth < 600;
        final isTablet = constraints.maxWidth < 1000;

        if (isMobile) {
          return MobileTableView(constraints: constraints);
        } else if (isTablet) {
          return TabletTableView(constraints: constraints);
        } else {
          return DesktopTableView(constraints: constraints);
        }
      },
    );
  }
}

Notice that I'm passing the constraints to the child widgets. This is crucial because LayoutBuilder itself becomes a constraint source for its children. By passing constraints along, I let my lower-level widgets make their own layout decisions based on what they actually received.

MediaQuery.sizeOf: Reading Screen Size Efficiently

While LayoutBuilder gives you parent constraints, sometimes you need to know the absolute screen dimensions. That's where MediaQuery comes in. The MediaQuery widget sits at the top of your widget tree (inserted by MaterialApp or CupertinoApp) and provides global information about the device and current media environment.

dart
final screenSize = MediaQuery.sizeOf(context);
final screenWidth = screenSize.width;
final screenHeight = screenSize.height;

I use MediaQuery.sizeOf(context) specifically because it's the most efficient way to read screen dimensions in modern Flutter. It was added as a convenience method that avoids the common mistake of calling MediaQuery.of(context).size, which can be less clear about intent.

Here's a real example from a travel app I built. We needed to show different numbers of flight cards per row depending on screen width:

dart
class FlightGrid extends StatelessWidget {
  const FlightGrid({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.sizeOf(context).width;
    
    int crossAxisCount;
    if (screenWidth > 1200) {
      crossAxisCount = 4;
    } else if (screenWidth > 800) {
      crossAxisCount = 3;
    } else if (screenWidth > 400) {
      crossAxisCount = 2;
    } else {
      crossAxisCount = 1;
    }

    return GridView.count(
      crossAxisCount: crossAxisCount,
      children: flights.map((flight) => FlightCard(flight)).toList(),
    );
  }
}

This works, but I want to be honest: I've moved away from this pattern in my recent projects. The issue is that this widget is now tightly coupled to screen dimensions. If I want to reuse this grid inside a constrained container (like a sidebar panel), it will ignore that constraint and still try to use the full screen width. That's why I prefer LayoutBuilder for reusable, composable widgets.

Prefer Constraints for Reusable Widgets

Here's the core principle I've learned through building multiple production systems: use LayoutBuilder (constraints-based sizing) for widgets you want to reuse in different contexts. Use MediaQuery (screen-based sizing) only when you genuinely need global screen information.

A reusable widget should adapt to wherever it's placed, not fight against its parent. Let me show you the difference:

Constraint-aware (reusable):

dart
class AdaptiveCard extends StatelessWidget {
  const AdaptiveCard({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Card(
          child: Padding(
            padding: EdgeInsets.all(constraints.maxWidth > 500 ? 24 : 12),
            child: Column(
              children: [
                Text('Title', style: constraints.maxWidth > 500 
                  ? Theme.of(context).textTheme.headlineSmall
                  : Theme.of(context).textTheme.bodyLarge),
                SizedBox(height: constraints.maxWidth > 500 ? 16 : 8),
                Text('Content'),
              ],
            ),
          ),
        );
      },
    );
  }
}

Screen-aware (less reusable):

dart
class ScreenAwareCard extends StatelessWidget {
  const ScreenAwareCard({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final isWideScreen = MediaQuery.sizeOf(context).width > 800;
    
    return Card(
      child: Padding(
        padding: EdgeInsets.all(isWideScreen ? 24 : 12),
        child: Column(
          children: [
            Text('Title', style: isWideScreen
              ? Theme.of(context).textTheme.headlineSmall
              : Theme.of(context).textTheme.bodyLarge),
            SizedBox(height: isWideScreen ? 16 : 8),
            Text('Content'),
          ],
        ),
      ),
    );
  }
}

The first version (AdaptiveCard) will behave correctly whether it's displayed full-screen, inside a sidebar, or inside a dialog. The second version (ScreenAwareCard) assumes the screen is at least 800 pixels wide to be "wide," which breaks if you put it in a 400-pixel-wide sidebar on a tablet.

This matters because in my experience with e-commerce and SaaS platforms, you eventually want to reuse components across different parts of your app. A card that works in the main view should also work in a drawer, modal, or split-view panel.

Avoid Hard-Coded Pixel Sizes

I'll be direct: hard-coded pixel sizes are the source of most responsive design bugs I've had to fix. Statements like width: 400 or height: 200 are almost always wrong because they don't adapt to context.

Instead, think in terms of ratios and constraints:

dart
// Bad: assumes 400px is always right
Container(width: 400, child: MyWidget())

// Good: adapts to available space
LayoutBuilder(
  builder: (context, constraints) {
    return Container(
      width: constraints.maxWidth * 0.9, // 90% of available width
      child: MyWidget(),
    );
  },
)

Or use Flutter's built-in sizing tools:

dart
// Excellent: no hard-coded sizes
Container(
  width: double.infinity, // Take all available width
  child: MyWidget(),
)

I learned this lesson the hard way on a ride-hailing app. We had form fields with hard-coded widths of 300 pixels. On a tablet, they looked tiny. On a phone in landscape, they overflowed. The fix was LayoutBuilder, and it took maybe 10 minutes but would have saved hours of debugging if we'd done it right initially.

There are legitimate cases for fixed sizes—button padding, icon sizes, standard spacing units—but they should be the exception, not the rule. Everything that determines layout structure should be either percentage-based or constraint-aware.

Combining LayoutBuilder and MediaQuery

Sometimes you need both. For example, you might want to adapt to available space (LayoutBuilder) while also being aware of the device's safe area for notches:

dart
LayoutBuilder(
  builder: (context, constraints) {
    final screenHeight = MediaQuery.sizeOf(context).height;
    final isShortScreen = screenHeight < 600;
    
    return SingleChildScrollView(
      child: ConstrainedBox(
        constraints: BoxConstraints(
          minHeight: constraints.maxHeight,
        ),
        child: Column(
          children: [
            if (!isShortScreen) Text('Header'),
            Expanded(child: MainContent()),
          ],
        ),
      ),
    );
  },
)

Here, I'm using LayoutBuilder to know my container's height, and MediaQuery to make a global decision about screen size. This is legitimate because we're answering two different questions: "How much space does my parent give me?" and "What kind of device are we on?"

Common Mistakes

  • Nesting LayoutBuilder unnecessarily: If your parent is already layout-aware, don't wrap it again. You'll cause unnecessary rebuilds and make your widget tree harder to follow.
  • Using MediaQuery for layout decisions in reusable widgets: This breaks composability. Your widget suddenly works differently depending on where it's placed, not based on its actual space.
  • Forgetting to handle constraints.minWidth: Sometimes your parent sets a minimum width. Just checking maxWidth isn't enough; you need to ensure your content fits between min and max.
  • Building complex layouts entirely inside LayoutBuilder's builder: Move the build logic to separate methods or classes. A 200-line builder function becomes unmaintainable fast. I've done this, and it hurt.
  • Assuming MediaQuery values don't change: They do, especially during device rotation or when the app enters split-view on iPad. Always listen for changes with context.watch or MediaQuery.of(...).addListener.

Key Takeaways

  • LayoutBuilder gives you the actual constraints from your parent widget, enabling truly responsive layouts that work in any context.
  • MediaQuery.sizeOf efficiently reads global screen dimensions, but it's best reserved for app-level layout decisions, not for building reusable components.
  • Prefer constraint-based design (LayoutBuilder) over screen-size-based design (MediaQuery) when building widgets you want to reuse across different parts of your app.
  • Avoid hard-coded pixel sizes for structural layout. Use percentages, double.infinity, or constraints instead.
  • When both are needed, use LayoutBuilder for local space awareness and MediaQuery for global device information—but keep them separate logically.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles