You’ve got the hang of scrollable widgets, but there’s so much more to explore. Don’t limit your app to just mobile screens—Flutter excels at adapting to various devices, from phones to tablets, desktops and the web. With Flutter, create an app that’s not only mobile-friendly but effortlessly scales to any screen size. Embrace versatility and go universal.
In this chapter, you’ll delve deeper into the world of scrollable widgets. You’ll learn how to:
Create custom scroll effects with the Sliver widget.
Make your UI responsive with the GridView widget.
You’ll continue to build out your food app, Yummy, by introducing a new feature: the RestaurantPage. Here, users can tap on a restaurant to explore everything from today’s menu to a gallery of enticing dishes, all displayed responsively.
Heads up: Grab a snack! The sight of food pictures might just work up an appetite.
Here is what the mobile view looks like:
And here’s the experience reimagined for the web:
In a bit your app will look and function beautifully on any device, providing a seamless and responsive experience from mobile devices to the web.
Getting Started
Open the starter project in Android Studio, then run flutter pub get if necessary and run the app.
You’ll see the explore page as shown below:
Note In this chapter you’ll be running the app on mobile and web to test and develop responsive a UI. In Android Studio you can run multiple devices by clicking the drop-down menu as shown below:
New Files in the Project
There are new files in this starter project to help you out. Before you start take a look at them.
Open the new lib/components/restaurant_item.dart file. You’ll find the class RestaurantItem.
This widget is designed to showcase individual menu items in a restaurant’s menu.
Here’s a breakdown of its components:
Title
Description
Price
Popularity indicator
Image
Add button, to add an item to the cart
Introducing Slivers
Slivers in Flutter are a fundamental part of creating custom scroll effects in a scrollable area. They are a family of widgets that provide various ways to lay out a list of children in a scrolling view.
Unlike more straightforward widgets like ListView or GridView, slivers give developers fine-grained control over scroll behavior, animation, and the geometry of scrolling elements, making them the building blocks for complex scrollable areas.
SliversSlivers
Types of Slivers
Slivers operate within the CustomScrollView widget, which allows them to combine different scrolling behaviors in a single scroll view.
SliverList and SliverGrid are the sliver equivalents of ListView and GridView, respectively. They allow you to lay out items linearly or in a grid pattern.
SliverAppBar is a highly flexible app bar that can expand, collapse, float, and snap as you scroll.
SliverToBoxAdapter allows you to place a single non-sliver widget within a CustomScrollView.
SliverFillRemaining and SliverFillViewport let you size children based on the remaining space in the viewport, creating dynamic effects as you scroll.
Note: For a deeper dive into how you can leverage slivers to create various scrolling effects, you can review Flutter’s documentation.
Ready to explore the world of slivers? Let’s get scrolling!
Building the Restaurant Page
First you need to set up the RestaurantPage. When users click on a restaurant in the Food Near Me section of your app it will direct them to this page that displays the restaurant’s menu.
In lib/screens, create a new file called restaurant_page.dart. Add the following code:
Define a new class RestaurantPage as a StatefulWidget so we can have a mutable state.
RestaurantPage takes in a restaurant object which contains data such as restaurant info and menu to display on the page.
_RestaurantPageState is a class that holds the state for RestaurantPage.
The build() method currently returns a temporary placeholder text that displays Restaurant Page. This is where you’ll add the new elements of this view.
Navigating to a Restaurant Page
Let’s first implement a way to display a single restaurant.
Open lib/components/restaurant_landscape_card.dart.
Locate // TODO: Push Restaurant Page and replace it with the following code:
This function returns a CustomScrollView, a versatile widget that coordinates a variety of sliver widgets to create a multifaceted scrolling interface. Initially, this scroll view is populated with placeholder slivers:
SliverToBoxAdapter is a handy widget that adapts a standard, non-sliver widget for use within a sliver list.
SliverFillRemaining sizes its children to occupy the available remaining space in the viewport, perfect for expanding content areas.
These placeholders will be replaced later with the actual content for the app’s scrolling layout.
In the same file, locate the comment // TODO: Replace with Custom Scroll View and replace it and child with the following:
child: _buildCustomScrollView(),
Perform a hot reload, and you should see the following screen(s):
Next, you’ll create a SliverAppBar that remains pinned to the top of a scrollable area.
Building a Sliver App Bar
A SliverAppBar expands to reveal a large image of a restaurant with a circular icon overlayed at the bottom left. As the user scrolls up, the app bar will remain at the top, and shrink the regular app bar size.
Locate // TODO: Build Sliver App Bar and replace it with the following code:
The function returns a SliverAppBar widget that creates a collapsible app bar.
Keep the app bar pinned at the top of the view.
Specify expandedHeight of 300.0 pixels for maximum height when fully expanded.
Use a FlexibleSpaceBar for the collapsible part of the app bar.
Within the FlexibleSpaceBar, set a background widget.
Apply some padding to create internal spacing for the background.
Arrange elements using a Stack.
Create a Container for the backdrop with styling.
Show the restaurant image as a background using DecorationImage.
Place a circular icon at the bottom left using Positioned widget.
Integrate the Sliver App Bar
Now add the app bar to your custom scroll view. Locate // TODO: Add Sliver App Bar and replace it and SliverToBoxAdapter with the following:
_buildSliverAppBar(),
Perform a hot reload. Tap on a restaurant, and you should see the app bar as shown below:
When you look at a restaurant knowing its details is helpful, but you don’t have that. No worries, you’ll add the info section next.
Building the Restaurant Info Section
After adding a sliver containing the restaurant’s information such as the name, address, rating, distance and attributes your app bar will look like this:
Locate the comment // TODO: Build Info Section and replace it with the following code:
Create _buildInfoSection() to construct a UI section for the restaurant’s details.
Retrieve the application’s text styles for consistent theming.
Access the restaurant’s data passed to the widget.
Create a SliverToBoxAdapter to enable a column of text widgets in a sliver-based layout.
Apply padding around the column for spacing.
Create a column and align text elements to the start of the column.
Display the restaurant’s name, address, rating, and attributes with styled text widgets.
Replace // TODO: Add Restaurant Info Section and SliverToBoxAdapter beneath it with:
_buildInfoSection(),
Perform a hot reload. Tap on a restaurant, and you should see the app bar as shown below:
Next, you’ll use a grid view to display the collection of menu items for a restaurant. Before you do that, take a moment to get acquainted with the GridView widget.
Introducing GridView
GridView is a 2D array of scrollable widgets. Similar to its linear counterpart, the ListView, it supports both horizontal and vertical scrolling, but it arranges its children in a grid format, which is perfect for displaying multiple items in a clean, organized layout.
cross axismain axis
Getting used to GridView is easy. Like ListView it inherits from ScrollView, so their constructors are very similar.
The default GridView.count() constructor is great for creating a grid with a fixed number of tiles in the cross-axis. You would use this if you know the number of columns or rows you want upfront.
GridView.builder() works similarly to ListView.builder() by lazily constructing items as they’re scrolled into the viewport, ideal for displaying a large or indeterminate number of items.
GridView.custom() offers the highest level of customization, letting you use a custom SliverGridDelegate for precise control over how your grid is laid out.
GridView.extent() allows you to specify the maximum extent of the tiles in the cross-axis, and it will determine the number of tiles in each row or column dynamically based on the available space.
GridView Key Parameters
Here are some parameters you should pay attention to:
crossAxisSpacing: The spacing between each child in the cross-axis.
mainAxisSpacing: The spacing between each child on the main axis.
crossAxisCount: The number of children in the cross-axis. You can also think of this as the number of columns you want in a grid.
shrinkWrap. Controls the fixed scroll area size.
physics: Controls how the scroll view responds to user input.
primary: Helps Flutter determine which scroll view is the primary one.
scrollDirection: Controls the axis along which the view will scroll.
What’s the difference between the main and cross axis? Remember that Columns and Rows are like ListViews, but without a scroll view.
The main axis always corresponds to the scroll direction!
If your scroll direction is horizontal, you can think of this as a Row. The main axis represents the horizontal direction, as shown below:
Rowmain axiscross axis
If your scroll direction is vertical, you can think of it as a Column. The main axis represents the vertical direction, as shown below:
main axisColumncross axis
Understanding Grid Delegates
Grid delegates help you figure out the spacing and the number of columns to use to lay out the children in a GridView.
Aside from customizing your grid delegates, Flutter provides two delegates you can use out of the box:
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithMaxCrossAxisExtent
The first creates a layout that has a fixed number of tiles along the cross-axis. The second creates a layout with tiles that have a maximum cross-axis extent.
Building the Grid View Section
You’ll use the GridView widget to display a collection of items on a restaurant’s menu. GridView is a great widget to make your app responsive on different screens.
For example, depending on the screen width, you could have the grid view display one or two columns to show more information and leverage the real estate of a bigger screen, as shown below:
Building the Grid Item
Still within restaurant_page.dart locate // TODO: Build Grid Item and replace it with the following:
Widget _buildGridItem(int index) {
final item = widget.restaurant.items[index];
return InkWell(
onTap: () {
// Present Bottom Sheet in the future.
},
child: RestaurantItem(item: item),
);
}
This function takes an index and uses it to access a specific item from the restaurant’s menu. It then creates a RestaurantItem widget for that menu item. By wrapping the widget in an InkWell, we lay the groundwork for interactive functionality, such as opening a detail view in a bottom sheet upon tapping, which will be implemented in the next chapter.
Add this import at the top of the file:
import '../components/restaurant_item.dart';
Building the Section Title
Next, locate // TODO: Build Section Title and replace it with the following:
The function _buildGridView() accepts the number of columns as a parameter. This is used to determine the number of columns to display on different devices.
GridView.builder() is used for efficient, on-demand building of grid items.
Set up SliverGridDelegateWithFixedCrossAxisCount to define the grid’s column count and spacing.
Each grid item is built via _buildGridItem(), called within the itemBuilder callback.
Set the number of items to display in the grid.
The shrinkWrap property is enabled, allowing the GridView to size itself according to its children vertically.
Set the physics to NeverScrollableScrollPhysics, to prevent scrolling within the grid itself.
Now that you’ve created your grid view, it is time to wrap it in a sliver.
Building the Grid View Sliver Section
Find // TODO: Add Desktop Threshold and replace it with the following:
static const desktopThreshold = 700;
This constant is used to determine whether to adapt the restaurant menu layout to big or small screens.
You need to calculate the number of columns depending on the screen’s width. Replace // TODO: Calculate Column Count with:
Calculate the number of columns based on the screen’s width.
Build a SliverToBoxAdapter to embed a non-sliver widget.
Initialize a container for content with some padding.
Set up a vertical layout with a Column widget.
Add a section title using a custom method.
Add a grid view with the specified number of columns.
Finally, locate // TODO: Add Menu Item Grid View Section and replace it and the sliver placeholder widget with the following:
_buildGridViewSection('Menu'),
Perform a hot reload, tap on a restaurant, and you should see the following on mobile or web:
When building for multiple platforms, it’s important to make sure that your app looks great on each platform. A web app has more screen real estate than a mobile app, so it’s important to make sure that the menu is responsive and adapts to the screen size. That’s what you’ll do in the next section.
Implementing a Responsive Menu
Crafting a responsive restaurant menu for web applications is a pivotal task that strikes a balance between optimal use of screen real estate, readability, and visual appeal.
When transitioning from mobile to web views, it’s crucial to consider how the menu’s layout adapts to larger screens. Let’s explore two primary strategies:
Option 1: Full-Screen Stretch - This approach extends the menu across the full width of the screen. While it maximizes space, it can also make the menu appear too large. The vastness can overwhelm users, presenting too much information at once and detracting from the ability to focus on individual items.
Option 2: Fixed Width - The alternative is to constrain the menu within a fixed-width container. This design is more aligned with standard web browsing expectations. It offers a structured layout with ample white space around the menu, which enhances visual appeal and improves content legibility.
Implementing Responsive Design
Next, you’ll delve into the technical implementation to achieve a responsive and visually pleasant menu for web users.
Still in restaurant_page.dart, locate the comment // TODO: Add Constraint Properties and replace it with the following:
With these changes, your restaurant menu will dynamically adapt to both mobile and web screen sizes. The result is a seamless user experience across devices.
Perform a hot reload, build, run your app and look at the restaurant menu on both mobile and web.
Try resizing your web browser window. You’ll observe how elegantly your restaurant menu adapts to various screen sizes – a testament to responsive design in action!
And there you have it – a responsive, visually appealing, and user-friendly restaurant menu for your Flutter application!
Key Points
Slivers allow building intricate scrolling layouts with CustomScrollView and various sliver widgets.
With GridView you can create grid layouts with customizable columns and spacing.
SliverToBoxAdapter enables the integration of non-sliver widgets into sliver lists.
Manage scroll behavior and direction with properties like physics.
Embed grid views within sliver lists for complex scrollable structures.
Use MediaQuery to create responsive grid layouts with GridView, adjusting the number of columns based on the screen size.
Where to Go From Here?
Now that you’ve a grasp of slivers and grid views in Flutter, you can create complex and custom scrollable layouts that look good and perform well on a wide range of devices. Slivers enable you to make scrollable areas in your app that look and behave exactly how you want.
In the next chapter, you’ll take a look at some more interactive widgets.