In this tutorial you'll learn how to easily make a UI layout for your flutter app that adapts to different screen sizes with theadaptive_layout
package.
The next few sections show how to build a simple UI that displays a list of movie posters and titles and allows the user to see more details of the movie when they tap an item in the list.
Small screen UI
On small, phone-sized, screens when the user taps an item they will navigate to a different screen with the details.
Large screen UI
On large screens, when the user taps a list item the details are displayed on the right side of the list and there is no navigation.
Live demo
Even better than an image,see a live demo of what we'll be building.
Let's go!
We'll take care of the behavior expected for small screens first, then adapt our implementation for larger screens. These are the steps:
- 1. Create a new app
- 2. Provide the data set
- 3. Create the model
- 4. Create the movie list page
- 5. Create the movie detail page
- 6. Navigate to the detail screen when an item is tapped
- 7. Convert movie list page into home page
- 8. Make the home page adaptive with the
adaptive_layout
package - 9. Display the detail view when an item is tapped on large screens
- BONUS: Make it look a little better on large screens
- BONUS: Tests
1. Create a new app
Go ahead andcreate
a new flutter app.
flutter create adaptive_layout_example
2. Provide the data set
We'll be using a hard-coded data set, so we can focus our efforts on the UI layout.
- Under the
lib
folder, create a folder calleddata
with a file nameddata.dart
. - Copy the contents ofthis file into your file.
3. Create the model
Create aMovie
class with a static function that parses the data into a list ofMovie
s and returns the list.
- Under the
lib
folder, create a folder calledmodel
with a filemovie.dart
. - Paste the following content into the file.
import'../data/data.dart';classMovie{finalintid;finalStringtitle;finalStringoverview;finalStringposterPath;Movie({requiredthis.id,requiredthis.title,requiredthis.overview,requiredthis.posterPath,});staticList<Movie>getList()=>data.map((element)=>Movie(id:element['id'],title:element['title'],overview:element['overview'],posterPath:element['poster_path'],),).toList();}constStringLARGE_POSTER_BASE_URL="https://image.tmdb.org/t/p/w300";constStringSMALL_POSTER_BASE_URL="https://image.tmdb.org/t/p/w92";
4. Create the movie list page
Now let's create a page that will display the list of movies:
- Create a new folder under
lib
calledpages
. - Within the
pages
folder create a file namedmovie_list_page.dart
. - In
movie_list_page.dart
create twoStatelessWidget
s: one calledMovieListPage
and another calledMovieListView
, with this content.
classMovieListPageextendsStatelessWidget{constMovieListPage({Key?key}):super(key:key);@overrideWidgetbuild(BuildContextcontext){finalmovies=Movie.getList();returnScaffold(appBar:AppBar(title:Text('Movies'),),body:MovieListView(movies:movies,onTapItem:(Moviemovie){// TODO: Navigate to detail screen},),);}}classMovieListViewextendsStatelessWidget{finalList<Movie>movies;finalFunction(Movie)onTapItem;constMovieListView({Key?key,requiredthis.movies,requiredthis.onTapItem,}):super(key:key);@overrideWidgetbuild(BuildContextcontext){returnListView.builder(itemCount:movies.length,itemBuilder:(context,index){finalmovie=movies[index];returnListTile(key:Key('list_item_$index'),leading:Image.network(SMALL_POSTER_BASE_URL+movie.posterPath),title:Text('${movie.title}'),contentPadding:EdgeInsets.all(12.0),onTap:(){onTapItem.call(movie);},);},);}}
Observe thatMovieListPage
is a container widget for the presentationalMovieListView
widget.
Now, remove the framework generatedMyHomePage
and_MyHomePageState
classes fromlib/main.dart
and useMovieListPage()
as thehome
widget for theMaterialApp
. Thelib/main.dart
file should now look like this:
voidmain(){runApp(MyApp());}classMyAppextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:'Adaptive Layout Tutorial',theme:ThemeData(primarySwatch:Colors.blue,),// Use `MovieListPage()` as the `home` widgethome:MovieListPage(),);}}
The list page should now be displayed when the app starts:
5. Create the movie detail page
Now let's create a page that will display more details of a given movie:
- Within the
pages
folder create a file namedmovie_detail_page.dart
. - Similarly, in
movie_detail_page.dart
create twoStatelessWidget
s: a container widget calledMovieDetailPage
and a presentationalMovieDetailView
widget, with this content:
classMovieDetailPageextendsStatelessWidget{finalMoviemovie;constMovieDetailPage(this.movie,{Key?key,}):super(key:key);@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(),body:MovieDetailView(movie),);}}classMovieDetailViewextendsStatelessWidget{finalMoviemovie;constMovieDetailView(this.movie,{Key?key,}):super(key:key);@overrideWidgetbuild(BuildContextcontext){returnSingleChildScrollView(padding:EdgeInsets.all(24.0),child:Column(crossAxisAlignment:CrossAxisAlignment.center,children:[ClipRRect(borderRadius:BorderRadius.circular(10.0),child:Image.network(LARGE_POSTER_BASE_URL+movie.posterPath),),SizedBox(height:24.0),Text(movie.title,style:Theme.of(context).textTheme.headline4,textAlign:TextAlign.center,),SizedBox(height:24.0),Text(movie.overview,style:Theme.of(context).textTheme.subtitle1,textAlign:TextAlign.center,),],),);}}
For simplicity, we are just displaying the poster image, title and overview of the movie in the detail view.
6. Navigate to the detail screen when an item is tapped
Update theonTapItem()
callback of theMovieListView
in theMovieListPage
widget, so we can navigate to the detail screen and back.
// Within the `MovieListPage` widgetonTapItem:(Moviemovie){Navigator.push(context,MaterialPageRoute(builder:(context)=>MovieDetailPage(movie)),);},
With that, our small screen UI is done! You should now be able to navigate to the detail screen and back:
7. Convert movie list page into home page
As a first step to making our layout adaptive to different screen sizes:
- Convert
MovieListPage
to aStatefulWidget
. (In VS Code place the cursor onStatelessWidget
, press CTRL/CMD + . and select "Convert to StatefulWidget".) - For semantic's sake rename it to
HomePage
and the file tohome_page.dart
.
On small screens theHomePage
widget will contain only theMovieListView
. On large screens theHomePage
widget will contain theMovieListView
and theMovieDetailView
side by side. You'll understand why we converted the widget to aStatefulWidget
in just a bit.
8. Make the home page adaptive with theadaptive_layout
package
To help us implement us display different layouts on different screen sizes we'll use theadaptive_layout
package.
- Install it with
$flutter pub add adaptive_layout
This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get).
dependencies:adaptive_layout:^0.1.3
- Now in
home_page.dart
, import the package:
import'package:adaptive_layout/adaptive_layout.dart';
- Update the
body
of theScaffold
ofHomePage
to be anAdaptiveLayout
with asmallLayout
and alargeLayout
like so:
classHomePageextendsStatefulWidget{constHomePage({Key?key}):super(key:key);@override_HomePageStatecreateState()=>_HomePageState();}class_HomePageStateextendsState<HomePage>{@overrideWidgetbuild(BuildContextcontext){finalmovies=Movie.getList();returnScaffold(appBar:AppBar(title:Text('Movies'),),// Now using an `AdaptiveLayout` as the `body`body:AdaptiveLayout(// Provide `MovieListView` as the `smallLayout`smallLayout:MovieListView(movies:movies,onTapItem:(Moviemovie){Navigator.push(context,MaterialPageRoute(builder:(context)=>MovieDetailPage(movie)),);},),// Provide a `Row` as the `largeLayout`largeLayout:Row(children:[Expanded(child:MovieListView(movies:movies,onTapItem:(Moviemovie){// TODO: Update detail view to the right},),),Expanded(child:Container(child:Center(child:Text('Select a movie from the list on the left to see the details here.',),),),)],),),);}}
ThesmallLayout
is just ourMovieListView
and thelargeLayout
is a 2-columnRow
with theMovieListView
in the firstExpanded
column.
At this point, on small screens the app should have the same behavior as before of navigating to the detail page when a list item is tapped. On large screens we should now see the list on the left-hand side of the screen and the message "Select a movie from the list on the left to see the details here" in the center of the right-hand side of the screen:
However, nothing happens when we tap a list item on large screens. Let's tackle that next.
9. Display the detail view when an item is tapped on large screens
With a few new lines of code in thehome_page.dart
file we can display the detail view on the right hand side when an item is tapped on large screens.
- Create a private property of the
_HomePageState
of typeMovie?
named_selectedMovie
.
class_HomePageStateextendsState<HomePage>{Movie?_selectedMovie;...
- Update the
onTapItem()
callback of theMovieListView
in thelargeLayout
of theAdaptiveLayout
widget to update the_selectedMovie
property withsetState
. This is why we converted the widget to aStatefulWidget
a few steps ago.
// Within the `largeLayout`onTapItem:(Moviemovie){setState((){_selectedMovie=movie;});},
- Update the second
Expandable
of theRow
to display the message if_selectedMovie
isnull
and theMovieDetailView
otherwise.
Expanded(child:Container(child:_selectedMovie==null?Center(child:Text('Select a movie from the list on the left to see the details here.',),):MovieDetailView(_selectedMovie!,),),)
And... we're done! 🎉
On small, phone-sized, screens when the user taps an item they will navigate to a different screen with the details and on larger screens the details are displayed on the right side of the list and there is no navigation.
BONUS: Make it look a little better on large screens
To make our app look a little better, let's leave the selected list item highlighted and put a divider line between the list view and the detail view with a few new lines of code in thehome_page.dart
file.
- Add a new attribute
selectedId
attribute toMovieListView
, so it knows which item a selected.
classMovieListViewextendsStatelessWidget{finalList<Movie>movies;finalFunction(Movie)onTapItem;finalint?selectedId;constMovieListView({Key?key,requiredthis.movies,requiredthis.onTapItem,this.selectedId,}):super(key:key);...
- Update the
itemBuilder
of theListView.builder
to wrap theListTile
in aContainer
and give theContainer
color if theListTile
's movie is the selected one.
itemBuilder:(context,index){finalmovie=movies[index];finalcolor=movie.id==selectedId?Theme.of(context).primaryColor.withOpacity(0.25):Colors.transparent;returnContainer(color:color,child:ListTile(key:Key('list_item_$index'),leading:Image.network(SMALL_POSTER_BASE_URL+movie.posterPath),title:Text('${movie.title}'),contentPadding:EdgeInsets.all(12.0),onTap:(){onTapItem.call(movie);},),);},
- Update the first
Expandable
of theRow
in thelargeLayout
of theAdaptiveLayout
to provide the selected movie'sid
to theMovieListView
.
Expanded(child:MovieListView(movies:movies,onTapItem:(Moviemovie){setState((){_selectedMovie=movie;});},selectedId:_selectedMovie?.id,),),
- Lastly, let's add a border to the left-hand side of the detail view's
Container
with someBoxDecoration
.
Expanded(child:Container(decoration:BoxDecoration(border:Border(left:BorderSide(color:Colors.grey[300]!,width:1.0,),),),child:_selectedMovie==null?Center(child:Text('Select a movie from the list on the left to see the details here.',),):MovieDetailView(_selectedMovie!,),),)
Of course the sky's the limit with the design, but I think our app is minimally presentable now:
BONUS: Tests
I chose not to TDD this so we could focus on the layout, but you can findthe tests here in the source code.
Note in the tests that I useWidgetTester
to specify a small screen size in asetUp
function like this:
setUp((){binding.window.physicalSizeTestValue=Size(AdaptiveLayout.getBreakpoints().mediumScreenMinWidth-10,500,);binding.window.devicePixelRatioTestValue=1.0;});
and to specify a large screen size like this:
setUp((){binding.window.physicalSizeTestValue=Size(AdaptiveLayout.getBreakpoints().largeScreenMinWidth+10,500,);binding.window.devicePixelRatioTestValue=1.0;});
Head over the theGithub repo to see the complete source code with a separate commit for each step 😉.
Congrats on making it to the end and thanks for following along! 👏 ❤️ ⭐ 🦄 🔖
Top comments(3)

- LocationUtrecht, the Netherlands
- WorkSoftware engineer at Touchwonders
- Joined
I added this story to my newsletterishouldgotosleep.com/this-week-in-...

Awesome newsletter! And thanks for adding the article ❤️

- LocationColombia
- WorkJunior frontend dev at 4-72
- Joined
excelent!!
For further actions, you may consider blocking this person and/orreporting abuse