DEV Community

Cover image for Building Responsive UIs in Flutter: A Short Guide
Dario Digregorio
Dario Digregorio

Posted on

Building Responsive UIs in Flutter: A Short Guide

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.

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

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

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.

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 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 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

1. Counters Page, 2. Counter Detail Page, 3. Master Detail for bigger Screens

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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 to easily switch layouts. Here’s how our main widget shapes up:

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
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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(),
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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<br>

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 for more insights.

Keen to see these methods in action? Check out my app, Yawa: Weather & Radar, on Android and iOS. And if you have any questions or feedback, feel free to reach out on Twitter!

Top comments (2)

Collapse
 
callmephil profile image
Phil

What would be your recommendation for adapting the font size, padding, etc?

Collapse
 
dariodigregorio profile image
Dario Digregorio

This is handled mostly by the OS's accessibility features. You just need to scale your UI and Text and see that your app layout does not break. Btw I released a new more sophisticated guide which you can check out. :)