Implementing design systems in flutter: Flutter Mix Primer.

codedigga

Shalom A.

Posted on September 19, 2024

Implementing design systems in flutter: Flutter Mix Primer.

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');
Enter fullscreen mode Exit fullscreen mode

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)
});
Enter fullscreen mode Exit fullscreen mode

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(),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

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),
        ),
      ),
Enter fullscreen mode Exit fullscreen mode

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)));
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

Here are a few points to note:

  1. 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.
  2. The order of these utilities matter. The utility declared lower in the style will take precedence over the same utility declared higher.
  3. 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.
  4. 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.
  5. 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 our Delete 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!

💖 💪 🙅 🚩
codedigga
Shalom A.

Posted on September 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related