Suppose, you have an app and you want to look a certain way on a Mobile and a certain way on a Tablet or Desktop. For example, you have a ListView on mobile and you click on the item and you go to a detail page but on a tablet or a computer screen, the left side is ListView, and just click through changes my view on the right.

When you’re designing a real app it’s helpful to think about these things as early as possible because you don’t know what size of the device your users going to have. We might test on a mobile phone or we might test on a tablet or we might test on with a desktop window whatever it happens to be but your users are going to on a different size so it’s really helpful to think about the overall presentation and layout of your app and how it’s going to adapt to the different sizes as you go about.
We start with the adapted design and we are going to make our app such that it can adapt to different sizes whether our window mobile or tablet or desktop whatever it needs to be. It depends on what our content does and likes.
The primary view of the app is going to be a list of the animals. One of the easiest assemblies of an app is the list of something and then the detail of the list on the one side.
class _MyHomePageState extends State<MyHomePage> {
final List<String> animals = <String
['Tiger','Lion','Leopard',
'Jaguar','Wolf','Elephant','Deer'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(animals[index]),
onTap: () {
......
);
},
separatorBuilder: (BuildContext context, int index) =>
const Divider(),
itemCount: animals.length),
),
);
}
}
We have this and now we probably want the user to be able to go through the detail. We need a detailed widget and for that, I’m just gonna do a very simple class DetailPage.
class DetailPage extends StatelessWidget {
final String title;
var animalList = {
'Tiger': 'assets/images/Tiger.jpeg',
'Deer': 'assets/images/Deer.jpeg',
'Elephant': 'assets/images/Elephant.jpeg',
'Lion': 'assets/images/Lion.jpeg',
'Jaguar': 'assets/images/Jaguar.jpeg',
'Wolf': 'assets/images/Wolf.jpeg',
'Leopard': 'assets/images/Leopard.jpeg',
};
DetailPage({Key key, @required this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(animalList[title]),
);
}
}
The DetailPage is a completely pure widget and that means I’ll just do a column and that will have children.
There are cases where nested scaffolds are useful this may not be one of them. I’ll make the DetailPage pure. This is something by the way if you’re coming from android. You may be familiar with fragments and these are these where a similar idea of like a fragment can be either on its own or it could be put as a frame.
Flutter hasn’t Fragment because it doesn’t need that because everything on the screen is a widget so we can just make a column and that can be one part of the column can be one fragment. It’s just a widget and the other part of the column is another or a row or whatever.
The first thing would be like what does a scaffold do and why would we want to have this in detail widget have its own scaffold.
If we have a separate DetailView on a mobile phone we need a scaffold. We will need it when we want this on the mobile phone but we don’t want that for desktop when we have that widget just sitting next to the list right.
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
body: DetailPage(
title: animals[index],
))));
},
We have the scaffold in the material page route. We can make sure that the DetailPage widget is still only responsible for rendering and building the details of an Animal. They don’t worry about the scaffold. It’s not worried about the theming, it’s not worried about anything else only Animal Details and then our page can handle the scaffold.
If you have a standalone page you might want to have the name of the Animal in the app bar. This app bar you’re adding in it’s just there so that we can go back.
This was all we’ve just done the easy part the one way to display it and now what if we want to do the other one the more like everything in one place.
Download this project from GitHub.
Adaptive Design
We have two different ways to do this, one of them is a media query where you are asking as a whole the app, what it looks like is its big screen is or it a small screen, and so on.
The important part is that flutter has a widget for this, LayoutBuilder. It is more context-sensitive. It takes the two parameters, the context, and the constraints, from the constraint it will get the size. It’s a box constraint so that’s all give us the height and the width.
LayoutBuilder is a pretty simple widget it just rebuilds anytime the layout around it changes. For any time you switch the orientation of your screen that changes the layout that changes the constraints of the whole app. Therefore we will rerun or this builder will be rerun with new contacts. You can use it in many different ways.
The cleanest possible way to do is, it would be to just say something like If the width is above some threshold like more than 600 then return WideLayout.This something called breakpoints. This is like the adaptive thing, we’re adapting our screen or app somehow to WideLayout.
body: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return WideLayout();
} else {
return NarrowLayout();
}
},
));
You as your app designer can decide what those values actually are which makes sense for your data since we’re doing a fairly typical list detail layout that is pretty common. We can use pretty common values 600 is approximately different between mobile and tablet.
If we could factor out the list view into our own widget that would be helpful because each one of our layouts only needs to say “here’s where the list view goes here’s where the detail view goes”. There needs to some callback where the WideLayout says just change this and the NarrowLayout says push this.
class AnimalListWidget extends StatelessWidget {
AnimalListWidget({Key key, this.onTapCallback}) : super(key: key);
List<String> animals = <String>['Tiger','Lion','Leopard'
,'Jaguar','Wolf','Elephant','Deer'];
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(animals[index]),
onTap: () => onTapCallback(animals[index]));
},
separatorBuilder: (BuildContext context, int index) => const Divider(),
itemCount: animals.length);
}
}
The void callback is a function that says that takes an animal(String). This is basically what to add.
final void Function(String) onTapCallback;
...........
onTap: () => onTapCallback(animals[index]));
onTap() this callback is used to tie to the detail and the list together to make sure we have both the same thing selected on both. So that NarrowLayout and WideLayout can do their own thing because that it’s going to be different.
We can’t hard code the navigator off because then the WideLayout would look weird you would select something and it would create a new screen.
NarrowLayout basically just has the Animal List in it. Callback wherein the NarrowLayout would do this new page route onto the navigator.
class NarrowLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimalListWidget(onTapCallback: (animal) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: Text(animal)),
body: DetailPage(
title: animal,
))));
});
}
}
Then the WideLayout, you don’t really need the callback it just kind of updates you do because the WideLayout needs to update the other part of the screen. If you tap on Deer then it should show on the right-hand side. This time state management comes in.
class WideLayout extends StatefulWidget {
@override
_WideLayoutState createState() => _WideLayoutState();
}
class _WideLayoutState extends State<WideLayout> {
String _animal = 'Lion';
@override
Widget build(BuildContext context) {
return Row(
children: [
......
],
);
}
}
It needs to be a stateful widget because some state needs to be set so that WideLayout knows what to show.
In WideLayout you have a row. It’ll have the Animal list on one side and what initially nothing slash an empty Animal detail on the other side.
Expanded(
child: AnimalListWidget(
onTapCallback: (animal) => setState(() => _animal = animal)),
flex: 2,
),
Expanded(
child: _animal == null ? Placeholder() : DetailPage(title: _animal),
flex: 3,
)
It has the first one on the left is the Animal list and the other thing on it would be just a container or placeholder.
We know which animal is selected therefore we can tell the AnimalList where to go where to scroll to and then the Animal detail which one to show.
That’s also a good point to emphasize is we have only one if statement in here and that’s to define our breakpoints on whether we’re doing WideLayout versus and NarrowLayout and that’s it there’s no other if statements we don’t have to worry about really any other configurations.