Material Components (MDC) helps developers execute Material Design. Developed by a core team of engineers and UX designers at Google, MDC includes the same visual components Google uses in its apps. MDC has over 10 beautiful and functional Android components and is available also for iOS and Web.

This codelab will show you some situations where MDC can make your app more functional and more beautiful while saving you engineering time.

What you will build

In this codelab, you're going to complete Shrine, an ecommerce app that sells clothing, home goods, and popsicles. Your app will:

  • Have an animated, stretchable app bar.
  • Allow users to navigate between categories using the bottom navigation pattern
  • Have a grid layout with adjusted density based on screen size

Components to use

What you'll need

This codelab is focused on using Material Components. There are mentions of non-relevant concepts and code blocks for you to simply copy and paste.

Download the codelab starter app

Download starter app

Or clone it from GitHub

git clone https://github.com/material-components/material-components-android.git

Import the project

  1. Open Android Studio and select Import Project
  2. Navigate to the starter directory for building-beautiful-apps and select it. (the complete folder has the finished app.)
  3. Click OK

Run the starter app

  1. Make sure the configuration to the left of the Run / Play button is ‘app'.
  2. Press the Run / Play button.
  3. Select a device in the deployment target dialog.
  4. Click OK
  5. Android Studio builds the app, deploys it, and automatically opens it on the target device.

Voila! Shrine is running on your device or emulator. Scroll through the list of products on the app and click on them. But the app is clearly not finished yet.

What's a Collapsing App Bar?

Let's say the designer gave you the following mocks and you're going to update the app to match.

Making a static layout that looks like the mock on the left can be done with simple layout classes built into the Android framework, and the mock on the right can be done with the Toolbar class, but how do you transition between those two states and handle the gestural input properly?

AppBarLayout, together with CollapsingToolbarLayout and CoordinatorLayout, can do that: CoordinatorLayout notifies its children of scroll events and changing positions, AppBarLayout provides configurable behavior based on scroll position, and CollapsingToolbarLayout allows you to configure how views move between an expanded and collapsed size. When it gets to a minimum height, say 56dp, it freezes in place and allows the scrolling View (such as a NestedScrollView or RecyclerView) to scroll beneath it! It even adds a shadow.

Add a Material Components for Android to your project

In the app module's build.gradle file, update the dependency section to add a dependency for Material Components for Android:

app/build.gradle

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support:cardview-v7:25.3.1'
    compile 'com.android.support:recyclerview-v7:25.3.1'
    compile 'com.android.volley:volley:1.0.0'
    compile 'com.google.code.gson:gson:2.2.4'

    // Add this:
    compile 'com.android.support:design:25.3.1'
}

After editing the file, make sure to sync the project to these changes so that all the necessary libraries can be installed. This can be done by choosing "Sync Project with Gradle Files" from the Tools -> Android menu.

Add a CoordinatorLayout to MainActivity

CoordinatorLayout acts like a FrameLayout, but allows its children to react to changes in dependent views, such as scrolling or other movement. To take advantage of these features, use a CoordinatorLayout as the top-level container.

Replace the FrameLayout in MainActivity's layout file with a CoordinatorLayout:

app/res/layout/shr_main.xml

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- ... -->

</android.support.design.widget.CoordinatorLayout>

Make sure that you include the xmlns:app attribute when making this replacement, as we'll need the app namespace to use attributes from these widgets.

Wrap the Toolbar in an AppBarLayout and CollapsingToolbarLayout

As the main content moves, AppBarLayout and CollapsingToolbarLayout will work with the scrolling RecyclerView to adjust toolbar content.

Wrap the existing Toolbar tag in MainActivity's layout file with a CollapsingToolbarLayout inside of an AppBarLayout:

app/res/layout/shr_main.xml

<!-- ... -->
<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="400dp">

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/collapsing_toolbar"
        style="@style/Widget.Shrine.CollapsingToolbar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">


        <!-- Wrap this view: -->
        <android.support.v7.widget.Toolbar
            android:id="@+id/app_bar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"/>

    </android.support.design.widget.CollapsingToolbarLayout>

</android.support.design.widget.AppBarLayout>
<!-- ... -->

The AppBarLayout's height determines the height of the app bar in its expanded state. The Toolbar's height determines the height of the app bar in its collapsed state. In this case, this means that our app bar will collapse from 400dp to actionBarSize (56dp on smaller devices like phones, 64dp on larger devices like tablets).

The layout_scrollFlags tell the AppBarLayout how that child should behave during scrolling. In this case, layout_scrollFlags specifies that scrolling events will be consumed by this View until the CollapsingToolbarLayout reaches its minimum height, and that it should snap to be fully expanded or fully collapsed once scrolling stops (and never be stuck in a middle state).

There's another detail we need to get the scroll behavior correct: specify to the CollapsingToolbarLayout that you want to keep the Toolbar itself on the screen at all times. Add a layout_collapseMode attribute to the Toolbar tag:

app/res/layout/shr_main.xml

<!-- ... -->
<!-- Add attribute to this view: -->
<android.support.v7.widget.Toolbar
    android:id="@+id/app_bar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    app:layout_collapseMode="pin"/>
<!-- ... -->

One last thing: tell CoordinatorLayout that the RecyclerView's scroll events should notify AppBarLayout. CoordinatorLayout Behaviors can respond to a number of different user interactions, including scrolls and flings (very fast scrolling). We need a Behavior implementation that intercepts scrolling events and forwards them to our AppBarLayout for handling, so that it can potentially use them to expand or collapse our app bar. Fortunately, there is a Behavior implementation written just for this purpose: appbar_scrolling_view_behavior. Add this Behavior implementation to the RecyclerView in MainActivity's layout file and remove the top margin. CoordinatorLayout will handle the vertical offset automatically.

app/res/layout/shr_main.xml

<!-- ... -->
<!-- Add attribute to this view: -->
<android.support.v7.widget.RecyclerView
    android:id="@+id/product_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="@dimen/shr_list_margin"
    android:layout_marginRight="@dimen/shr_list_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<!-- ... -->

Remove the v21-specific styles for the Toolbar. CollapsingToolbarLayout will handle adding the necessary elevation as the toolbar is collapsed. Delete the Widget.Shrine.Toolbar style from the values-v21/styles.xml file. Note that in Android Studio, all the styles.xml files will be grouped under a single entry. You can expand this entry to select the (v21) version and make this change on it.

app/res/values-v21/styles.xml

<!-- ... -->
<!-- Delete this v21 style override: -->
<!--
<style name="Widget.Shrine.Toolbar" parent="Widget.AppCompat.Toolbar">
    <item name="android:background">?attr/colorPrimary</item>
    <item name="android:elevation">4dp</item>
    <item name="contentInsetStart">12dp</item>
    <item name="titleTextAppearance">@style/TextAppearance.Shrine.Logo</item>
</style>
-->
<!-- ... -->

Remove the background color from the main Toolbar styles, as it will interfere with the collapse animation:

app/res/values/styles.xml

<!-- ... -->
<style name="Widget.Shrine.Toolbar" parent="Widget.AppCompat.Toolbar">
    <!-- Delete this style item: -->
    <!--
    <item name="android:background">?attr/colorPrimary</item>
    -->
    <item name="contentInsetStart">12dp</item>
    <item name="titleTextAppearance">@style/TextAppearance.Shrine.Logo</item>
</style>
-->
<!-- ... -->

Uncomment the predefined styles for the collapsing toolbar and its children:

app/res/values/styles.xml

<!-- ... -->

<!-- Uncomment these: -->
<style name="Widget.Shrine.CollapsingToolbar" parent="">
    <item name="collapsedTitleTextAppearance">@style/TextAppearance.Shrine.Logo</item>
    <item name="contentScrim">?attr/colorPrimary</item>
    <item name="expandedTitleGravity">top</item>
    <item name="expandedTitleMarginStart">124dp</item>
    <item name="expandedTitleMarginTop">140dp</item>
    <item name="expandedTitleTextAppearance">@style/TextAppearance.Shrine.Logo</item>
    <item name="scrimVisibleHeightTrigger">140dp</item>
</style>

<style name="Widget.Shrine.CollapsingToolbarImage" parent="">
        <item name="android:layout_marginTop">48dp</item>
    <item name="android:layout_marginLeft">-124dp</item>
    <item name="android:adjustViewBounds">true</item>
</style>

<style name="Widget.Shrine.CollapsingToolbarContent" parent="">
    <item name="android:layout_marginTop">160dp</item>
    <item name="android:layout_marginLeft">124dp</item>
    <item name="android:layout_marginRight">16dp</item>
    <item name="android:layout_gravity">top</item>
    <item name="android:orientation">vertical</item>
</style>

<!-- ... -->

Run the app:

Now, there is a nice, large landing page header that slides away, condensing into just the logo in the app bar. However, we can make better use of this enlarged content area.

Add More Content to the Expanded App Bar

To make good use of this expanded App Bar real estate, include some larger, immersive content that will collapse and fade out as the user scrolls. CollapsingToolbarLayout has built-in support. To use it, add the content as children of the CollapsingToolbarLayout and specify their behavior as layout_* flags.

First, let's add a large photo that will collapse slightly slower than its container, making it seem as if it's further away from the user than the content in front of it. Add a Volley NetworkImageView as the first child of the CollapsingToolbarLayout:

app/res/layout/shr_main.xml

<!-- ... -->
<android.support.design.widget.CollapsingToolbarLayout
    android:id="@+id/collapsing_toolbar"
    style="@style/Widget.Shrine.CollapsingToolbar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">


    <!-- Add this view -->
    <com.android.volley.toolbox.NetworkImageView
        android:id="@+id/app_bar_image"
        style="@style/Widget.Shrine.CollapsingToolbarImage"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:layout_collapseMode="parallax"
        app:layout_collapseParallaxMultiplier="0.75"/>

    <android.support.v7.widget.Toolbar
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_collapseMode="pin"/>

</android.support.design.widget.CollapsingToolbarLayout>
<!-- ... -->

NetworkImageView allows us to load images from a URL, but CollapsingToolbarLayout will work just as well with a stock ImageView class (or any other View type). The important piece is adding the layout_collapseMode and layout_collapseParallaxMultiplier attributes. A layout_collapseMode of parallax tells the CollapsingToolbarLayout that this view will collapse at a slower or faster rate (specified by layout_collapseParallaxMultipler) than its container.

You can also add the text content shown in the mocks. Add a LinearLayout with two TextViews (one for the title and one for the description) inside the CollapsingToolbarLayout as well:

app/res/layout/shr_main.xml

<!-- ... -->
<android.support.design.widget.CollapsingToolbarLayout
    android:id="@+id/collapsing_toolbar"
    style="@style/Widget.Shrine.CollapsingToolbar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

    <com.android.volley.toolbox.NetworkImageView
        .../>

    <!-- Add these views: -->
    <LinearLayout
        style="@style/Widget.Shrine.CollapsingToolbarContent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_collapseMode="parallax"
        app:layout_collapseParallaxMultiplier="0.65">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/shr_product_category"
            android:textAppearance="@style/TextAppearance.AppCompat.Display2"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingTop="16dp"
            android:text="@string/shr_product_description"
            android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>

    </LinearLayout>

    <android.support.v7.widget.Toolbar
        .../>

</android.support.design.widget.CollapsingToolbarLayout>
<!-- ... -->

As a final step, give the new NetworkImageView some content. Add to MainActivity's onCreate method to select a product for the header, and make a network request for the product's associated image URL (obtained from our application's built-in product data) to get the image data:

app/java/MainActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.shr_main);

    Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
    setSupportActionBar(appBar);

    ArrayList<ProductEntry> products = readProductsList();
    ImageRequester imageRequester = ImageRequester.getInstance(this);

    // Add this code:
    ProductEntry headerProduct = getHeaderProduct(products);
    NetworkImageView headerImage = (NetworkImageView) findViewById(R.id.app_bar_image);
    imageRequester.setImageFromUrl(headerImage, headerProduct.url);

    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.product_list);
    recyclerView.setHasFixedSize(true);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    adapter = new ProductAdapter(products, imageRequester);
    recyclerView.setAdapter(adapter);

}

// Add this code as well:
private ProductEntry getHeaderProduct(List<ProductEntry> products) {
    if (products.size() == 0) {
        throw new IllegalArgumentException("There must be at least one product");
    }

    for (int i = 0; i < products.size(); i++) {
        if ("Perfect Goldfish Bowl".equals(products.get(i).title)) {
            return products.get(i);
        }
    }
    return products.get(0);
}

Run the app:

The large header is filled with eye-catching content that disappears as the user scrolls up.

Apps need a way to navigate between screens. The mocks use the bottom navigation pattern, which is good for apps with a small number of navigation destinations that are all of similar importance and prominence.

Material Components provides BottomNavigationView to implement this pattern. It functions much like a menu, and can be configured almost entirely via XML resources.

Add the view

BottomNavigationView needs to go on top of all the previously added content. Add it as the last child in MainActivity's CoordinatorLayout:

app/res/layout/shr_main.xml

<!-- ... -->
    </android.support.design.widget.AppBarLayout>

    <!-- Add this view -->
    <android.support.design.widget.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="#ffffff"
        android:theme="@style/ThemeOverlay.Shrine.BottomNavigation"
        app:menu="@menu/shr_bottom_navigation"/>

</android.support.design.widget.CoordinatorLayout>

The added BottomNavigationView element makes use of a theme overlay to change the tint color of the selected icons. Add the theme overlay definition to the main styles.xml file:

app/res/values/styles.xml

<!-- ... -->
    <style name="ThemeOverlay.Shrine.BottomNavigation" parent="">
        <item name="colorPrimary">?attr/colorAccent</item>
    </style>

</resources>

Theme overlays are a feature of Android that allow you to override specific theme attributes for a View and all of its children, while leaving the rest of the Activity or Application theme intact. This feature is often used for pieces of the UI that have a different color scheme than the rest of the app, such as app bars that have a dark color scheme that are displayed inside a light-themed application. The theme overlay applied to our BottomNavigationView will change the primary color to the accent color from our main theme (replacing the white color with a blue-grey one). Since BottomNavigationView uses the theme's current primary color for tinting the active navigation icon, this has the effect of making the active navigation icon a blue-grey color.

This view also makes use of a menu resource to specify which items, icons, and labels to display in the navigation. Add a new menu resource:

app/res/menu/shr_bottom_navigation.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/category_clothing"
        android:enabled="true"
        android:title="@string/shr_category_clothing"
        android:icon="@drawable/ic_favorite_vd_theme_24"
        app:showAsAction="ifRoom"/>
    <item
        android:id="@+id/category_home"
        android:enabled="true"
        android:title="@string/shr_category_home"
        android:icon="@drawable/ic_shrine_vd_theme_24"
        app:showAsAction="ifRoom"/>
    <item
        android:id="@+id/category_popsicles"
        android:enabled="true"
        android:title="@string/shr_category_popsicles"
        android:icon="@drawable/ic_shopping_cart_vd_theme_24"
        app:showAsAction="ifRoom"/>
</menu>

Make the magic happen

Users generally want their navigation buttons to take them somewhere. Attach some listeners to the view and handle those changes. BottomNavigationView provides two listeners for navigation events. OnNavigationItemSelectedListener lets you know when a new navigation item has been selected, so that you can change to the new category. OnNavigationItemReselectedListener lets you know when the currently selected navigation destination was reselected. A common way to handle this is to scroll to the top of the current category's items.

Since the app currently doesn't have a lot of possible destinations, simulate navigation by shuffling the product list. Add a method to shuffle the product list in MainActivity:

app/java/MainActivity.java

private void shuffleProducts() {
    ArrayList<ProductEntry> products = readProductsList();
    Collections.shuffle(products);
    adapter.setProducts(products);
}

Attach the OnNavigationItemSelectedListener first in MainActivity's onCreate. This will shuffle the list of products and scroll to the top:

app/java/MainActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    /* ... */

    BottomNavigationView bottomNavigation =
            (BottomNavigationView) findViewById(R.id.bottom_navigation);
    bottomNavigation.setOnNavigationItemSelectedListener(
            new BottomNavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            LinearLayoutManager layoutManager =
                        (LinearLayoutManager) recyclerView.getLayoutManager();
            layoutManager.scrollToPositionWithOffset(0, 0);

            shuffleProducts();
            return true;
        }
    });
}

Below that method call (still inside of MainActivity's onCreate), add the OnNavigationItemReselectedListener. This will scroll to the top of the list:

app/java/MainActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    /* ... */

    bottomNavigation.setOnNavigationItemReselectedListener(
            new BottomNavigationView.OnNavigationItemReselectedListener() {
        @Override
        public void onNavigationItemReselected(@NonNull MenuItem item) {
            LinearLayoutManager layoutManager =
                    (LinearLayoutManager) recyclerView.getLayoutManager();
            layoutManager.scrollToPositionWithOffset(0, 0);
        }
    });
}

Finishing Touches

Given the categories, it would best for users to start in the Home section. BottomNavigationView allows you to set the selected item programmatically. Initialize things to the right state in MainActivity's onCreate. Add a call to setSelectedItemId whenever the activity is starting fresh:

app/java/MainActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    /* ... */

    if (savedInstanceState == null) {
        bottomNavigation.setSelectedItemId(R.id.category_home);
    }
}

Run the app:

The bottom navigation is easy to reach on the screen and gives the user a smooth way to move around the app. By tapping on the currently selected navigation icon, the user can quickly scroll back to the top of the content — a great resource when reading long lists.

To create the card layout with increased density, move to a GridLayoutManager for the RecyclerView. This card layout also supports large screens.

Add resource values to define column width

You can select different values based on screen size with Android resources. Use Android resources to pick a different number of cards for smaller and larger screens.

Add a new integers.xml file in app's values resources folder:

app/res/values/integers.xml

<resources>
    <integer name="shr_column_count">2</integer>
</resources>

This new integer resource will be the default column count, if none of the other possible qualifiers apply. Next, add some special qualifier values for larger screens. Add another integers.xml file with a qualifier for Screen Width >= 480dp:

app/res/values-w480dp/integers.xml

<resources>
    <integer name="shr_column_count">3</integer>
</resources>

If the screen width ever grows larger than 480dp in landscape mode, the grid will display 3 columns instead of 2.

Add one for 960dp as well:

app/res/values-w960dp/integers.xml

<resources>
    <integer name="shr_column_count">6</integer>
</resources>

This will take care of devices that are much larger than the base size, such as large tablets.

Use GridLayout manager in the RecyclerView

Adjust MainActivity to use a GridLayoutManager and initialize it with the column count from the resources. Update MainActivity's onCreate to replace its LinearLayoutManager with a GridLayoutManager:

app/java/MainActivity.java

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
/* ... */

    final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.product_list);
    recyclerView.setHasFixedSize(true);
    recyclerView.setLayoutManager(
            new GridLayoutManager(this, getResources().getInteger(R.integer.shr_column_count)));
    adapter = new ProductAdapter(products, imageRequester);
    recyclerView.setAdapter(adapter);

/* ... */
}

Run the app:

GridLayoutManager makes it easy to create a layout with items of constant size.

Loading the column count from resources will take care of changing the density of content as screen size changes, such as when the user rotates their device or adjusts the size of an application in multi-window mode.

Try the app across rotations or maybe on a larger device or emulator to see how the layout changes.

We started with a basic ecommerce application, but gave it new life with Material Components for Android. Now there is a large, immersive app bar with eye-catching content that moves away when the user scrolls down to look at the content. The bottom navigation is easy to reach and makes it simple to navigate around the app. The grid adjusts its content density across various screen sizes. You and your designer can rest easy knowing your app is using components that are well tested and follow the Material Design guidelines.

Where to go from here

This simple example demonstrates a few important components, but Material Components for Android has more to offer. Take a look at the Material Design GitHub repository, which contains all of the code that makes up the library, as well as documentation for the components, and applications that demonstrate a wider variety of components and usage. Thank you for giving Material Components for Android a try, and we hoped you enjoyed this codelab!