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.
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
If you installed the app on Android OS < Android 12 then the app will look like this:
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
- 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
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());
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();
}
}
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();
}
}
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();
},
);
}
}
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:
),
);
},
);
}
}
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:
- primary
- tertiary
- secondaryContainer
- secondary
- onPrimary
- background
- 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:
- withOpacity(double opacity)
- withAlpha(int a)
- withGreen(int a)
- withRed(int a)
- withBlue(int a)
- 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,
),
),
],
),
],
),
),
),
);
},
);
}
}
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,
),
),
],
),
],
),
),
),
);
},
);
}
}
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)
Thank you!
Great article i really learned a lot from it
Keep up! 🙏
Thank you! 😊