MDC-103 Flutter: Material Theming with Color, Shape, Elevation, and Type

1. Introduction

logo_components_color_2x_web_96dp.png

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

Shrine login page, themed brown and pink

Shrine login page, themed brown and pink

Shrine product page, with a top app bar and an asymmetric, horizontally scrollable grid full of products, themed pink

Material Flutter components and subsystems in this codelab

  • Themes
  • Typography
  • Elevation
  • Image list

How would you rate your level of experience with Flutter development?

Novice Intermediate Proficient

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

  1. Open the project in your editor of choice.
  2. 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

unthemed Shrine login page

unthemed Shrine login page

Click "Next" to see the product page.

Android

iOS

unthemed Shrine product grid page

unthemed Shrine product grid page

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.

d0362cb45c565a8e.jpeg 470b0e1c2669ae2.png

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

pink and brown themed Shrine login page

pink and brown themed Shrine login page

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

Shrine product grid page with text themes applied

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

Shrine product grid page with different text alignment

Shrine product grid page with different text alignment

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

Shrine login page with username field focused

Shrine login page with username field focused

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

Shrine login page with accessible CANCEL button

Shrine login page with accessible CANCEL button

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

Shrine login page with elevated NEXT button

Shrine login page with elevated NEXT button

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

Shrine product grid page without elevation for each card

Shrine product grid page without elevation for each card

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

Shrine login page with shape theming applied

Shrine login page with shape theming applied

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

Shrine product page with asymmetric horizontally-scrollable layout

Shrine product page with asymmetric horizontally-scrollable layout

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

Shrine login page with a purple and pink theme

Shrine login page with a purple and pink theme

Android

iOS

Shrine product page with a pink theme

Shrine product page with a pink theme

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.

I was able to complete this codelab with a reasonable amount of time and effort

Strongly agree Agree Neutral Disagree Strongly disagree

I would like to continue using Material Components in the future

Strongly agree Agree Neutral Disagree Strongly disagree