Skip to content

Feature Request: a more readable WidgetStateProperty constructor that maintains flexibility #146042

@nate-thegrate

Description

@nate-thegrate

As explained in a Decoding Flutter video, the WidgetStateProperty class (which until recently was called MaterialStateProperty) allows for styling based on multiple different factors without an enormous parameter list.


Problem

Let's look at some common use cases:

  • Check WidgetStates one at a time
  • Check multiple WidgetStates to see if any of them match
  • Check multiple WidgetStates to see if all of them match
code snippet (click to collapse)
// one at a time
WidgetStateProperty.resolveWith<Color>((states) {
  if (states.contains(WidgetState.disabled)) {
    return Colors.grey;
  } else if (states.contains(WidgetState.error)) {
    return Colors.red;
  } else if (states.contains(WidgetState.pressed)) {
    return Colors.blueAccent;
  } else if (states.contains(WidgetState.focused)) {
    return Colors.blue;
  } else if (states.contains(WidgetState.hovered)) {
    return Colors.blueGrey;
  }
  return Colors.black;
});

const activeStates = [
  WidgetState.pressed,
  WidgetState.hovered,
  WidgetState.focused,
];

// see if any match
WidgetStateProperty.resolveWith<Color>((states) {
  if (activeStates.any(states.contains)) {
    if (states.contains(WidgetState.error) {
      return Colors.red;
    }
    return Colors.blue;
  }
  return Colors.black;
});

// see if all match
WidgetStateProperty.resolveWith<Color>((states) {
  if (states.containsAll(activeStates)) {
    if (states.contains(WidgetState.error) {
      return Colors.red;
    }
    return Colors.blue;
  }
  return Colors.black;
});

Pros

  • effective

Cons

  • ugly

With 8 different enum values, we have 2⁸ different Set<WidgetState> possibilities, so finding a WidgetPropertyResolver implementation that isn't messy or limited in scope is challenging.


Solutions

Set pattern matching

code snippet
// one at a time
WidgetStateProperty.resolveWith<Color>((states) => switch (states) {
  {WidgetState.disabled} => Colors.grey,
  {WidgetState.error}    => Colors.red,
  {WidgetState.pressed}  => Colors.blueAccent,
  {WidgetState.focused}  => Colors.blue,
  {WidgetState.hovered}  => Colors.blueGrey,
  _                      => Colors.black,
});

// see if any match
WidgetStateProperty.resolveWith<Color>((states) => switch (states) {
  {WidgetState.pressed || WidgetState.hovered || WidgetState.focused, WidgetState.error} => Colors.red,
  {WidgetState.pressed || WidgetState.hovered || WidgetState.focused} => Colors.blue,
  _ => Colors.black,
});

// see if all match
WidgetStateProperty.resolveWith<Color>((states) => switch (states) {
  {WidgetState.pressed, WidgetState.hovered, WidgetState.focused, WidgetState.error} => Colors.red,
  {WidgetState.pressed, WidgetState.hovered, WidgetState.focused} => Colors.blue,
  _ => Colors.black,
});

Pros

  • super clean & readable 😍

Cons

  • This isn't supported in the Dart language 😢
    There's an open issue for it though; maybe we could just keep our fingers crossed.

Convert to Map

T resolveWithMap<T>(T function(Map<WidgetState, bool> resolve)) => resolveWith((states) {
  final stateMap = {for (final state in WidgetStates) state: states.contains(state)};
  return resolve(stateMap);
});
code snippet
// one at a time
WidgetStateProperty.resolveWithMap<Color>((stateMap) => switch (stateMap) {
  {WidgetState.disabled: true} => Colors.grey,
  {WidgetState.error:    true} => Colors.red,
  {WidgetState.pressed:  true} => Colors.blueAccent,
  {WidgetState.focused:  true} => Colors.blue,
  {WidgetState.hovered:  true} => Colors.blueGrey,
  _                            => Colors.black,
});

// see if any match
WidgetStateProperty.resolveWithMap<Color>((stateMap) {
  switch (stateMap) {
    case {WidgetState.pressed: true}:
    case {WidgetState.hovered: true}:
    case {WidgetState.focused: true}:
      if (states.contains(WidgetState.error) {
        return Colors.red;
      }
      return Colors.blue;
    default:
      return Colors.black;
  }
});

// see if all match
WidgetStateProperty.resolveWithMap<Color>((stateMap) {
  switch (stateMap) {
    case {WidgetState.pressed: false}:
    case {WidgetState.hovered: false}:
    case {WidgetState.focused: false}:
      return Colors.black;
    case {WidgetState.error: true}:
      return Colors.red;
    default:
      return Colors.blue;
  }
});

Pros

  • Works with the current pattern matching features
  • The ability to check for either true or false helps avoid repetition

Cons

  • A tinge of sadness—in most situations, it's not quite as concise as Set pattern matching would be

Enhanced enum

Instead of just trying to improve the way we parse the set, we could also try to improve WidgetState directly.

enum WidgetState with SpicyMixin { ... }
code snippet
// one at a time
WidgetStateProperty.map<Color>({
  WidgetState.disabled: Colors.grey,
  WidgetState.error:    Colors.red,
  WidgetState.pressed:  Colors.blueAccent,
  WidgetState.focused:  Colors.blue,
  WidgetState.hovered:  Colors.blueGrey,
  WidgetState.any:      Colors.black, // "any" is a static const member, not an enum value
});

final anyActive = WidgetState.pressed | WidgetState.hovered | WidgetState.focused;

// see if any match
WidgetStateProperty.map<Color>({
  anyActive & WidgetState.error: Colors.red,
  anyActive: Colors.blue,
  ~anyActive: Colors.black,
});

final allActive = WidgetState.pressed & WidgetState.hovered & WidgetState.focused;

// see if all match
WidgetStateProperty.map<Color>({
  allActive & WidgetState.error: Colors.red,
  allActive: Colors.blue,
  ~allActive: Colors.black,
});

Pros

  • Even more concise and readable than Set pattern matching
  • Even more flexible than Map pattern matching
  • Syntax is similar to named arguments

Cons

  • The syntax still feels kinda wonky, though.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterframeworkflutter/packages/flutter repository. See also f: labels.r: fixedIssue is closed as already fixed in a newer versionteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions