Skip to Content
Mix 2.0 is in development! You can access the Mix 1.0 docs here.
DocsOverviewBest Practices

Best Practices

Follow these practices to keep your Mix code consistent and maintainable as your app grows.

These guidelines target scalability and maintainability in Mix, and will evolve with community feedback.

This walkthrough creates a button based on Shadcn’s Button  component, called CustomButton.

File Structure

A well-organized file structure is crucial for maintaining a clean and scalable codebase, especially when working with design systems in Flutter. By dividing the button component into separate files as shown below, we not only enhance readability but also promote modularity and maintainability:

      • button.dart
      • button.variant.dart
      • button.style.dart
  • button.dart - This file will contain the CustomButton class and define the component’s structure.

  • button.variant.dart - Contains the CustomButtonVariant class and all its Variant instances.

  • button.style.dart - In this file, the CustomButtonStyle class will be created, housing all Style instances related to the button.

Widget Structure

Use Mix’s styled widgets for component creation. These widgets are key in structuring and styling your component. For example, the structure of your Button might look like this:

class CustomButton extends StatelessWidget { const CustomButton({ super.key, required this.title, required this.onPress, }); final String title; final void Function() onPress; @override Widget build(BuildContext context) { return Pressable( onPress: onPress, child: Box( child: StyledText(title), ), ); } }

This example uses Mix’s styled widgets: Pressable, Box, and StyledText. See the full list in the StyleWidgets reference.

Defining Variants

Variants are a powerful feature in design systems, allowing for flexible and reusable component configurations. In our CustomButton example, we create two types of variants: CustomButtonType and CustomButtonSize.

class CustomButtonType extends Variant { const CustomButtonType._(super.name); static const primary = CustomButtonType._('custom.button.primary'); static const destructive = CustomButtonType._('custom.button.destructive'); static const link = CustomButtonType._('custom.button.link'); } class CustomButtonSize extends Variant { const CustomButtonSize._(super.name); static const medium = CustomButtonSize._('custom.button.medium'); static const large = CustomButtonSize._('custom.button.large'); }

By defining variants, we create a set of predefined styles that can be easily applied to the CustomButton. This approach offers several advantages:

  • Consistency: Variants allow you to maintain a simple API to style your component. You can easily apply a set of predefined styles to your component, ensuring consistency across your application.
  • Flexibility: They allow for customization without altering the core component logic. You can introduce new styles or modify existing ones without impacting the button’s basic functionality.
  • Ease of Use: With variants, the component’s API becomes more intuitive and easier to use, as developers can pick from a set of predefined options.

Now, let’s incorporate these variants into the CustomButton class to influence the component’s style:

class CustomButton extends StatelessWidget { const CustomButton({ super.key, required this.title, required this.onPress, this.type = CustomButtonType.primary, this.size = CustomButtonSize.large, }); final String title; final void Function() onPress; final CustomButtonType type; final CustomButtonSize size; @override Widget build(BuildContext context) { return Pressable( onPress: onPress, child: Box( child: StyledText(title), ), ); } }

Styling the component

The CustomButtonStyle class defines styles for each variant. Each method returns a Styler with the appropriate visual properties based on the active type and size.

class CustomButtonStyle { CustomButtonStyle(this.type, this.size); final CustomButtonType type; final CustomButtonSize size; BoxStyler container() { var style = BoxStyler().borderRadius(.circular(8)); // Apply size style = switch (size) { CustomButtonSize.medium => style.paddingX(16).paddingY(8), CustomButtonSize.large => style.paddingX(24).paddingY(16), }; // Apply type style = switch (type) { CustomButtonType.primary => style.color(Colors.black), CustomButtonType.destructive => style.color(Colors.redAccent), CustomButtonType.link => style.color(Colors.transparent), }; return style; } TextStyler label() { var style = TextStyler() .color(Colors.white) .fontWeight(.bold); // Apply size style = switch (size) { CustomButtonSize.medium => style.fontSize(14), CustomButtonSize.large => style.fontSize(18), }; // Apply type style = switch (type) { CustomButtonType.primary => style.color(Colors.white), CustomButtonType.destructive => style.color(Colors.white), CustomButtonType.link => style .color(Colors.black) .decoration(.underline), }; return style; } }

With these styles in place, update the CustomButton class to apply them:

class CustomButton extends StatelessWidget { const CustomButton({ super.key, required this.title, required this.onPress, this.type = CustomButtonType.primary, this.size = CustomButtonSize.large, }); final String title; final void Function() onPress; final CustomButtonType type; final CustomButtonSize size; @override Widget build(BuildContext context) { final style = CustomButtonStyle(type, size); return Pressable( onPress: onPress, child: Box( style: style.container(), child: StyledText( title, style: style.label(), ), ), ); } }

The CustomButton component is now ready to use, following Mix’s recommended structure and conventions.