DEV Community

Vinay Tiparadi
Vinay Tiparadi

Posted on

Implementing Android 12 Material You's Monet theme engine in Flutter

Introduction to Material You

The biggest change we saw in Android 12 is Material You, which is the latest version of Google's Material design language.
Google describes Material You as

"seeks to create designs that are personal for every style, accessible for every need, alive and adaptive for every screen."

During the development of Android 12, Google created a new theme engine code-named "monet" which generates a rich palette of pastel colors base on user's wallpaper. These colors are then applied to various parts of the system and their values are made available through an API that the userโ€™s applications can call, thus letting apps decide whether they also want to recolor their UI. Google has been going all-in on Material You, and the company has updated most of its apps to incorporate dynamic colors. The source code of "monet" is now made available to AOSP with the release of Android 12L. Previously the theme engine was Pixel exclusive.

Note: Monet wont't work on devices that are running android version which is older then 12.

Monet Theme Engine Example

Image Credits: material.io

You can learn more Google's new Material You and dynamic theme-engine here:


Prerequisites

  • Basic Flutter knowledge
  • Flutter 2.10.1 installed or later
  • A physical device running Android 12 or new version of OS with monet support. (Android 12 emulator doesn't support monet as of now)

While writing this post, i am using a device with Android 12L Custom Rom installed which is based on AOSP.

As "monet" source code is recently open-sourced, devices other than Pixels will be getting Android 12 with dyanamic theming support later this year.


Final result

In this tutorial we will be building a simple ID card application with Text() and FloatingActionButton() widgets to demonstrate implementation of monet.

Source code of this project is available here: GitHub

Screenshots

app screenshot 1

app screenshot 2

app screenshot 3

If you installed the app on Android OS < Android 12 then the app will look like this:

app screenshot 4


Let's get started!

  • Create the Flutter app

Open your Android Studio and create new Flutter project as usual or if you prefer command line then run the command given below in terminal (for vscode users)

flutter create id_card_monet
Enter fullscreen mode Exit fullscreen mode
  • Add dependendies

Open pubspec.yaml file which will be available inside your project folder. In this case id_card_monet/pubspec.yaml

We will be using "dynamic_color" package which is supported on flutter 2.10.1 and later.

dynamic_color package is published by material.io and it's available here.

Add dynamic_color: ^1.1.2 in dependencies section of pubspec.yaml file.

Your dependencies section shoud look like this:

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  dynamic_color: ^1.1.2
Enter fullscreen mode Exit fullscreen mode

After making changes to pubspec.yaml file, your next is to click on the popup saying Pub get on android studio or enter flutter pub get in terminal if you are using vscode. This will download all the required dependencies of your project.

Link to dynamic_color flutter package.

  • Import required dart package in main.dart

Navigate to id_card_monet/lib/main.dart file in your project and add this import import 'package:dynamic_color/dynamic_color.dart'; so that you will be able to use the widgets from dynamic_color.dart file.

  • Deleting the template code from main.dart

In the main.dart file you will see the template code provided by Flutter, we will write our custom widget tree so it's better to delete it.

After deleting the template code add the following code block:

void main() => runApp(const IdCard());
Enter fullscreen mode Exit fullscreen mode

If you getting errors like IdCard() function is not defined, just follow the tutorial and you will be good to go!

  • Creating a StatefulWidget

In main.dart file type stful and your IDE or editor will create a StatefulWidget and enter name of the StatefulWidget as 'IdCard'

If above method doesn't work for you, just copy paste from given code below:

class IdCard extends StatefulWidget {
  const IdCard({Key? key}) : super(key: key);

  @override
  State<IdCard> createState() => _IdCardState();
}

class _IdCardState extends State<IdCard> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
Enter fullscreen mode Exit fullscreen mode

We are using a StatefulWidget as state of one of our widget changes.

  • Use DynamicColorBuilder

Currently your _IdCardState looks like this

class _IdCardState extends State<IdCard> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
Enter fullscreen mode Exit fullscreen mode

Replace above code snippet with

class _IdCardState extends State<IdCard> {
  int age = 0;

  @override
  Widget build(BuildContext context) {
    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        return MaterialApp();
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We are returning DynamicColorBuilder() instead of Container().
DynamicColorBuilder is a stateful widget that provides the device's dynamic colors in a light and dark ColorScheme which are extracted from your wallpaper. Under the hood, DynamicColorBuilder uses a plugin to talk to the Android OS.

It builds the child widget of this widget, providing a light and dark ColorScheme.
The ColorSchemes will be null if dynamic color is not supported (i.e on non-Android platforms and pre-Android S devices), or if the colors have yet to be obtained.

  • Customizing Scffold and AppBar with dynamic colors

Replace _IdCardState with:

class _IdCardState extends State<IdCard> {
  int age = 0;

  @override
  Widget build(BuildContext context) {
    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        return MaterialApp(
          home: Scaffold(
            backgroundColor: darkDynamic?.background ?? Colors.white, // *1
            appBar: AppBar(
              title: Text(
                'ID Card',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  letterSpacing: 1.9,
                  color: darkDynamic?.onPrimaryContainer ?? Colors.white, // *2
                ),
              ),
              centerTitle: true,
              backgroundColor:
                  darkDynamic?.secondaryContainer.withAlpha(50) ?? Colors.blue, // *3
              elevation: 6.0,
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                setState(() {
                  age += 1;
                });
              },
              child: const Icon(
                Icons.add,
                color: Colors.black,
              ),
              backgroundColor: darkDynamic?.primary.withOpacity(1) ?? Colors.blue, // *4
            ),
            body: 
          ),
        );
      },
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

As we discussed we are getting lightDynamic and darkDynamic palette. We can use either of it or switch between them as Android OS switches from light mode to dark mode. In this app we are using darkDynamic palette.

Both the palette provides a lot of different colors which you can use in your app.

Some of them are:

  1. primary
  2. tertiary
  3. secondaryContainer
  4. secondary
  5. onPrimary
  6. background
  7. onPrimaryContainer and much more

You can access them via the $colorPaletteName and . (dot) operator
For eg. darkDynamic?.primary

You can learn about the specific use of each color and customizing your app from material.io

Now, coming to the code part
Refer comments from above snippet to understand which line i am talking about.

*1 backgroundColor: darkDynamic?.background ?? Colors.white,

For the backgroundColor of our Scaffold Widget we are using darkDynamic palette and background color extracted from our wallpaper. If the use is using Android OS older than Android 12 (S) or for some reasons it doesn't get dynamic palette, the scaffold background will be set 'white' color from material library.

*2 color: darkDynamic?.onPrimaryContainer ?? Colors.white,

This sets the color of AppBar title to onPrimary Container extracted from wallpaper and if dynamicPalette returns null, then AppBar title color is set to empty.

*3 darkDynamic?.secondaryContainer.withAlpha(50) ?? Colors.blue,

This is similar to above implementation but if you notice I have added alpha to the extracted secondaryContainer color. You can tweak the colors according to you.

There are several methods you can call and tweak colors:

  1. withOpacity(double opacity)
  2. withAlpha(int a)
  3. withGreen(int a)
  4. withRed(int a)
  5. withBlue(int a)
  6. harmonizeWith(Color color)

harmonizeWith method can be used to shift the hue of the color towards the passed in color

*4 backgroundColor: darkDynamic?.primary.withOpacity(1) ?? Colors.blue,

Here I have set the opacity to 1 which is the maximum opacity which you can pass and it's the default value.

  • Adding other widgets to our app

Replace the _IdCardState with code given below to complete our app ui.

class _IdCardState extends State<IdCard> {
  int age = 0;

  @override
  Widget build(BuildContext context) {
    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        return MaterialApp(
          home: Scaffold(
            backgroundColor: darkDynamic?.background ?? Colors.white, // *1
            appBar: AppBar(
              title: Text(
                'ID Card',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  letterSpacing: 1.9,
                  color: darkDynamic?.onPrimaryContainer ?? Colors.white, // *2
                ),
              ),
              centerTitle: true,
              backgroundColor:
                  darkDynamic?.secondaryContainer.withAlpha(50) ?? Colors.blue, // *3
              elevation: 6.0,
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                setState(() {
                  age += 1;
                });
              },
              child: const Icon(
                Icons.add,
                color: Colors.black,
              ),
              backgroundColor: darkDynamic?.primary.withOpacity(1) ?? Colors.blue, // *4
            ),
            body: Padding(
              padding: const EdgeInsets.fromLTRB(30, 40, 30, 30),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Text(
                    'NAME',
                    style: TextStyle(
                      color:
                          darkDynamic?.primary.withOpacity(0.9) ?? Colors.black, // *5
                      letterSpacing: 2,
                    ),
                  ),
                  const SizedBox(height: 10),
                  Text(
                    'Vinay :)',
                    style: TextStyle(
                      color: darkDynamic?.secondary ?? Colors.blue, // *6
                      letterSpacing: 2,
                      fontSize: 28.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 30),
                  Text(
                    'CURRENT AGE',
                    style: TextStyle(
                      color:
                          darkDynamic?.primary.withOpacity(0.9) ?? Colors.black, // *7
                      letterSpacing: 2,
                    ),
                  ),
                  const SizedBox(height: 10),
                  Text(
                    '$age',
                    style: TextStyle(
                      color: darkDynamic?.secondary ?? Colors.blue, // *8
                      letterSpacing: 2,
                      fontSize: 28.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 30),
                  Text(
                    'CONTACT',
                    style: TextStyle(
                      color:
                          darkDynamic?.primary.withOpacity(0.9) ?? Colors.black, // *9
                      letterSpacing: 2,
                    ),
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  Row(
                    children: <Widget>[
                      Icon(
                        Icons.email,
                        color: darkDynamic?.secondary ?? Colors.blue, // *10
                      ),
                      const SizedBox(width: 10),
                      Text(
                        'xyz@gmail.com',
                        style: TextStyle(
                          color: darkDynamic?.secondary ?? Colors.blue, // *11
                          letterSpacing: 1,
                          fontSize: 18,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}



Enter fullscreen mode Exit fullscreen mode

I have used Text(), SizedBox() and Icon() widgets to complete the UI.

*5, *6, *7, *8, *9, *10 and *11 uses the same logic as explained before to implements dynamic theming to your app.

  • Complete main.dart code
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';

void main() => runApp(const IdCard());

class IdCard extends StatefulWidget {
  const IdCard({Key? key}) : super(key: key);

  @override
  State<IdCard> createState() => _IdCardState();
}

class _IdCardState extends State<IdCard> {
  int age = 0;

  @override
  Widget build(BuildContext context) {
    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        return MaterialApp(
          home: Scaffold(
            backgroundColor: darkDynamic?.background ?? Colors.white, // *1
            appBar: AppBar(
              title: Text(
                'ID Card',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  letterSpacing: 1.9,
                  color: darkDynamic?.onPrimaryContainer ?? Colors.white, // *2
                ),
              ),
              centerTitle: true,
              backgroundColor: darkDynamic?.secondaryContainer.withAlpha(50) ??
                  Colors.blue, // *3
              elevation: 6.0,
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                setState(() {
                  age += 1;
                });
              },
              child: const Icon(
                Icons.add,
                color: Colors.black,
              ),
              backgroundColor:
                  darkDynamic?.primary.withOpacity(1) ?? Colors.blue, // *4
            ),
            body: Padding(
              padding: const EdgeInsets.fromLTRB(30, 40, 30, 30),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Text(
                    'NAME',
                    style: TextStyle(
                      color: darkDynamic?.primary.withOpacity(0.9) ??
                          Colors.black, // *5
                      letterSpacing: 2,
                    ),
                  ),
                  const SizedBox(height: 10),
                  Text(
                    'Vinay :)',
                    style: TextStyle(
                      color: darkDynamic?.secondary ?? Colors.blue, // *6
                      letterSpacing: 2,
                      fontSize: 28.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 30),
                  Text(
                    'CURRENT AGE',
                    style: TextStyle(
                      color: darkDynamic?.primary.withOpacity(0.9) ??
                          Colors.black, // *7
                      letterSpacing: 2,
                    ),
                  ),
                  const SizedBox(height: 10),
                  Text(
                    '$age',
                    style: TextStyle(
                      color: darkDynamic?.secondary ?? Colors.blue, // *8
                      letterSpacing: 2,
                      fontSize: 28.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 30),
                  Text(
                    'CONTACT',
                    style: TextStyle(
                      color: darkDynamic?.primary.withOpacity(0.9) ??
                          Colors.black, // *9
                      letterSpacing: 2,
                    ),
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  Row(
                    children: <Widget>[
                      Icon(
                        Icons.email,
                        color: darkDynamic?.secondary ?? Colors.blue, // *10
                      ),
                      const SizedBox(width: 10),
                      Text(
                        'xyz@gmail.com',
                        style: TextStyle(
                          color: darkDynamic?.secondary ?? Colors.blue, // *11
                          letterSpacing: 1,
                          fontSize: 18,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

If you made it till here then congrats!

Last step is to test your app with dyanamic theme on your device ๐Ÿ“ฑ

Visit this project on GitHub and โญ it so that you can refer it anytime.

Credits:
Abhishek Pandey for helping me.

Top comments (3)

Collapse
 
shavkatmoskvin profile image
ShavkatMoskvin

Thank you!

Collapse
 
at0m_03 profile image
ATOM

Great article i really learned a lot from it
Keep up! ๐Ÿ™

Collapse
 
vinaytiparadi profile image
Vinay Tiparadi

Thank you! ๐Ÿ˜Š