Flutter responsive applicationsšŸ“±šŸ’».

Kaue Murakami
7 min readDec 24, 2023

--

Unpacking Responsiveness: Flutter and the Art of Crafting Apps that Fit Like a Glove on Mobile, Tablets and Web! šŸš€

Starting the project

Letā€™s create a new project, run the following command:
flutter create responsive_ui

Structure

With your project created, letā€™s organize the projectā€™s folder structure:

- lib
- app // Create the folders from now on.
- modules
- mobile
- mobile_scaffold.dart
- desktop
- desktop_scaffold.dart
- tablet
- tablet_scaffold.dart
- core/theme/ // At the same level as 'modules'
- colors.dart
// This is where our global widgets will be placed, if needed
// In addition to the example, you can add widgets here as well
// Specific to each module, you can have a 'widgets'
// folder for each module.
- widgets
- app_bar.dart
- drawer.dart
- my_box.dart
- my_tile.dart
- responsive_layout.dart

With everything organized, letā€™s start coding šŸ‘Øā€šŸ’»

In your main file main.dart delete the default initial content and insert this initial code.

void main() {
runApp(const MyApp());
}

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

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const ResponsiveLayout(
mobileScreen: MobileScaffold(),
tabletScreen: TabletScaffold(),
desktopScreen: DesktopScaffold(),
),
);
}
}

A default main.dart, donā€™t be surprised by these classes and the errors due to their absence; weā€™ll create them one by one. First, letā€™s understand our ResponsiveLayout(), which is responsible for checking the device weā€™re using and rendering the correct layout for each option. In our case, these options are app ā€” tablet ā€” desktop, and weā€™ll delve into them shortly.

ResponsiveLayout

Our RespondiveLayout is the component weā€™ll use in our main.dart; it implements a LayoutBuilder. It has the properties we need to distinguish the type of device weā€™re using based on the size provided by the constraints. We can achieve this using the attributes provided by our constraints:
ā€” maxWidth
ā€” minWidth
ā€” minHeight
ā€” maxHeight

Criando nosso ResponsiveLayout

In the global widgets folder, create a file named responsive_layout.dart. In it, weā€™ll implement the following code:

import 'package:flutter/material.dart';

class ResponsiveLayout extends StatelessWidget {
const ResponsiveLayout({
super.key,
required this.mobileScreen,
required this.tabletScreen,
required this.desktopScreen,
});
final Widget mobileScreen, tabletScreen, desktopScreen;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return mobileScreen;
} else if (constraints.minWidth < 1100) {
return tabletScreen;
} else {
return desktopScreen;
}
},
);
}
}

Here we can see the implementation of LayoutBuilder, the true responsible for distinguishing our layouts, and it works reactively. That is, as you increase or decrease your page, it will automatically bring the correct page defined by the specified values. You can see that we compare our constraints.maxWidth to check the width of the device and assign its respective screen. So we can define that a width less than 600 is a mobile, less than 1100 is a tablet, and beyond that, we define it as a desktop.

First, letā€™s add the values in your core/theme/colors.dart file:

import 'package:flutter/material.dart';

abstract class AppColors {
static final defaultBackground = Colors.grey[300];
}

Now we can move on to programming each of our pages.

Create Mobile Page

In our mobile module, in the mobile_scaffold.dart file, letā€™s insert the following code:

class MobileScaffold extends StatefulWidget {
const MobileScaffold({super.key});

@override
State<MobileScaffold> createState() => _MobileScaffoldState();
}

class _MobileScaffoldState extends State<MobileScaffold> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const MyAppBar(),
backgroundColor: AppColors.defaultBackground,
drawer: const MyDrawer(),
body: Column(
children: [
AspectRatio(
aspectRatio: 1.0,
child: SizedBox(
width: double.infinity,
child: GridView.builder(
itemCount: 4,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) => const MyBox(),
),
),
),
Expanded(
child: ListView.builder(
primary: true,
shrinkWrap: true,
itemCount: 5,
itemBuilder: (context, index) => const MyTile(),
),
),
],
),
);
}
}

Here we have a simple page with an AppBar,Drawer, a Grid, and a List.
Donā€™t be alarmed by the components that havenā€™t been mentioned yet; weā€™ll create them now.

Create AppBar

Go to your file widgets/app_bar.dart and fill it with this code:

import 'package:flutter/material.dart';
import 'dart:io' show Platform;

class MyAppBar extends StatelessWidget implements PreferredSizeWidget {
const MyAppBar({super.key});

@override
Widget build(BuildContext context) {
return AppBar(
leading: IconButton(
onPressed: () => Scaffold.of(context).openDrawer(),
icon: const Icon(
Icons.menu,
color: Colors.white,
),
),
backgroundColor: Colors.grey[900],
);
}

@override
// TODO: implement preferredSize
Size get preferredSize => const Size.fromHeight(46.0);
}

Here we have a simple AppBar with the menu icon so that we can open our Drawer.

Creat Drawer

Go to your file widgets/drawer.dart and fill it with this code:

import 'package:flutter/material.dart';

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

@override
Widget build(BuildContext context) {
return Drawer(
backgroundColor: Colors.grey[300],
child: const Column(
children: [
DrawerHeader(
child: Icon(
Icons.favorite,
),
),
ListTile(
leading: Icon(
Icons.home,
),
title: Text('D A S H B O A R D'),
),
ListTile(
leading: Icon(
Icons.chat,
),
title: Text('M E S S A G E'),
),
ListTile(
leading: Icon(
Icons.settings,
),
title: Text('S E T T I N GS'),
),
ListTile(
leading: Icon(
Icons.logout,
),
title: Text('L O G O U T'),
),
],
),
);
}
}

Our Draweris quite simple, featuring a header and several ListTileitems simulating options with icons and text.

Create MyBox and MyTile

Since these components are just colored containers for example purposes, Iā€™ll go through them quickly here.
Go to your file widgets/my_box.dart and widgets/my_tile.dart and fill it with this code:

// MyBox

import 'package:flutter/material.dart';

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

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.blue,
),
);
}
}
// MyTile

import 'package:flutter/material.dart';

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

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.green,
height: 80.0,
),
);
}
}

These objects go beyond an explanation and are practically identical. Now with all our widgets ready, you can run your application.
flutter run to run it on an emulator or your phone, since weā€™ve only created the mobile screen so far, the other screens will be blank.

Nevertheless, I recommend running the app on the web to see the effect as you resize the browser window horizontally.
Use this command: flutter run -d chrome or use your preferred web browser.

When you run it at this point, youā€™ll see a result like this:

So letā€™s create the other two screens, which will be easier as weā€™ll leverage almost everything weā€™ve done so far.

Create Tablet Page

Go to the module tablet and in the file tablet_scaffold.dart we will use practically the same content from mobile_scaffold.dart with some proportion changes. Here is the code:

class TabletScaffold extends StatefulWidget {
const TabletScaffold({super.key});

@override
State<TabletScaffold> createState() => _TabletScaffoldState();
}

class _TabletScaffoldState extends State<TabletScaffold> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppBar(),
drawer: MyDrawer(),
backgroundColor: AppColors.defaultBackground,
body: Column(
children: [
AspectRatio(
aspectRatio: 4.0,
child: SizedBox(
width: double.infinity,
child: GridView.builder(
itemCount: 4,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: (context, index) => const MyBox(),
),
),
),
Expanded(
child: ListView.builder(
primary: true,
shrinkWrap: true,
itemCount: 9,
itemBuilder: (context, index) => const MyTile(),
),
),
],
),
);
}
}

Note that here we only have two differences, our aspectRatio before was 2, now itā€™s 4, and our crossAxisCount for our grid also changed from 2 to 4, to accommodate the larger size of the tablet.
Well, as you can see, the code is the same; you only need to adjust the size, which is different for mobile, tablet, and web, filling the space that would be ā€˜emptyā€™ if not modified.

Create Desktop Page

Here weā€™ll have some changes in the layout, adding an extra sidebar to the right while keeping our Drawer open.
For this, we will add aRow in ourbody, keeping the Drawer always open, dividing the screen horizontally into three sections.

class DesktopScaffold extends StatefulWidget {
const DesktopScaffold({super.key});

@override
State<DesktopScaffold> createState() => _DesktopScaffoldState();
}

class _DesktopScaffoldState extends State<DesktopScaffold> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.defaultBackground,
appBar: const MyAppBar(),
body: Row(
children: [
//open drawer
const MyDrawer(),
Expanded(
flex: 2,
child: Column(
children: [
AspectRatio(
aspectRatio: 4.0,
child: SizedBox(
width: double.infinity,
child: GridView.builder(
itemCount: 4,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: (context, index) => const MyBox(),
),
),
),
Expanded(
child: ListView.builder(
primary: true,
shrinkWrap: true,
itemCount: 9,
itemBuilder: (context, index) => const MyTile(),
),
),
],
),
),
Expanded(
child: Column(
children: [
Expanded(
child: Container(
color: Colors.pink,
),
),
],
))
],
),
);
}
}

Notice that we added Expanded widgets, so that the space is divided according to the weight (flex) of each.

Rodando no navegador

flutter run -d chrome or your preferred browser, youā€™ll be able to see this result:

Algumas observaƧƵes

Be careful with imports, especially if youā€™re also running it as an app. Sometimes, I had to clean the project after running it in the browser and then running it as an app, and vice versa. Just use a flutter clean and then run it again.

You can find the repository for this project on my GitHub.

Reference

Mitch Koko

--

--

Kaue Murakami
Kaue Murakami

Written by Kaue Murakami

Ninja in Dart - Flutter and NodeJS - JavaScript

Responses (1)