Building Responsive UIs in Flutter: A Short Guide
Tags: flutter, dart, ui, design
Published: 2023-11-15 20:53:10


Flutter has become a go-to framework for developing cross-platform apps, but the real challenge lies in making these apps responsive across a variety of devices. This article is all about navigating the world of Flutter to ensure your app looks and works great, whether on mobile, desktop, or web.

**Side Note**: This guide sticks to the basics of Flutter for responsiveness, avoiding additional packages.

**The Tools I’m Using:** For this, I’m working with Dart 3.1.3, Flutter 3.13.9 and optional Riverpod 2.4.5.

# Setting Up and Key Insights

Testing your layouts on different devices and platforms is crucial. Choose a development environment that allows easy resizing. Each platform has its nuances, so let’s explore them:

## Understanding Platform and Device Differences
**Windows/MacOS/Web:** These are the easiest for development. They offer flexible window resizing and support HotReload. However, Flutter Web doesn’t support HotReload. Also, `MediaQuery.orientationOf()` here is based on window size, not device orientation.

**MacOS Í(Designed for iPad):** Optimize your app for both iOS and MacOS with the ‘My Mac (Designed for iPad)’ target in Xcode. This special build allows you to create an iOS-optimized app that functions seamlessly on MacOS. It behaves like a native MacOS app while maintaining compatibility with libraries used in the iOS version. This approach offers a unified development process for both platforms, ensuring that your app delivers a consistent experience across Apple devices. There is still some work to set it up properly. See this [issue](https://github.com/flutter/flutter/issues/125741).

**iPad with Stage Manager:** If your dependencies limit your platform choice, an iPad with Stage Manager is a good option. It allows resizing within predefined screen sizes.

![Resizable Experimental Emulator Menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/afphwm7d1hr648n3rilc.png)


**Android:** The Android Resizable Emulator, still in its experimental phase, lets you switch between phone, foldable, or tablet modes. It’s a bit buggy, but useful. Android’s split-screen feature is also worth exploring.

![Resizable Experimental Emulator Menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n1v0avg3cpp9wxuy50e6.png)

**iOS:** iOS is more restrictive, lacking a split-screen feature and offering limited screen sizes. The presence of the notch on modern iPhones adds to the responsiveness challenge, especially with the behavior of `SafeArea`. iOS adds an additional Padding to the other side of the Notch. Read more in this [issue](https://github.com/flutter/flutter/issues/28956#issuecomment-472188242).

## Mobile-First Approach
Starting your design with mobile in mind makes scaling up to larger screens smoother. This approach helps in efficiently adapting your designs for tablets or desktops. One popular pattern is using a [Master Detail interface](https://en.wikipedia.org/wiki/Master%E2%80%93detail_interface) to adapt your screens.

## Fonts
Pay attention to font sizes and language directions (LTR vs. RTL). These can mess up your layout so make sure to test your layouts properly.

## Using the Composite Pattern
The [Composite Pattern](https://en.wikipedia.org/wiki/Composite_pattern) is effective for organizing widgets, making it easier to reuse and adjust your code. While I’m using Riverpod for state management, you can choose any library that suits your project. This pattern also aids in adapting widgets inside your layouts to different screen sizes.

# Implementation: Bringing It All Together
Now, let’s dive into the practical side of things. We’ll be enhancing the classic Counter App to showcase responsive design in Flutter. The goal is to manage multiple counters and introduce a master-detail interface for larger screens.

REPOSITORY: [GitHub](https://github.com/dario-digregorio/flutter_responsive)

![1. Counters Page, 2. Counter Detail Page, 3. Master Detail for bigger Screens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqeaak4qbkem4sfaatsq.png)

## Breakpoints: The Foundation
First things first, we establish our breakpoints. These are key in determining how our app will flex and adapt to different screen sizes. Let’s take a look at our foundation:

``` dart
enum ScreenSize {
  small(300),
  normal(400),
  large(600),
  extraLarge(1000);

  final double size;

  const ScreenSize(this.size);
}
ScreenSize getScreenSize(BuildContext context) {
  double deviceWidth = MediaQuery.sizeOf(context).shortestSide; // Gives us the shortest side of the device
  if (deviceWidth > ScreenSize.extraLarge.size) return ScreenSize.extraLarge;
  if (deviceWidth > ScreenSize.large.size) return ScreenSize.large;
  if (deviceWidth > ScreenSize.normal.size) return ScreenSize.normal;
  return ScreenSize.small;
}
```

This setup uses enums to categorize screen sizes, making it easier for us to tailor the app’s layout to the device.

## Building the Responsive Layout
Now, onto the layout. We’re keeping things modular and adaptable, using [Dart Patterns](https://dart.dev/language/patterns) to easily switch layouts. Here’s how our main widget shapes up:

``` dart
class CounterAppContent extends StatelessWidget {
  const CounterAppContent({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final screenSize = getScreenSize(context);
    return Scaffold(
      bottomNavigationBar: switch (screenSize) {
        (ScreenSize.large) => null, // For larger screens, we might not need a bottom nav
        (_) => const CounterNavigationBar(), // The standard bottom nav
      },
      body: SafeArea(
        child: switch (screenSize) {
          (ScreenSize.large) => const Row( // A layout for larger screens
              children: [
                CounterNavigationRail(),
                VerticalDivider(thickness: 1, width: 1),
                Expanded(
                  child: CountersPage(isFullPage: false), // isFullPage is being used to define if the CounterTiles should navigate
                ),
                Expanded(
                  child: CounterDetailPage(),
                ),
              ],
            ),
          (_) => const CountersPage(),// The default layout for smaller screens
        },
      ),
    );
  }
}
```

Here, we’re using different navigation widgets based on the screen size — a `NavigationBar` for smaller screens and a `NavigationRail` for larger ones. Unfortunately it is a bit tricky using `NavigationRail` together with `NavigationBar` because `Scaffold` has only an input for a `bottomNavigationBar`. With the underscore we specify which layout should be shown on default.

## Adapting to Orientation Changes
What about when users flip their phones to landscape? We’ve got that covered too:

``` dart
class CounterAppContent extends StatelessWidget {
  const CounterAppContent({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final screenSize = getScreenSize(context);
    final orientation = MediaQuery.orientationOf(context);
    return Scaffold(
      bottomNavigationBar: switch ((screenSize, orientation)) {
        (_, Orientation.landscape) => null, // we will show NavigationRail when ever the app is being used in landscape
        (ScreenSize.large, _) => null,
        (_, _) => const CounterNavigationBar(),
      },
      body: SafeArea(
        child: switch ((screenSize, orientation)) {
          (ScreenSize.large, _) => const Row(
              children: [
                CounterNavigationRail(),
                VerticalDivider(thickness: 1, width: 1),
                Expanded(
                  child: CountersPage(isFullPage: false),
                ),
                Expanded(
                  child: CounterDetailPage(),
                ),
              ],
            ),
          (_, Orientation.landscape) => const Row( // the same here
              children: [
                CounterNavigationRail(),
                VerticalDivider(thickness: 1, width: 1),
                Expanded(
                  child: CountersPage(),
                )
              ],
            ),
          (_, _) => const CountersPage(),
        },
      ),
    );
  }
}
```

Using Dart’s Record feature, we can elegantly handle multiple conditions, adapting our layout to both screen size and orientation. You can react to more variables by adding them to the Record.

## Wrapping Up

![Counter App resizes to different Layouts
](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ry3asw1tz6wxozo86le0.png)

Our app now dynamically responds to every screen size. The beauty of Flutter is that it provides all the tools necessary for responsive design across any device. Don’t forget to check the source code on [GitHub](https://github.com/dario-digregorio/flutter_responsive) for more insights.

Keen to see these methods in action? Check out my app, Yawa: Weather & Radar, on [Android](https://play.google.com/store/apps/details?id=de.digregorio.dario.yawa) and [iOS](https://apps.apple.com/de/app/yawa-weather-forecast/id1626828365). And if you have any questions or feedback, feel free to reach out on [Twitter](https://twitter.com/DigregorioDario)!