Flutter is a cross-platform application development framework that supports devices of all screen sizes. It can run on small devices like smartwatches as well as large ones like TVS. Using the same code to accommodate different screen sizes and pixel densities is a challenge.
There are no hard and fast rules for the design of a Flutter responsive layout. In this article, I’ll show you some of the approaches you can follow when designing a responsive layout.
Before building a responsive layout with Flutter, I want to explain how Android and iOS handle layouts with different screen sizes.
1. The Android approach
In order to handle different screen sizes and pixel densities, the following concepts are used in Android:
1.1 ConstraintLayout
One revolutionary thing introduced in Android UI design is ConstraintLayout. It can be used to create flexible, responsive UI designs to accommodate different screen sizes and sizes. It allows you to specify the location and size of each view based on its spatial relationship to other views in the layout.
But that doesn’t solve the problem of large devices, where stretching or just resizing UI components isn’t the most elegant way to use screen real estate. On a smartwatch with a small screen area, tweaking components to fit the screen size can lead to strange UI.
1.2 the Alternative layouts
To solve this problem, you can use alternative layouts for different sizes of devices. For example, you can use split view on devices like tablets to provide a good user experience and use large screens wisely.
In Android, you can define different layout files for different screen sizes, and the Android framework automatically handles switching between these layouts based on the screen size of the device.
1.3 Fragments could
With Fragments, you can extract your UI logic into separate components, so you don’t have to define the logic separately when designing multi-pane layouts for large screen sizes. You can reuse the fragments defined for each Fragment.
1.4 the Vector graphics
Instead of using pixel bitmaps, Vector Graphics creates images using XML to define paths and colors. It can scale to any size. In Android, you can use VectorDrawable to draw any type of illustration, such as ICONS.
2. The iOS approach
The way iOS defines responsive layouts is as follows
2.1 Auto Layout
Auto Layout can be used to build an adaptive interface where you can define rules (called constraints) that control the content of your application. When certain environmental changes (called features) are detected, Auto Layout automatically readjusts the Layout based on the constraints specified.
2.2 the Size classes
The Size class is automatically assigned to content areas based on their Size. IOS dynamically adjusts the layout based on the Size category of the content area. On the iPad, the size class also works.
2.3 Some UI Components
There are other UI components that you can use to build responsive UIs on iOS, like UIStackView, UIViewController, and UISplitViewController.
3. How does Flutter adapt
Even if you’re not an Android or iOS developer, by now you should have a good idea of how these platforms handle responsive layouts.
In Android, to display multiple UI views on a single screen, use Fragments that are similar to reusable components that can be run in an application’s Activity.
You can run multiple Fragments in an Activity, but you cannot run multiple activities simultaneously in an application.
In iOS, to control multiple view controllers, a UISplitViewController is used, which manages child view controllers in a hierarchical interface.
Now we come to Flutter
Flutter introduces the concept of widgets. They fit together like building blocks to build an application picture.
Remember that in Flutter, each screen and the entire application is also a widget!
Widgets are reusable by nature, so you don’t need to learn any other concepts when building a responsive layout in Flutter.
3.1 The responsive concept of Flutter
As I said earlier, I’ll discuss the important concepts needed to develop a responsive layout, and then you can choose how to implement a responsive layout on your APP.
3.1.1 MediaQuery
You can use MediaQuery to retrieve the screen size (width/height) and orientation (portrait/landscape).
Here’s an example
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { Size screenSize = MediaQuery.of(context).size; Orientation orientation = MediaQuery.of(context).orientation; return Scaffold( body: Container( color: CustomColors.android, child: Center( child: Text( 'View\n\n' + '[MediaQuery width]: ${screenSize.width.toStringAsFixed(2)}\n\n' + '[MediaQuery orientation]: $orientation', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ); }}Copy the code
3.1.2 LayoutBuilder
Using the LayoutBuilder class, you can get a BoxConstraints object that can be used to determine the maxWidth and maxHeight of the widget.
Remember: The main difference between MediaQuery and LayoutBuilder is that MediaQuery uses the full context of the screen, not just the size of a particular widget. LayoutBuilder can determine the maximum width and height of a particular widget.
Here’s an example
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { Size screenSize = MediaQuery.of(context).size; return Scaffold( body: Row( children: [ Expanded( flex: 2, child: LayoutBuilder( builder: (context, constraints) => Container( color: CustomColors.android, child: Center( child: Text( 'View 1\n\n' + '[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' + '[LayoutBuilder]:\n${constraints.maxWidth.toStringAsFixed(2)}', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ), ), Expanded( flex: 3, child: LayoutBuilder( builder: (context, constraints) => Container( color: Colors.white, child: Center( child: Text( 'View 2\n\n' + '[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' + '[LayoutBuilder]:\n${constraints.maxWidth.toStringAsFixed(2)}', style: TextStyle(color: CustomColors.android, fontSize: (() (() [() [() [() [() [() }}Copy the code
PS: When you are building a widget and want to know what its width is, you can use this component to build different layouts based on the height/width available to the child components
3.1.3 OrientationBuilder
To determine the current orientation of the widget, use the OrientationBuilder class.
Remember: This is a different direction from the device you use for MediaQuery retrieval.
Here’s an example
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { Orientation deviceOrientation = MediaQuery.of(context).orientation; return Scaffold( body: Column( children: [ Expanded( flex: 2, child: Container( color: CustomColors.android, child: OrientationBuilder( builder: (context, orientation) => Center( child: Text( 'View 1\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ), ), Expanded( flex: 3, child: OrientationBuilder( builder: (context, orientation) => Container( color: Colors.white, child: Center( child: Text( 'View 2\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: CustomColors.android, fontSize: 18), ), ), ), ), ), ], ), ); }}Copy the code
Portrait (landscape)
PS: READ the source code notes of OrientationBuilder
The orientation of a widget is simply a coefficient of its width relative to its height. If a [Column] widget is wider than its height, its orientation is horizontal, even if it displays its children in vertical form.
This is the translator’s code
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; /// Copyright (C), 2020-2020, flutter_demo /// FileName: orientationBuilder_demo /// Author: Jack /// Date: 2020/12/6 /// Description: class OrientationBuilderDemo extends StatelessWidget { @override Widget build(BuildContext context) { Orientation deviceOrientation = MediaQuery.of(context).orientation; return Scaffold( body: Column( children: [ Expanded( flex: 1, child: Container( color: Colors.greenAccent, child: OrientationBuilder( builder: (context, orientation) => Center( child: Text( 'View 1\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ), ), Expanded( flex: 2, child: OrientationBuilder( builder: (context, orientation) => Container( color: Colors.white, child: Center( child: Text( 'View 2\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: Colors.greenAccent, fontSize: 18), ), ), ), ), ), ], ), ); }}Copy the code
If a widget is wider than it is tall, it is horizontal. If it is taller than it is wide, it is horizontal. That’s all.
3.1.4 Expanded and Flexible
Widgets that are particularly useful in a Row or Column are Expanded and Flexible. When Expanded is used in a Row, Column, or Flex, Expanded can have its child widgets automatically fill the available space, as opposed to Flexible’s child widgets that do not fill the entire available space.
Here’s an example.
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( children: [ Row( children: [ ExpandedWidget(), FlexibleWidget(), ], ), Row( children: [ ExpandedWidget(), ExpandedWidget(), ], ), Row( children: [ FlexibleWidget(), FlexibleWidget(), ], ), Row( children: [ FlexibleWidget(), ExpandedWidget(), ], ), ], ), ), ); } } class ExpandedWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( child: Container( decoration: BoxDecoration( color: CustomColors.android, border: Border.all(color: Color.white),), child: Padding(Padding: const EdgeInsets. All (16.0), child: Text('Expanded', style: TextStyle(color: color) Colors.white, fontSize: 24), ), ), ), ); } } class FlexibleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Flexible( child: Container( decoration: BoxDecoration( color: CustomColors.androidAccent, border: Border.all(color: Color.white),), child: Padding(Padding: const EdgeInsets. All (16.0), child: Text('Flexible', style: TextStyle(color: color) CustomColors.android, fontSize: 24), ), ), ), ); }}Copy the code
PS: Unlike [expand], [Flexible] does not require child widgets to fill the remaining space. In the first example, expanded has the ability to fill free space, but flex is 1 for both expanded and Flexible components, which is equivalent to splitting the vertical axis in half. All space owned by Expanded is half of the vertical axis, which is already filled.
3.1.5 FractionallySizedBox
The FractionallySizedBox widget resizes its child elements to a fraction of the available space. It is especially useful in Expanded or Flexible widgets.
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ FractionallySizedWidget(widthFactor: 0.4)],), Row (crossAxisAlignment: crossAxisAlignment. Start, children: [FractionallySizedWidget (widthFactor: 0.6)],), Row (crossAxisAlignment: crossAxisAlignment. Start, children: [FractionallySizedWidget (widthFactor: 0.8)],), Row (crossAxisAlignment: crossAxisAlignment. Start, children: [FractionallySizedWidget (widthFactor: 1), [,], [,),),); } } class FractionallySizedWidget extends StatelessWidget { final double widthFactor; FractionallySizedWidget({@required this.widthFactor}); @override Widget build(BuildContext context) { return Expanded( child: FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: widthFactor, child: Container( decoration: BoxDecoration( color: CustomColors.android, border: Border.all(color: Colors.white), ), child: Padding( padding: Const EdgeInsets. All (16.0), child: Text('${widthFactor * 100}%', style: TextStyle(color: Colors. White, fontSize: 24), ((), ((), ((); }}Copy the code
PS: Use this component when you want your widget to take up a percentage of the current screen width and height. Wrap expanded or Flexible around the FractionallySizedBox when you want to use percentage layouts in Row and Column components
3.1.6 AspectRatio
You can use the AspectRatio widget to resize child elements to a specific AspectRatio. First, it tries the maximum width allowed by the layout constraint and determines the height by applying the given aspect ratio to the width.
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( children: [ AspectRatioWidget(ratio: '16 / 9'), AspectRatioWidget(ratio: '3 / 2'), ], ), ), ); } } class AspectRatioWidget extends StatelessWidget { final String ratio; AspectRatioWidget({@required this.ratio}); @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: Fraction.fromString(ratio).toDouble(), child: Container( decoration: BoxDecoration( color: CustomColors.android, border: Border.all(color: Colors.white), ), child: Padding( padding: Const EdgeInsets. All (16.0), child: Center(child: Text('AspectRatio - $ratio', style: TextStyle(color: Colors.white, fontSize: 24), ), ), ), ), ); }}Copy the code
We have studied most of the important concepts for building a responsive layout of Flutter app, except the last one.
Let’s learn one last concept as we build a sample reactive application.
3.2 Creating a responsive APP
Now, we’ll apply some of the concepts described in the previous section. At the same time, you’ll also learn another important concept for building layouts for large screens, namely split view (multiple pages on one screen).
Responsive layout: Use different layouts for different screen sizes. We will build a chat application called Flow.
App mainly consists of two parts:
-
HomePage (
PeopleView
,BookmarkView
,ContactView
) -
ChatPage (
PeopleView
.ChatView
)
For the large screen, we will display a split view containing the MenuWidget and DestinationView. As you can see, it is very easy to create split views in Flutter by placing them side by side with one row, and then wrapping the two views with Expanded Widgets to fill the entire space. You can also define flex properties for extension widgets, which will allow you to specify how much of the screen each widget should cover (the default Flex setting is 1).
However, if you now move to a particular screen and then switch between views, you will lose the context of the page, meaning you will always be returned to the first page, “Chat.” To solve this problem, I used multiple callback functions to return the selected page to the home page. Instead, you should use state management techniques to handle this scenario. Since the sole purpose of this article is to teach you how to build responsive layouts, I won’t discuss any of the complexities of state management.