If you are a Flutter Developer who has had to work with a custom design system from the Product team, you may understand how difficult it is to set up the custom theme in a flutter app.
Adding the different colors, fonts, spacing and other elements of the design system while keeping your code modular and maintainable takes a lot of effort especially when working with Flutter's default theming system.
Flutter Mix is a good package that helps simplify this process. We will take a look at the basics of this package to help understand how to make efficient use of it. If you are new to design systems or have no idea what it is all about, I suggest you take a look at this great article which also explains design systems in flutter in deeper detail.
In this short tutorial, we will be using the mix package to apply the theme to a small app that displays awesome quotes. We will take it step-by-step.
Step one: add the package
The Mix package is available on pub.dev and can be added to a project by running the following command in the terminal from your project's root directory: flutter pub add mix
Step two: create your design tokens.
Create a file to hold your design tokens. Design tokens are the different elements that make up the design system. These include the different colors, fonts, spacings, radii and breakpoints. For each of these, Mix provides classes to create the corresponding token namely: ColorToken
, TextStyleToken
, SpaceToken
, RadiusToken
and BreakpointToken
. The constructor for each takes in a String value which is the name of the token. This should normally be the same name as that defined in the design document. Create a file 'tokens.dart' and add the following code:
tokens.dart
// tokens for colors defined in the design system
const primaryColor = ColorToken('primary-color');
const secondaryColor = ColorToken('secondary-color');
const accentColor = ColorToken('accent-color');
// tokens for custom radii in design
const smallRadius = RadiusToken('small-radius');
const largeRadius = RadiusToken('large-radius');
// tokens for adding uniform spacing
const smallSpace = SpaceToken('small-space');
const largeSpace = SpaceToken('large-space');
// tokens to define the different textstyles available in our designs
const smallText = TextStyleToken('small-text');
const midText = TextStyleToken('mid-text');
const largeText = TextStyleToken('large-text');
To keep things simple, we have defined these as global variables but you may decide to have a class that holds them as class variables.
Step three: create your theme data class.
MixThemeData
is the class in which we define the different values for our tokens. It has five properties which are all maps that hold our tokens and their corresponding values.
Again, to keep things simple we will also define this class in our tokens.dart file. Update the file with the following code:
tokens.dart
final customThemeData = MixThemeData(colors: {
primaryColor: Colors.blue,
secondaryColor: Colors.red,
accentColor: Colors.blueAccent
}, radii: {
smallRadius: const Radius.circular(10),
largeRadius: const Radius.circular(16)
}, spaces: {
smallSpace: 15.0,
largeSpace: 30.0
}, textStyles: {
smallText: const TextStyle(
fontSize: 14,
),
midText: const TextStyle(fontSize: 16),
largeText: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
});
As you may have noticed, the properties of our class is a map of the already created design tokens and their values which should, again, come from the design specifications.
Step four: provide the data to your app.
In this step, we will be wrapping our MaterialApp/CupertinoApp with a MixTheme
widget which will make our design tokens accessible globally in our app. The MixTheme
widget accepts a data argument which will be our previously defined MixThemeData
object.
Update your main.dart file to wrap your app with a new widget as shown:
main.dart
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MixTheme(
data: customThemeData,
child: const MaterialApp(
title: 'Flutter Mix',
debugShowCheckedModeBanner: false,
home: HomePage(),
),
);
}
}
We will be creating the HomePage
widget in the next step.
Step five: create the basic page structure.
Our demo app is a simple one-page app with an Appbar, two Text widgets and three buttons. Add a new file, home_page.dart and create a stateful widget HomePage()
inside of it.
Mix provides its own set of primitive widgets (more on that later), but does not intend to replace flutter's own widgets with its primitives. However, we can apply our defined design tokens from Mix to flutter's widgets using the resolve() method which accepts a BuildContext
argument.
For example, to style our home page appbar, we update the scaffold of our home page with an appbar as follows:
home_page.dart
appbar: AppBar(
backgroundColor: accentColor.resolve(context),
title: Text(
"Great Quotes",
style: largeText.resolve(context).copyWith(color: Colors.white),
),
),
As you can see, to give the appbar a background color, we use the resolve() method on our accentColor variable and pass in the context. Also to style the title, we use the largeText token we created alongside the copyWith() function to add a color to the text. This further shows Mix's great flexibility in styling widgets.
Step six: reusable widgets.
In our app, we have three buttons which are of equal styles except for the colors. As such, we can have a resusable widget to represent the buttons. Moreover, in most cases, design systems will come with specs for things like your app buttons which means we should make those reusable.
Now create a new file custom_button.dart and add the following code:
custom_button.dart
class CustomButton extends StatelessWidget {
const CustomButton(
{super.key,
required this.onPressed,
required this.btnTitle,
this.shouldEnable = true,
this.customStyle});
final void Function() onPressed;
final String btnTitle;
final bool shouldEnable;
final Style? customStyle;
@override
Widget build(BuildContext context) {
return PressableBox(
onPress: onPressed,
style: customStyle ?? baseBtnStyle,
enabled: shouldEnable,
child: Center(child: StyledText(btnTitle)));
}
}
The widgets PressableBox
and StyledText
are some of the aforementioned primitive widgets built into Mix. You can see details on other available widgets here. These widgets can be used to add Mix's styling to widgets. Our customStyle
parameter is also of type Style
imported from Mix and is the basic class for applying styles to widgets. The PressableBox
can be likened to your normal button and its enabled
parameter is used to mark the button as either enabled or disabled and to apply styles depending on this state.
Step seven: widget variants.
Remember that our buttons are basically the same and only differentiated by the colors. Mix has the concept of Variants
which is used to provide variations to what is essentially the same component with minor visual differences. Rather than define a whole new style for a component, we can just create a variant and style a component based on that variant. There are two types of variants: the basic Variant
which we will be using now and the ContextVariant
which helps us create variations based on certain contextual properties. More on this here.
In our app, we assume the Previous
button to be the standard and will provide a variation for the Next
button. To do this, we simply create a variable of type Variant
and pass in a name in the constructor. Update your custom button file with:
custom_button.dart
const nextVariant = Variant('next-button');
Step eight: applying styles
Now we are set to apply styles to our custom button.
In Mix, styles are applied to widgets using the Style
class. The style class takes in as many arguments as we want. These arguments are the attributes or styles we wish to apply to the widget.
Recall that our custom button accepts an optional style, customStyle. If this is not provided then, style defaults to baseBtnStyle. We will create the baseBtnStyle to further understand how styles work.
Add the following to your custom_button.dart file:
custom_button.dart
final baseBtnStyle = Style(
$box.width(150),
$box.height(70),
$box.borderRadius.all.ref(smallRadius),
$box.border.all.color.ref(primaryColor),
$box.color.ref(primaryColor),
$text.style.ref(largeText),
$text.style.color(Colors.white),
$text.style.ref(largeText),
// style for any button marked as disabled
$on.disabled($box.color(Colors.grey), $box.border.all.color(Colors.grey)),
// style for any button with the next variant applied to it.
nextVariant($box.border.all.color.ref(secondaryColor),
$box.color.ref(secondaryColor)));
Here are a few points to note:
- We make use of utilities to apply styles such as $box.width() and $text.style(). Mix provides several of them and you can find more info on these here.
- The order of these utilities matter. The utility declared lower in the style will take precedence over the same utility declared higher.
- To define variant styles, we simply call the name of the variable we defined and pass in the utilities for that variant. Whenever this variant is applied to our style, the utilities passed in will be used.
- We can use our design tokens for styling in utilities or pass in new values. To use our design tokens, the ref() method is called on the utility and we pass in the name of our design token to be applied.
- The
$on.disabled
utility is applied when the button is marked as disabled as explained earlier with our custom button. Hence, there was no need to define a new variant for ourDelete this
button. We simply mark this as disabled and the defined style will be used for that. Also, buttons marked as disabled will not respond to user interactions like press, longPress etc.
With this, I hope you have been able to grasp the basic concepts of the Mix package and can already start seeing the benefits of using this package to style your flutter apps. There's more to Mix than was considered here and you may want to take a look at the official docs for more information. You can easily find that here.
Here is a link to the GitHub repository for the example app for any reference you may need.
Happy code digging!
Top comments (0)