Flutter Layouts & Responsive Design

MediaQuery & Screen Size

50 min Lesson 10 of 16

What Is MediaQuery?

MediaQuery provides information about the current device’s screen size, orientation, pixel density, accessibility settings, and more. It is one of the most fundamental tools for building responsive Flutter applications that adapt to different devices.

Key Concept: MediaQuery is an InheritedWidget that sits near the top of the widget tree (placed by MaterialApp / WidgetsApp). Any widget in the tree can access it to read device metrics and respond accordingly.

MediaQuery.of(context)

The traditional way to access media query data is through MediaQuery.of(context), which returns a MediaQueryData object containing all available information about the device and display.

Basic MediaQuery Usage

class ResponsiveWidget extends StatelessWidget {
  const ResponsiveWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);

    return Column(
      children: [
        Text('Screen width: \${mediaQuery.size.width}'),
        Text('Screen height: \${mediaQuery.size.height}'),
        Text('Orientation: \${mediaQuery.orientation}'),
        Text('Pixel ratio: \${mediaQuery.devicePixelRatio}'),
        Text('Text scale: \${mediaQuery.textScaler}'),
        Text('Brightness: \${mediaQuery.platformBrightness}'),
      ],
    );
  }
}

MediaQueryData Properties

The MediaQueryData object provides a wealth of information. Here are the most commonly used properties:

Size

The size property gives you the screen’s logical dimensions in device-independent pixels (dp). This is not the physical pixel count—it accounts for the device pixel ratio.

Working with Screen Size

Widget build(BuildContext context) {
  final size = MediaQuery.sizeOf(context);
  final width = size.width;
  final height = size.height;

  // Responsive column count
  int columns;
  if (width < 600) {
    columns = 2;  // Mobile
  } else if (width < 900) {
    columns = 3;  // Tablet
  } else {
    columns = 4;  // Desktop
  }

  return GridView.builder(
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: columns,
      crossAxisSpacing: 16,
      mainAxisSpacing: 16,
    ),
    itemCount: 20,
    itemBuilder: (context, index) {
      return Card(
        child: Center(child: Text('Item \$index')),
      );
    },
  );
}

Padding & View Insets

Understanding the difference between padding, viewPadding, and viewInsets is essential for handling safe areas and the software keyboard:

  • padding — Areas obscured by system UI (status bar, notch, bottom bar). Reduces when the keyboard opens.
  • viewPadding — Same areas, but does NOT reduce when the keyboard opens. Represents the physical obstructions.
  • viewInsets — Space occupied by the software keyboard and other system interfaces that overlay the app.

Safe Area Handling

Widget build(BuildContext context) {
  final mediaQuery = MediaQuery.of(context);

  return Padding(
    padding: EdgeInsets.only(
      top: mediaQuery.padding.top,       // Status bar height
      bottom: mediaQuery.padding.bottom,  // Bottom safe area
      left: mediaQuery.padding.left,      // Left notch area
      right: mediaQuery.padding.right,    // Right notch area
    ),
    child: const Scaffold(
      body: Center(
        child: Text('Content safely inside all system UI'),
      ),
    ),
  );
}

// Or simply use SafeArea which does the same thing:
SafeArea(
  child: Scaffold(
    body: const Center(
      child: Text('SafeArea handles padding automatically'),
    ),
  ),
)

Keyboard Insets

When the software keyboard opens, viewInsets.bottom reflects the keyboard height. This is critical for forms and input screens.

Keyboard-Aware Layout

class KeyboardAwareForm extends StatelessWidget {
  const KeyboardAwareForm({super.key});

  @override
  Widget build(BuildContext context) {
    final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
    final isKeyboardVisible = bottomInset > 0;

    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            if (!isKeyboardVisible) ...[
              const SizedBox(height: 40),
              const Icon(Icons.lock, size: 80, color: Colors.blue),
              const SizedBox(height: 20),
              const Text(
                'Welcome Back',
                style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 40),
            ],
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 24),
              child: TextField(
                decoration: InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            const SizedBox(height: 16),
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 24),
              child: TextField(
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'Password',
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            const Spacer(),
            Padding(
              padding: EdgeInsets.only(
                left: 24,
                right: 24,
                bottom: bottomInset + 24,
              ),
              child: SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: () {},
                  child: const Text('Sign In'),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Orientation

The orientation property returns either Orientation.portrait or Orientation.landscape. You can also use OrientationBuilder for a more localized approach.

Orientation-Based Layout

Widget build(BuildContext context) {
  final orientation = MediaQuery.orientationOf(context);

  return orientation == Orientation.portrait
      ? Column(
          children: [
            _buildHeader(),
            Expanded(child: _buildContent()),
          ],
        )
      : Row(
          children: [
            SizedBox(
              width: 300,
              child: _buildHeader(),
            ),
            Expanded(child: _buildContent()),
          ],
        );
}

Device Pixel Ratio & Platform Brightness

The devicePixelRatio tells you how many physical pixels correspond to one logical pixel. platformBrightness indicates whether the system is in light or dark mode.

Pixel Ratio and Brightness

Widget build(BuildContext context) {
  final mediaQuery = MediaQuery.of(context);
  final pixelRatio = mediaQuery.devicePixelRatio;
  final isDarkMode =
      mediaQuery.platformBrightness == Brightness.dark;

  return Container(
    color: isDarkMode ? Colors.grey[900] : Colors.white,
    child: Column(
      children: [
        Text(
          'Pixel ratio: \$pixelRatio',
          style: TextStyle(
            color: isDarkMode ? Colors.white : Colors.black,
          ),
        ),
        // Use higher resolution images on high-DPI screens
        Image.asset(
          pixelRatio > 2
              ? 'assets/images/logo@3x.png'
              : 'assets/images/logo@2x.png',
        ),
      ],
    ),
  );
}

MediaQuery.sizeOf & Other Optimized Accessors

Flutter 3.10+ introduced more efficient ways to read specific MediaQuery properties. Using MediaQuery.sizeOf(context) instead of MediaQuery.of(context).size means your widget only rebuilds when the size changes, not when any media query property changes.

Optimized MediaQuery Accessors

// Instead of this (rebuilds on ANY media query change):
final size = MediaQuery.of(context).size;

// Use this (rebuilds ONLY when size changes):
final size = MediaQuery.sizeOf(context);

// Other optimized accessors:
final orientation = MediaQuery.orientationOf(context);
final padding = MediaQuery.paddingOf(context);
final viewInsets = MediaQuery.viewInsetsOf(context);
final viewPadding = MediaQuery.viewPaddingOf(context);
final brightness = MediaQuery.platformBrightnessOf(context);
final textScaler = MediaQuery.textScalerOf(context);
final highContrast = MediaQuery.highContrastOf(context);
Performance Tip: Always prefer the specific accessors (sizeOf, orientationOf, etc.) over the general MediaQuery.of(context). The general accessor causes your widget to rebuild whenever any media query property changes, which can lead to unnecessary rebuilds when the keyboard opens or the text scale changes.

Practical Example: Adaptive Padding

Adaptive Padding Based on Screen Width

class AdaptivePadding extends StatelessWidget {
  final Widget child;

  const AdaptivePadding({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;

    // Scale padding based on screen width
    final horizontalPadding = width < 600
        ? 16.0   // Mobile
        : width < 1200
            ? 32.0  // Tablet
            : (width - 1200) / 2 + 32; // Desktop: center content

    return Padding(
      padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
      child: child,
    );
  }
}

Practical Example: Orientation-Based UI

Complete Orientation-Aware Screen

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.sizeOf(context);
    final orientation = MediaQuery.orientationOf(context);
    final isLandscape = orientation == Orientation.landscape;

    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: SafeArea(
        child: isLandscape
            ? Row(
                children: [
                  SizedBox(
                    width: size.width * 0.35,
                    child: _buildAvatar(size: 100),
                  ),
                  Expanded(child: _buildDetails()),
                ],
              )
            : SingleChildScrollView(
                child: Column(
                  children: [
                    const SizedBox(height: 24),
                    _buildAvatar(size: 120),
                    const SizedBox(height: 24),
                    _buildDetails(),
                  ],
                ),
              ),
      ),
    );
  }

  Widget _buildAvatar({required double size}) {
    return Center(
      child: CircleAvatar(
        radius: size / 2,
        backgroundImage:
            const NetworkImage('https://example.com/avatar.jpg'),
      ),
    );
  }

  Widget _buildDetails() {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('John Doe',
              style: TextStyle(
                  fontSize: 24, fontWeight: FontWeight.bold)),
          SizedBox(height: 8),
          Text('Flutter Developer',
              style: TextStyle(fontSize: 16, color: Colors.grey)),
          SizedBox(height: 16),
          Text('Bio: Passionate about building beautiful apps.'),
        ],
      ),
    );
  }
}
Warning: Avoid using MediaQuery.of(context) inside the build method of widgets deep in the tree unless you truly need all properties. Each call creates a dependency on the entire MediaQueryData, causing unnecessary rebuilds. Use the specific accessors or consider using LayoutBuilder for parent-relative sizing instead of screen-relative sizing.
Summary: MediaQuery is your gateway to device information in Flutter. Use sizeOf for responsive layouts, viewInsetsOf for keyboard handling, paddingOf for safe areas, and orientationOf for orientation-aware designs. Always prefer the specific accessors over the general of(context) for better performance.