mix

repository·main·Indexed 21 days ago

https://github.com/conceptadev/mix

A styling system for Flutter that separates style definitions from widget structure, enabling composable, type-safe, and context-aware styles using a fluent API and design tokens. The ecosystem includes mix_chart for Line, Bar, and Pie charts with stable ID selection and animation support, as well as mix_generator for creating immutable Spec mixins, Styler classes, and WidgetModifiers via annotations like @MixableSpec, @MixableStyler, and @MixWidget.

Tokens
60.7K
Snippets
188
Records
247
Agent score
73%

What's inside mix

  1. Explore mix_chart API features via the gallery

    main

    The mix_chart gallery demonstrates several chart families and their capabilities:

    • Line Charts: Supports curves, area fills, per-series overrides, steps, gaps, widget axis labels, selection, custom tooltips, viewport transforms, and Mix animation.
    • Bar Charts: Supports grouped, stacked, floating, gradient, tracked, labeled, and selected bars.
    • Pie Charts: Supports pies, donuts, labels, badges, rounded slices, selection, tooltips, and safe empty data.
    • Playground: Provides live generated-Styler controls for all three chart families.
    • Dashboard: Demonstrates a polished responsive composition using the public API.

    Note: The gallery examples primarily use direct Flutter values. mix_chart does not define its own token registry; instead, it is designed to consume consumer-owned Mix tokens.

  2. What is mix_protocol?

    main

    mix_protocol is a versioned JSON wire protocol designed for representable Mix styles and token themes. It provides a mechanism to:

    • Decode untrusted JSON into real Mix stylers.
    • Encode supported runtime stylers into canonical JSON.
    • Export JSON Schema for use in authoring tools and CI.

    The protocol is intentionally narrow, focusing on a fixed v1 vocabulary for specific style types: box, text, flex, wrap, stack, icon, image, flex_box, wrap_box, and stack_box.

    Key Constraints:

    • Every top-level document must include v: 1.
    • Nested variant styles inherit the root version.
    • Explicit JSON null is forbidden.
    • Unsupported runtime values cause encoding to fail rather than being silently omitted.
  3. What is MixScope and how does theming work?

    main

    MixScope is an InheritedModel that acts as the central provider for design tokens and configuration in a Mix application. It allows descendant widgets to access type-safe design values (like colors, spacing, and text styles) dynamically via BuildContext.

    To optimize performance, MixScope uses Aspects to trigger targeted rebuilds:

    • tokens: Rebuilds when token values change.
    • modifierOrder: Rebuilds when the order of applied modifiers changes.

    Tokens are declared using specific classes (e.g., ColorToken, SpaceToken) and can be resolved either via the reference system token() or by passing context token.resolve(context).

    // Declaration
    const primary = ColorToken('color.primary');
    const spacingMd = SpaceToken('space.md');
    
    // Resolution
    final color1 = primary();                 // via reference system
    final color2 = primary.resolve(context);  // via BuildContext
  4. Understand the Mix Core Mental Model

    main

    Mix is a type-safe styling system for Flutter that separates style semantics from widgets. The core workflow follows a resolution pipeline:

    1. Spec: Immutable resolved data representing the final style.
    2. Styler: A fluent builder that uses Prop<V> to define properties.
    3. Widget: The UI component that renders the Spec.

    Resolution Pipeline: StyleWidgetStyleBuilder → merge active variants → resolve Prop<V> fields (tokens, Mix types, directives) → produce StyleSpec<S> → animate → widget.build(context, spec) → provide StyleSpec → apply widget modifiers.

    Spec (immutable resolved data) ← Styler (fluent builder with Prop<V>) → Widget (renders Spec)
  5. How the Mix token system works

    main

    The Mix framework uses a type-safe design token system based on the MixToken<T> abstract class. Tokens act as semantic identifiers (e.g., primaryColor) that are resolved to actual values (e.g., Colors.blue) at runtime using a MixScope provided in the widget tree.

    The Lifecycle of a Token:

    1. Definition: Create a constant token using a specific token type (e.g., ColorToken('primary')).
    2. Reference: Call the token using token() to get a reference (like ColorRef or DoubleRef) that can be used in styling utilities.
    3. Styling: Pass these references to stylers like BoxStyler().color(primaryColor()).
    4. Resolution: When the widget builds, MixScope.tokenOf<T>(token, context) looks up the actual value in the nearest MixScope provider.
    const primaryColor = ColorToken('primary');
    
    // 1. Create reference
    final colorRef = primaryColor(); // Returns ColorRef
    
    // 2. Use in styling 
    BoxStyler().color(colorRef)
    
    // 3. During resolution, token is extracted and resolved
    // MixScope resolves primaryColor to actual Color value
  6. Use WidgetModifier to wrap widgets

    main

    A WidgetModifier is a specialized Spec that wraps a rendered widget with additional Flutter widgets (such as Padding, Opacity, or Transform). Modifiers are defined at the style level using ModifierMix<S> and are resolved and applied during the StyleWidget resolution pipeline.

    abstract class WidgetModifier<Self extends WidgetModifier<Self>> extends Spec<Self> {
      Widget build(Widget child);
    }
  7. Implement theme-aware and responsive components

    main

    Mix enables several common theming patterns:

    1. Theme-aware Components: Use BoxStyler with tokens (e.g., AppTokens.surface()) to create components that automatically adapt to the current MixScope.
    2. Light/Dark Mode: Provide different maps to the colors parameter of MixScope based on the platformBrightness from MediaQuery.
    3. Responsive Tokens: Manually resolve tokens using MixScope.tokenOf(token, context) and apply logic (like scaling) based on screen width.
    4. Semantic Tokens: Define custom tokens with semantic names (e.g., ColorToken('action.primary')) to decouple your UI logic from specific color values.
    // Theme-aware Component
    class ThemedContainer extends StatelessWidget {
      const ThemedContainer({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Box(
          style: BoxStyler()
              .color(AppTokens.surface())
              .paddingAll(AppTokens.md())
              .borderRounded(AppTokens.rounded()),
          child: Text('Themed content'),
        );
      }
    }
    
    // Light/Dark Theme Support
    return MixScope(
      colors: isLight ? AppTheme.lightColors : AppTheme.darkColors,
      spaces: AppTheme.spacing,
      textStyles: AppTheme.typography,
      child: MaterialApp(
        home: HomePage(),
      ),
    );
  8. Compose advanced styles using .flow()

    main

    For advanced composition, use the .flow() method. This method replaces or merges a nested WrapStyler directly into the current styler.

    Note: The method is named .flow() because .wrap() is reserved for the Mix widget-modifier API. When using WrapBoxStyler, be aware of property ownership:

    • .alignment() and .clipBehavior() belong to the outer Box.
    • .wrapAlignment() and .wrapClipBehavior() belong to the inner Flutter Wrap.
  9. Use Design Tokens and MixScope for theming

    main

    To maintain design consistency, you can define reusable tokens (e.g., colors, spacing) and provide them to your widget tree using a MixScope.

    1. Define tokens using ColorToken or SpaceToken.
    2. Wrap your app (or a subtree) in a MixScope providing the actual values.
    3. Reference the tokens in your stylers by calling them as functions.
    final $primary = ColorToken('primary');
    final $spacingMd = SpaceToken('spacing.md');
    
    MixScope(
      colors: { $primary: Colors.blue },
      spaces: { $spacingMd: 16.0 },
      child: MyApp(),
    );
    
    // Usage in a style
    final style = BoxStyler()
        .color($primary())
        .paddingAll($spacingMd());
  10. Use Style and Styler for fluent styling

    main

    A Style<S> represents a collection of styling rules, while a Styler is the fluent builder used to construct these styles.

    Generated Stylers (extending MixStyler<ST, SP>) provide domain-specific and utility mixins to build styles declaratively:

    • Variants: onDark(), onLight(), breakpoint/platform variants via VariantStyleMixin.
    • Widget States: onHovered(), onPressed(), onFocused() via WidgetStateVariantMixin.
    • Animations: animate(), phaseAnimation(), keyframeAnimation() via AnimationStyleMixin.
    • Modifiers: wrap() via WidgetModifierStyleMixin.
    • Domain Mixins: SpacingStyleMixin, DecorationStyleMixin, BorderStyleMixin, etc.

    When you call methods on a Styler, it returns a new merged Style instance.

    // Example of how a Styler is structured
    class BoxStyler extends MixStyler<BoxStyler, BoxSpec>
        with
            BorderStyleMixin<BoxStyler>,
            BorderRadiusStyleMixin<BoxStyler>,
            // ... other mixins
     {
      final Prop<AlignmentGeometry>? $alignment;
      // ...
    
      // Public constructor wraps raw values into Props
      BoxStyler({
        AlignmentGeometry? alignment,
        EdgeInsetsGeometryMix? padding,
        // ...
      }) : super(...);
    }
  11. Use Implicit Animations for simple transitions

    main

    Implicit animations are the simplest way to animate in Mix. By calling .animate() on any Styler, Mix automatically interpolates between the old and new values whenever the state or variants change.

    Use this for:

    • Simple hover/press effects.
    • State transitions (e.g., changing a boolean in setState).
    • Variant-driven changes (e.g., using .onHovered(...)).
    // State-triggered example
    final style = BoxStyler()
        .color(Colors.black)
        .scale(appear ? 1 : 0.1)
        .animate(.easeInOut(1.s));
    
    // Variant-triggered example
    final style = BoxStyler()
        .scale(1)
        .onHovered(
          BoxStyler().scale(1.5),
        )
        .animate(.spring(800.ms));
  12. Use nested shorthand in typed contexts

    main

    When you are inside a typed context—such as a variant callback (onHovered), a state callback (onDark), or a typed parameter (.container(...))—you should use bare dot-shorthand without the type prefix. Do not use the explicit constructor inside these nested blocks.

    Common typed contexts include:

    • .container(.shadow(...))
    • .onHovered(.color(...))
    • `.onDisabled(.color(...))
    // CORRECT — bare shorthand in nested contexts
    style.onHovered(.color(Colors.blue))
    style.onDark(.color(Colors.white))
    style.onDisabled(.color(Colors.grey))
    
    // Nested chaining
    BoxStyler().color(Colors.blue)
      .onHovered(.shadow(.color(Colors.black12).blurRadius(10)))