The Best Flutter Responsive UI Pattern

Dane Mackier
Oct 14, 2019 · 6 min read
Image for post
Image for post

Today I want to go over my solution to provide Sizing information at a base widget level so it can easily be incorporated into any architecture. This tutorial will not be to make a UI appear exactly the same on every screen size. It will actually be the opposite. I would like to provide myself with enough information to intelligently modify my UI’s appearance based on screen sizing. I want to do this at an view (screen) level as well as a widget level.

The Problem

The question that we’re answering today is “How can we provide all relevant information at a widget level to modify our UI?”.

The Solution

Provide a BaseWidget that gives you the opportunity to build your UI using custom ScreenInformation compiled by you specifically for that goal. Instead of making use of the the MediaQuery in every widget and view file we will build it into the BaseWidget and build SizingInformation object that will be available to every widget using the BaseWidget.

Let's go over what we’d like to provide in the SizingInformation and also why we would want this information:

Implementation

Watch the Full video tutorial here.

To implement the functionality we will make use of two Flutter provided sets of Functionality.

  • MediaQuery: This provides us with some valuable information, most importantly the Orientation and ScreenSize. We'll use this to determine the first 3 info points mentioned above.
  • LayoutBuilder: This widget provides us with a builder that also gives us BoxConstraints which indicate to use the Bounds the current widget is taking up. We'll use the bounds from the Builder to create our LocalWidgetSize value and pass that back.

With that in mind lets start the implementation.

SizingInformation

Before we can create the SizingInformation we need an enum that represents our DeviceScreenType. Under the lib folder create a new folder called enums, inside create a new file, device_screen_type.dart (naming can definitely change).

enum DeviceScreenType {
Mobile,
Tablet,
Desktop
}

Then create a new folder under lib called ui and inside create a new file called sizing_information.dart

import 'package:flutter/widgets.dart';
import 'package:response_architecture/enums/device_screen_type.dart';
class SizingInformation {
final Orientation orientation;
final DeviceScreenType deviceType;
final Size screenSize;
final Size localWidgetSize;
SizingInformation({
this.orientation,
this.deviceType,
this.screenSize,
this.localWidgetSize,
});
@override
String toString() {
return 'Orientation:$orientation DeviceType:$deviceType ScreenSize:$screenSize LocalWidgetSize:$localWidgetSize';
}
}

Now that the model is done lets setup the builder that we’ll use to build our UI for us.

UI Architecture

We’ll start by creating a StatelessWidget under the UI folder called BaseWidget. It will take in a Function that returns a widget, that function will be named builder. The parameters to the function will be the BuildContext and the SizingInformation. For the build method, we will execute that builder and return the widget it produces.

import 'package:flutter/material.dart';
import 'package:response_architecture/ui/sizing_information.dart';
class BaseWidget extends StatelessWidget {
final Widget Function(
BuildContext context, SizingInformation sizingInformation) builder;
const BaseWidget({Key key, this.builder}) : super(key: key);
@override
Widget build(BuildContext context) {
var sizingInformation = SizingInformation();
return builder(context, sizingInformation);
}
}

Then we can set up a simple home_view to see how we’ll make use of this. Under the UI folder create a new file called home_view.dart

import 'package:flutter/material.dart';
import 'package:response_architecture/ui/base_widget.dart';
class HomeView extends StatelessWidget {
const HomeView({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BaseWidget(builder: (context, sizingInformation) {
return Scaffold(
body: Center(
child: Text(sizingInformation.toString()),
));
});
}
}

Before we test we’ll set up a package that helps us to easily test our layouts. This will be used in development for quick UI testing only, the majority of your testing should still be on the real devices, if not possible the emulator or the Web to easily test all sizes. We’ll add the Device Preview package to the pubspec.

device_preview: 0.1.9-beta

Then in the main file we’ll follow the instructions, wrap the App in Device preview and supply it with the appBuilder from the package. Make sure your home file looks like below.

void main() => runApp(
DevicePreview(
child: MyApp(),
),
);
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: DevicePreview.appBuilder,
title: 'Flutter Demo',
home: HomeView(),
);
}
}

You should be seeing a preview like below if you’re using an Android emulator. I’d recommend spinning up a Tablet sized AVD but a basic emulator works fine for me when it’s sized to be as big as possible.

Image for post
Image for post

If you click on the top left corner on the cog icon you can rotate the device, change the device and see how the UI responds in milliseconds. Now lets move on to gathering the information and populating the SizingInformation.

Populating the Information

We’ll start off by passing in our Orientation.

var mediaQuery = MediaQuery.of(context);
var sizingInformation = SizingInformation(
orientation: mediaQuery.orientation,
);

Then we’ll determine the DeviceType with a top-level function stored in the ui_utils.dart file under the utils folder. The way we determine the device we’re on is through the width of the device. Since the mediaQuery.size doesn’t return the size of the physical device, we have to make sure we’re using the correct dimensions. We determine the screen type by the width of the device. Since the width of the screen will reflect differently based on the orientation we will use either the width or height depending on the orientation. When the device is in portrait we can use the width to determine the width of the device. When it’s in landscape we have to use the screen height to determine the width of the device.

DeviceScreenType getDeviceType(MediaQueryData mediaQuery) {
var orientation = mediaQuery.orientation;
double deviceWidth = 0; if (orientation == Orientation.landscape) {
deviceWidth = mediaQuery.size.height;
} else {
deviceWidth = mediaQuery.size.width;
}
if (deviceWidth > 950) {
return DeviceScreenType.Desktop;
}
if (deviceWidth > 600) {
return DeviceScreenType.Tablet;
}
return DeviceScreenType.Mobile;
}

In the BaseWidget we can now get the devicesScreenType from using this function and passing in the MediaQuery. We can also pass the screen size.

var mediaQuery = MediaQuery.of(context);
var sizingInformation = SizingInformation(
orientation: mediaQuery.orientation,
deviceType: getDeviceType(mediaQuery),
screenSize: mediaQuery.size,
);

If you reload your code now and change the sizing to an ipad Air2 you’ll see DeviceType.Tablet on the screen, the correct orientation etc. The only thing left is the LocalWidgetSize. And for that, we'll require the LayoutBuilder mentioned in the beginning.

Instead of executing the builder function and returning that we’ll return the LayoutBuilder as the root widget and for it's builder we'll execute and return the function passed in along with the BoxConstraints converted into a Size object.

Widget build(BuildContext context) {
var mediaQuery = MediaQuery.of(context);
return LayoutBuilder(builder: (context, boxSizing) {
var sizingInformation = SizingInformation(
orientation: mediaQuery.orientation,
deviceType: getDeviceType(mediaQuery),
screenSize: mediaQuery.size,
localWidgetSize: Size(boxSizing.maxWidth, boxSizing.maxHeight),
);
return builder(context, sizingInformation);
});
}

If you reload the code now you’ll see the correct sizing information required. If you want to see how we build a responsive UI using this setup subscribe to the FilledStacks YouTube channel, to see how I build a responsive UI.

Flutter Community

Articles and Stories from the Flutter Community

Dane Mackier

Written by

A full stack software developer focused on building mobile products, its tools and architecture. Always reducing boiler plate code and experimenting.

Flutter Community

Articles and Stories from the Flutter Community

Dane Mackier

Written by

A full stack software developer focused on building mobile products, its tools and architecture. Always reducing boiler plate code and experimenting.

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store