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 |
You can now use Material Flutter to customize your apps' distinctive style more than ever. Material Design's recent expansion gives designers and developers increased flexibility to express their product's brand.
In codelabs MDC-101 and MDC-102, you used Material Flutter to build the basics of an app called Shrine, an e-commerce app that sells clothing and home goods. This app contains a user flow that starts with a login screen, then takes the user to a home screen that displays products.
What you'll build
In this codelab, you'll customize the Shrine app using:
- Color
- Typography
- Elevation
- Shape
- Layout
Android | iOS |
Material Flutter components and subsystems in this codelab
- Themes
- Typography
- Elevation
- Image list
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-102?
If you completed MDC-102, your code should be ready to go for this codelab. Skip to step: Change the colors.
Starting from scratch?
Download the starter codelab app
The starter app is located in the material-components-flutter-codelabs-103-starter_and_102-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 103-starter_and_102-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 previous codelabs on your device.
Android | iOS |
Click "Next" to see the product page.
Android | iOS |
4. Change the colors
A color scheme has been created that represents the Shrine brand, and the designer would like you to implement that color scheme across the Shrine app
To start, let's import those colors into our project.
Create colors.dart
Create a new dart file in lib
called colors.dart
. Import material.dart
and add const Color
values:
import 'package:flutter/material.dart';
const kShrinePink50 = Color(0xFFFEEAE6);
const kShrinePink100 = Color(0xFFFEDBD0);
const kShrinePink300 = Color(0xFFFBB8AC);
const kShrinePink400 = Color(0xFFEAA4A4);
const kShrineBrown900 = Color(0xFF442B2D);
const kShrineErrorRed = Color(0xFFC5032B);
const kShrineSurfaceWhite = Color(0xFFFFFBFA);
const kShrineBackgroundWhite = Colors.white;
Custom color palette
This color theme has been created by a designer with custom colors (shown in the image below). It contains colors that have been selected from Shrine's brand and applied to the Material Theme Editor, which has expanded them to create a fuller palette. (These colors aren't from the 2014 Material color palettes.)
The Material Theme Editor has organized them into shades labelled numerically, including labels 50, 100, 200, .... to 900 of each color. Shrine only uses shades 50, 100, and 300 from the pink swatch and 900 from the brown swatch.
Each colored parameter of a widget is mapped to a color from these schemes. For example, the color for a text field's decorations when it's actively receiving input should be the theme's Primary color. If that color isn't accessible (easy to see against its background), use another color instead.
Now that we have the colors we want to use, we can apply them to the UI. We'll do this by setting the values of a ThemeData widget that we apply to the MaterialApp instance at the top of our widget hierarchy.
Customize ThemeData.light()
Flutter includes a few built-in themes. The light theme is one of them. Rather than making a ThemeData widget from scratch, we'll copy the light theme and change the values to customize them for our app.
Let's import colors.dart
in app.dart.
import 'colors.dart';
Then add the following to app.dart outside the scope of the ShrineApp class:
// TODO: Build a Shrine Theme (103)
final ThemeData _kShrineTheme = _buildShrineTheme();
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: base.colorScheme.copyWith(
primary: kShrinePink100,
onPrimary: kShrineBrown900,
secondary: kShrineBrown900,
error: kShrineErrorRed,
),
// TODO: Add the text themes (103)
// TODO: Decorate the inputs (103)
);
}
Now, set the theme:
at the end of ShrineApp's build()
function (in the MaterialApp widget) to be our new theme:
// TODO: Customize the theme (103)
theme: _kShrineTheme, // New code
Save your project. Your login screen should now look like this:
Android | iOS |
5. Modify typography and label styles
Besides the color changes, the designer has also given us specific typography to use. Flutter's ThemeData includes 3 text themes. Each text theme is a collection of text styles, like "headline" and "title". We'll use a few styles for our app and change some of the values.
Customize the text theme
To import fonts into the project, they have to be added to the pubspec.yaml file.
In pubspec.yaml, add the following immediately after the flutter:
tag:
# TODO: Insert Fonts (103)
fonts:
- family: Rubik
fonts:
- asset: fonts/Rubik-Regular.ttf
- asset: fonts/Rubik-Medium.ttf
weight: 500
Now you can access and use the Rubik font.
Troubleshooting the pubspec file
You may get errors in running pub get if you cut and paste the declaration above. If you get errors, start by removing the leading whitespace and replacing it with spaces using 2-space indentation. (Two spaces before
fonts:
, four spaces before
family: Rubik
, and so on.)
If you see Mapping values are not allowed here, check the indentation of the line that has the problem and the indentation of the lines above it.
In login.dart
, change the following inside Column()
:
Column(
children: <Widget>[
Image.asset('assets/diamond.png'),
const SizedBox(height: 16.0),
Text(
'SHRINE',
style: Theme.of(context).textTheme.headlineSmall,
),
],
)
In app.dart
, add the following after _buildShrineTheme()
:
// TODO: Build a Shrine Text Theme (103)
TextTheme _buildShrineTextTheme(TextTheme base) {
return base
.copyWith(
headlineSmall: base.headlineSmall!.copyWith(
fontWeight: FontWeight.w500,
),
titleLarge: base.titleLarge!.copyWith(
fontSize: 18.0,
),
bodySmall: base.bodySmall!.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14.0,
),
bodyLarge: base.bodyLarge!.copyWith(
fontWeight: FontWeight.w500,
fontSize: 16.0,
),
)
.apply(
fontFamily: 'Rubik',
displayColor: kShrineBrown900,
bodyColor: kShrineBrown900,
);
}
This takes a TextTheme and changes how the headlines, titles, and captions look.
Applying the fontFamily
in this way applies the changes only to the typography scale values specified in copyWith()
(headline, title, caption).
For some fonts, we're setting a custom fontWeight, in increments of 100: w500 (the 500 weight) corresponds to medium and w400 corresponds to regular.
Use the new text themes
Add the following themes to _buildShrineTheme
after error:
// TODO: Add the text themes (103)
textTheme: _buildShrineTextTheme(base.textTheme),
textSelectionTheme: const TextSelectionThemeData(
selectionColor: kShrinePink100,
),
Save your project. This time, also restart the app (known as Hot Restart), since we modified fonts.
Android | iOS |
Text in the login and home screens look different—some text uses the Rubik font, and other text renders in brown, instead of black or white. Icons also render in brown.
Shrink the text
The labels are too big.
In home.dart
, change the children:
of the innermost Column:
// TODO: Change innermost Column (103)
children: <Widget>[
// TODO: Handle overflowing labels (103)
Text(
product.name,
style: theme.textTheme.button,
softWrap: false,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 4.0),
Text(
formatter.format(product.price),
style: theme.textTheme.bodySmall,
),
// End new code
],
Center and drop the text
We want to center the labels, and align the text to the bottom of each card, instead of the bottom of each image.
Move the labels to the end (bottom) of the main axis and change them to be centered::
// TODO: Align labels to the bottom and center (103)
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
Save your project.
Android | iOS |
That looks much better.
Theme the text fields
You can also theme the decoration on text fields with an InputDecorationTheme.
In app.dart
, in the _buildShrineTheme()
method, specify an inputDecorationTheme:
value:
// TODO: Decorate the inputs (103)
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
Right now, the text fields have a filled
decoration. Let's remove that. Removing filled
and specifying the inputDecorationTheme
will give the text fields the outline style.
In login.dart
, remove the filled: true
values:
// Remove filled: true values (103)
TextField(
controller: _usernameController,
decoration: const InputDecoration(
// Removed filled: true
labelText: 'Username',
),
),
const SizedBox(height: 12.0),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
// Removed filled: true
labelText: 'Password',
),
obscureText: true,
),
Hot restart. Your login screen should look like this when the Username field is active (when you're typing in it):
Android | iOS |
Type into a text field—the borders and floating labels render in the primary color. But we can't see it very easily. It's not accessible to people who have trouble differentiating pixels that don't have a high enough color contrast. (For more information, see the Material Guidelines Color & Accessibility article.)
In app.dart
, specify a focusedBorder:
under inputDecorationTheme:
:
// TODO: Decorate the inputs (103)
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
),
Next, specify a floatingLabelStyle:
under inputDecorationTheme:
:
// TODO: Decorate the inputs (103)
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
floatingLabelStyle: TextStyle(
color: kShrineBrown900,
),
),
Finally, let's have the Cancel button use the secondary color rather than the primary for increased contrast.
TextButton(
child: const Text('CANCEL'),
onPressed: () {
_usernameController.clear();
_passwordController.clear();
},
style: TextButton.styleFrom(
primary: Theme.of(context).colorScheme.secondary,
),
),
Save your project.
Android | iOS |
6. Adjust elevation
Now that you've styled the page with specific color and typography that matches Shrine, let's adjust elevation.
Change the elevation of the NEXT button
The default elevation for an ElevatedButton
is 2. Let's raise it higher.
In login.dart
, add an style:
value to the NEXT ElevatedButton:
ElevatedButton(
child: const Text('NEXT'),
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
foregroundColor: kShrineBrown900,
backgroundColor: kShrinePink100,
elevation: 8.0,
),
),
Save your project.
Android | iOS |
Adjust Card elevation
Right now, the cards lay on a white surface next to the site's navigation.
In home.dart
, add an elevation:
value to the Cards:
// TODO: Adjust card heights (103)
elevation: 0.0,
Save the project.
Android | iOS |
You've removed the shadow under the cards.
7. Add Shape
Shrine has a cool geometric style, defining elements with an octagonal or rectangular shape. Let's implement that shape styling in the cards on the home screen, and the text fields and buttons on the login screen.
Change the text field shapes on the login screen
In app.dart
, import the following file:
import 'supplemental/cut_corners_border.dart';
Still in app.dart
, modify the text field decoration theme to use a cut corners border:
// TODO: Decorate the inputs (103)
inputDecorationTheme: const InputDecorationTheme(
border: CutCornersBorder(),
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
floatingLabelStyle: TextStyle(
color: kShrineBrown900,
),
),
Change button shapes on the login screen
In login.dart
, add a beveled rectangular border to the CANCEL button:
TextButton(
child: const Text('CANCEL'),
onPressed: () {
_usernameController.clear();
_passwordController.clear();
},
style: TextButton.styleFrom(
foregroundColor: kShrineBrown900,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
),
),
The TextButton has no visible shape, so why add a border shape? So the ripple animation is bound to the same shape when touched.
Now add the same shape to the NEXT button:
ElevatedButton(
child: const Text('NEXT'),
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
foregroundColor: kShrineBrown900,
backgroundColor: kShrinePink100,
elevation: 8.0,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
),
),
To change the shape of all buttons, we can also use elevatedButtonTheme
or textButtonTheme
in app.dart
. That is left as a challenge to the learner!
Hot restart.
Android | iOS |
8. Change the layout
Next, let's change the layout to show the cards at different aspect ratios and sizes, so that each card looks unique from the others.
Replace GridView with AsymmetricView
We've already written the files for an asymmetrical layout.
In home.dart
, add the following import:
import 'supplemental/asymmetric_view.dart';
Delete _buildGridCards
and replace the body
:
body: AsymmetricView(
products: ProductsRepository.loadProducts(Category.all),
),
Save the project.
Android | iOS |
Now the products scroll horizontally in a woven-inspired pattern.
9. Try another theme (Optional)
Color is a powerful way to express your brand, and a small change in color can have a large effect on your user experience. To test this out, let's see what Shrine looks like if the brand's color scheme were slightly different.
Modify colors
In colors.dart
, add the following color:
const kShrinePurple = Color(0xFF5D1049);
In app.dart
, change the _buildShrineTheme()
function to the following:
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light();
return base.copyWith(
colorScheme: base.colorScheme.copyWith(
primary: kShrinePurple,
secondary: kShrinePurple,
error: kShrineErrorRed,
),
scaffoldBackgroundColor: kShrineSurfaceWhite,
textSelectionTheme: const TextSelectionThemeData(
selectionColor: kShrinePurple,
),
appBarTheme: const AppBarTheme(
foregroundColor: kShrineBrown900,
backgroundColor: kShrinePink100,
),
inputDecorationTheme: const InputDecorationTheme(
border: CutCornersBorder(),
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrinePurple,
),
),
floatingLabelStyle: TextStyle(
color: kShrinePurple,
),
),
);
}
Hot restart. The new theme should now appear.
Android | iOS |
Android | iOS |
The result is very different! Let's revert app.dart's
_buildShrineTheme
to what it was before this step. Or download 104's starter code.
10. Congratulations!
By now, you've created an app that resembles the design specifications from your designer.
Next steps
You've now used the following Material Flutter: theme, typography, elevation, and shape. You can explore more components and subsystems in the Material Flutter library.
Dig into the files in the supplemental
directory to learn how we made the horizontally scrolling, asymmetric layout grid.
What if your planned app design contains elements that don't have components in the library? In MDC-104: Material Advanced Components we show how to create custom components using the Material Flutter library to achieve a desired look.