Flutter responsive applicationsš±š».
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 Drawer
is quite simple, featuring a header and several ListTile
items 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.