1. Introduction
Material Components (MDC) help developers implement Material Design. Created by a team of engineers and UX designers at Google, MDC features dozens of beautiful and functional UI components and is available for Android, iOS, web and Flutter.material.io/develop |
In codelab MDC-101, you used two Material Components to build a login page: text fields and buttons with ink ripples. Now let's expand upon this foundation by adding navigation, structure, and data.
What you'll build
In this codelab, you'll build a home screen for an app called Shrine, an e-commerce app that sells clothing and home goods. It will contain:
- A top app bar
- A grid list full of products
Android | iOS |
Material Flutter components and subsystems in this codelab
- Top app bar
- Grids
- Cards
How would you rate your level of experience with Flutter development?
2. Set up your Flutter development environment
You need two pieces of software to complete this lab—the Flutter SDK and an editor.
You can run the codelab using any of these devices:
- A physical Android or iOS device connected to your computer and set to Developer mode.
- The iOS simulator (requires installing Xcode tools).
- The Android Emulator (requires setup in Android Studio).
- A browser (Chrome is required for debugging).
- As a Windows, Linux, or macOS desktop application. You must develop on the platform where you plan to deploy. So, if you want to develop a Windows desktop app, you must develop on Windows to access the appropriate build chain. There are operating system-specific requirements that are covered in detail on docs.flutter.dev/desktop.
3. Download the codelab starter app
Continuing from MDC-101?
If you completed MDC-101, your code should be prepared for this codelab. Skip to step: Add a top app bar.
Starting from scratch?
Download the starter codelab app
The starter app is located in the material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series
directory.
...or clone it from GitHub
To clone this codelab from GitHub, run the following commands:
git clone https://github.com/material-components/material-components-flutter-codelabs.git cd material-components-flutter-codelabs/mdc_100_series git checkout 102-starter_and_101-complete
Open the project and run the app
- Open the project in your editor of choice.
- Follow the instructions to "Run the app" in Get Started: Test drive for your chosen editor.
Success! You should see the Shrine login page from the MDC-101 codelab on your device.
Android | iOS |
Now that the login screen looks good, let's populate the app with some products.
4. Add a top app bar
Right now, if you click the "Next" button you will be able to see the home screen that says "You did it!". That's great! But now our user has no actions to take, or any sense of where they are in the app. To help, it's time to add navigation.
Material Design offers navigation patterns that ensure a high degree of usability. One of the most visible components is a top app bar.
To provide navigation and give users quick access to other actions, let's add a top app bar.
Add an AppBar widget
In home.dart
, add an AppBar to the Scaffold and remove the highlighted const
:
return const Scaffold(
// TODO: Add app bar (102)
appBar: AppBar(
// TODO: Add buttons and title (102)
),
Adding the AppBar to the Scaffold's appBar:
field, gives us a perfect layout for free, keeping the AppBar at the top of the page and the body underneath.
Add a Text widget
In home.dart
, add a title to the AppBar:
// TODO: Add app bar (102)
appBar: AppBar(
// TODO: Add buttons and title (102)
title: const Text('SHRINE'),
// TODO: Add trailing buttons (102)
Save your project.
Android | iOS |
Many app bars have a button next to the title. Let's add a menu icon in our app.
Add a leading IconButton
While still in home.dart
, set an IconButton for the AppBar's leading:
field. (Put it before the title:
field to mimic the leading-to-trailing order):
// TODO: Add buttons and title (102)
leading: IconButton(
icon: const Icon(
Icons.menu,
semanticLabel: 'menu',
),
onPressed: () {
print('Menu button');
},
),
Save your project.
Android | iOS |
The menu icon (also known as the "hamburger") shows up right where you'd expect it.
You can also add buttons to the trailing side of the title. In Flutter, these are called "actions".
Add actions
There's room for two more IconButtons.
Add them to the AppBar instance after the title:
// TODO: Add trailing buttons (102)
actions: <Widget>[
IconButton(
icon: const Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
print('Search button');
},
),
IconButton(
icon: const Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
print('Filter button');
},
),
],
Save your project. Your home screen should look like this:
Android | iOS |
Now the app has a leading button, a title, and two actions on the right side. The app bar also displays elevation using a subtle shadow that shows it's on a different layer than the content.
5. Add a card in a grid
Now that our app has some structure, let's organize the content by placing it into cards.
Add a GridView
Let's start by adding one card underneath the top app bar. The Card widget alone doesn't have enough information to lay itself out where we could see it, so we'll want to encapsulate it in a GridView widget.
Replace the Center in the body of the Scaffold with a GridView:
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
// TODO: Build a grid of cards (102)
children: <Widget>[Card()],
),
Let's unpack that code. The GridView invokes the count()
constructor since the number of items it displays is countable and not infinite. But it needs more information to define its layout.
The crossAxisCount:
specifies how many items across. We want 2 columns.
The padding:
field provides space on all 4 sides of the GridView. Of course you can't see the padding on the trailing or bottom sides because there's no GridView children next to them yet.
The childAspectRatio:
field identifies the size of the items based on an aspect ratio (width over height).
By default, GridView makes tiles that are all the same size.
We have one card but it's empty. Let's add child widgets to our card.
Layout the contents
Cards should have regions for an image, a title, and secondary text.
Update the children of the GridView:
// TODO: Build a grid of cards (102)
children: <Widget>[
Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 18.0 / 11.0,
child: Image.asset('assets/diamond.png'),
),
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Title'),
const SizedBox(height: 8.0),
Text('Secondary Text'),
],
),
),
],
),
)
],
This code adds a Column widget used to lay out the child widgets vertically.
The crossAxisAlignment: field
specifies CrossAxisAlignment.start
, which means "align the text to the leading edge."
The AspectRatio widget decides what shape the image takes no matter what kind of image is supplied.
The Padding brings the text in from the side a little.
The two Text widgets are stacked vertically with 8 points of empty space between them (SizedBox). We make another Column to house them inside the Padding.
Save your project.
Android | iOS |
In this preview, you can see the card is inset from the edge, with rounded corners, and a shadow (that expresses the card's elevation). The entire shape is called the "container" in Material. (Not to be confused with the actual widget class called Container.)
Cards are usually shown in a collection with other cards. Let's lay them out as a collection in a grid.
6. Make a card collection
Whenever multiple cards are present in a screen, they are grouped together into one or more collections. Cards in a collection are coplanar, meaning cards share the same resting elevation as one another (unless the cards are picked up or dragged, but we won't be doing that here).
Multiply the card into a collection
Right now our Card is constructed inline of the children:
field of the GridView. That's a lot of nested code that can be hard to read. Let's extract it into a function that can generate as many empty cards as we want, and returns a list of Cards.
Make a new private function above the build()
function (remember that functions starting with an underscore are private API):
// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
List<Card> cards = List.generate(
count,
(int index) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 18.0 / 11.0,
child: Image.asset('assets/diamond.png'),
),
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
Text('Title'),
SizedBox(height: 8.0),
Text('Secondary Text'),
],
),
),
],
),
);
},
);
return cards;
}
Assign the generated cards to GridView's children
field. Remember to replace everything contained in the GridView with this new code:
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
children: _buildGridCards(10) // Replace
),
Save your project.
Android | iOS |
The cards are there, but they show nothing yet. Now's the time to add product data.
Add product data
The app has some products with images, names, and prices. Let's add that to the widgets we have in the card already
Then, in home.dart
, import a new package and some files we supplied for a data model:
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'model/product.dart';
import 'model/products_repository.dart';
Finally, change _buildGridCards()
to fetch the product info, and use that data in the cards:
// TODO: Make a collection of cards (102)
// Replace this entire method
List<Card> _buildGridCards(BuildContext context) {
List<Product> products = ProductsRepository.loadProducts(Category.all);
if (products.isEmpty) {
return const <Card>[];
}
final ThemeData theme = Theme.of(context);
final NumberFormat formatter = NumberFormat.simpleCurrency(
locale: Localizations.localeOf(context).toString());
return products.map((product) {
return Card(
clipBehavior: Clip.antiAlias,
// TODO: Adjust card heights (103)
child: Column(
// TODO: Center items on the card (103)
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 18 / 11,
child: Image.asset(
product.assetName,
package: product.assetPackage,
// TODO: Adjust the box size (102)
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
// TODO: Align labels to the bottom and center (103)
crossAxisAlignment: CrossAxisAlignment.start,
// TODO: Change innermost Column (103)
children: <Widget>[
// TODO: Handle overflowing labels (103)
Text(
product.name,
style: theme.textTheme.titleLarge,
maxLines: 1,
),
const SizedBox(height: 8.0),
Text(
formatter.format(product.price),
style: theme.textTheme.titleSmall,
),
],
),
),
),
],
),
);
}).toList();
}
NOTE: Won't compile and run yet. We have one more change.
Also, change the build()
function to pass the BuildContext to _buildGridCards()
before you try to compile:
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
children: _buildGridCards(context) // Changed code
),
Hot restart the app.
Android | iOS |
You may notice we don't add any vertical space between the cards. That's because they have, by default, 4 points of margin on their top and bottom.
Save your project.
The product data shows up, but the images have extra space around them. The images are drawn with a BoxFit of .scaleDown
by default (in this case). Let's change that to .fitWidth
so they zoom in a little and remove the extra whitespace.
Add a fit:
field to the image with a value of BoxFit.fitWidth
:
// TODO: Adjust the box size (102)
fit: BoxFit.fitWidth,
Android | iOS |
Our products are now showing up in the app perfectly!
7. Congratulations!
Our app has a basic flow that takes the user from the login screen to a home screen, where products can be viewed. In just a few lines of code, we added a top app bar (with a title and three buttons) and cards (to present our app's content). Our home screen is now simple and functional, with a basic structure and actionable content.
Next steps
With the top app bar, card, text field, and button, we've now used four core components from the Material Flutter library! You can explore more by visiting the Material components widgets catalog.
While it's fully functioning, our app doesn't yet express any particular brand or point of view. In MDC-103: Material Design Theming with Color, Shape, Elevation and Type, we'll customize the style of these components to express a vibrant, modern brand.