← Back to posts Cover image for Responsive Flutter Layouts: Mastering MediaQuery, LayoutBuilder, and Flex for All Screen Sizes

Responsive Flutter Layouts: Mastering MediaQuery, LayoutBuilder, and Flex for All Screen Sizes

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Building Flutter UIs That Flex: Beyond Basic MediaQuery

We’ve all been there. You build a beautiful app on your preferred device, only to find it looks broken on a tablet, awkward on a desktop, or cramped on a small phone. The classic solution is to reach for MediaQuery.of(context).size.width and sprinkle conditional logic everywhere. But this often leads to a codebase littered with magic number breakpoints, widgets that rebuild unnecessarily, and font sizes that don’t scale appropriately.

The core issue is treating screen size as a one-dimensional problem. True responsiveness isn’t just about width; it’s about understanding the available space a widget can use and making intelligent layout decisions accordingly.

Let’s move beyond the boilerplate and explore three powerful tools that, when used correctly, create robust, adaptive layouts.

1. MediaQuery: For Global Context, Not Local Layouts

MediaQuery is your window into the device’s characteristics: screen size, padding, text scaling, and more. The common pitfall is overusing it for direct layout decisions inside every widget.

A better practice is to use it for high-level, global constraints or when you need information about the device, not just available space. A key improvement is using MediaQuery.sizeOf(context), which is more readable and null-safe than the older .of(context).size pattern.

Use it for:

  • Setting app-wide padding (like respecting the system status bar).
  • Adjusting to platform-specific insets (notch, bottom safe area).
  • Reacting to system text scale factor.
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Good use: Getting safe areas for a scaffold.
    final padding = MediaQuery.viewPaddingOf(context);
    final screenSize = MediaQuery.sizeOf(context);

    return Scaffold(
      appBar: AppBar(title: const Text('Responsive App')),
      body: SafeArea(
        // Use padding to ensure content is not hidden.
        minimum: EdgeInsets.only(top: padding.top),
        child: MyAdaptiveContent(),
      ),
    );
  }
}

2. LayoutBuilder: The Gold Standard for Local Adaptability

This is your most important tool. LayoutBuilder tells a widget exactly how much space its parent has allocated for it right now. This is fundamentally different from the screen size. A widget might be in a sidebar, a dialog, or a column, and LayoutBuilder respects that.

Use it for:

  • Changing a widget’s internal layout based on the space it’s given.
  • Creating components that adapt to their container, not the screen.
class MyAdaptiveContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // constraints.maxWidth is the width available TO THIS WIDGET.
        if (constraints.maxWidth > 600) {
          // Wide layout: place items side-by-side.
          return Row(
            children: [
              Expanded(child: ProductDetailsPanel()),
              Expanded(child: ShoppingCartPanel()),
            ],
          );
        } else {
          // Narrow layout: stack items vertically.
          return Column(
            children: [
              ProductDetailsPanel(),
              ShoppingCartPanel(),
            ],
          );
        }
      },
    );
  }
}

This widget is now self-contained and reusable anywhere. It will adapt whether it’s placed full-screen on a phone or inside a panel on a desktop app.

3. Flex, Expanded, and Flexible: Let the Framework Do the Math

When you have a row or column where children should share space proportionally, don’t manually calculate widths. Use the Flex family (Row and Column are Flex widgets) with Expanded and Flexible.

  • Expanded: Makes a child fill the available space in the main axis. Use flex property to assign proportions (default is 1).
  • Flexible: Similar but allows the child to be smaller than the allocated space (using fit: FlexFit.loose).

Use it for:

  • Creating fluid, proportion-based layouts (e.g., a 70/30 split).
  • Ensuring widgets fill remaining space without overflow errors.
Widget build(BuildContext context) {
  return Row(
    children: [
      // This panel will take 2 portions of the space.
      Expanded(
        flex: 2,
        child: MainArticleView(),
      ),
      // This panel will take 1 portion.
      Expanded(
        flex: 1,
        child: RelatedArticlesSidebar(),
      ),
    ],
  );
}

Putting It All Together: A Responsive Strategy

Here’s a practical approach:

  1. Start with LayoutBuilder at the top of your UI sections. Decide on major layout changes (like from column to row) based on the available constraints.
  2. Use Flex widgets internally to manage space distribution within those layouts. Avoid hard-coded widths when you can use Expanded.
  3. Reserve MediaQuery for truly global concerns like safe areas, orientation, or platform-specific adjustments. Consider using it once high up in your widget tree and passing relevant values down.

Common Mistake to Avoid: Responsive Text Sizes

Don’t scale text directly with MediaQuery.sizeOf(context).width. A headline that looks good on a tablet will be gigantic on a desktop window. Instead, use MediaQuery.textScaleFactorOf(context) to respect the user’s system accessibility settings, and define a sensible set of TextStyle themes in your ThemeData that work across breakpoints. For more granular control, consider a package like auto_size_text or calculate size based on the local constraints from a LayoutBuilder.

By thinking in terms of available space (LayoutBuilder) and proportional distribution (Flex) rather than just screen pixels (MediaQuery), you build components that are inherently adaptable. This leads to cleaner code and UIs that feel at home on any device.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.

Cover image for Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.

Cover image for Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.