Repository: flutter
Issue: API proposal: `WidgetStatePropertyOr<T>`
Labels: dependency: dart,a: annoyance,P2,c: tech-debt,team-framework,triaged-framework,dependency:dart-triaged

**TL;DR** at the bottom 🙂

<br>

# Problem

The current `WidgetStateProperty` API is fantastic—there's a [Decoding Flutter video](https://youtu.be/CylXr3AF3uU) with a great explanation of why—but it's also messy.

<br>

For every parameter that could potentially be a WidgetStateProperty, there are 3 possible types, as noted in [a recent Design Doc](https://flutter.dev/go/using-widget-state-property). Using the `Color` class as an example:

1. Some APIs only accept `Color`s
2. Some APIs only accept `WidgetStateProperty<Color>` objects
3. Some APIs can handle either a `Color` or a `WidgetStateProperty<Color>` being passed

<br>

Number 3 is possible thanks to an additional class declaration:

```dart
class WidgetStateColor extends Color implements WidgetStateProperty<Color> {
  // ...
}
```

Unfortunately, there's a long list of problems with classes like this one.

<br>

## Inaccurate documentation

#### copy/paste typos

- https://github.com/flutter/flutter/pull/151935

We needed the PR shown above because repeatedly copy/pasting near-identical logic & documentation is a slog: it's boring to write and boring to review, so mistakes are more likely to slip through.

> [!NOTE]
> At the time of writing, https://github.com/flutter/flutter/pull/151935 has landed in master but not stable, so the [api.flutter.dev docs](https://api.flutter.dev/flutter/widgets/WidgetStateTextStyle-class.html) are still showing inaccurate documentation.

#### `const` constructors

```dart
/// To define a `const` [WidgetStateTextStyle], you'll need to extend
/// [WidgetStateTextStyle] and override its [resolve] method.
```

Technically, that was never true, since global functions and static methods can be referenced in a constant context:

```dart
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  static TextStyle _favoriteStyle(Set<WidgetState> states) {
    if (states.contains(WidgetState.selected)) {
      return const TextStyle(color: Color(0xFF00FFFF));
    }
    return TextStyle(
      color: states.contains(WidgetState.disabled) ? Colors.black26 : Colors.black,
    );
  }

  @override
  Widget build(BuildContext context) {
    // defining a const WidgetStateTextStyle without extending the class!
    const widgetStateTextStyle = WidgetStateTextStyle.resolveWith(_favoriteStyle);

    // ...
  }
}
```

And on top of that, recently I was able to land a PR that adds another `const factory` constructor to WidgetStateTextStyle:

```dart
const widgetStateTextStyle = WidgetStateTextStyle.fromMap({
  WidgetState.focused:  TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
  WidgetState.disabled: TextStyle(color: Colors.grey),
  WidgetState.any:      TextStyle(color: Colors.black),
});
```

But even after the PR had been in review for over 2 months, neither I nor the reviewers noticed that the docs needed to be changed, since "why would anyone want to read through all of that?"

<br>

One more question about `const` constructors: why does `WidgetStateTextStyle` have a `const factory`, but `WidgetStateColor` doesn't?

```dart
  /// If used as a regular color, the color resolved in the default state (the
  /// empty set of states) will be used.
  factory WidgetStateColor.resolveWith(WidgetPropertyResolver<Color> callback) = _WidgetStateColor;
```

```dart
  /// If used as a regular text style, the style resolved in the default state (the
  /// empty set of states) will be used.
  const factory WidgetStateTextStyle.resolveWith(WidgetPropertyResolver<TextStyle> callback) = _WidgetStateTextStyle;
```

Answer: the [`WidgetStateTextStyle.resolveWith()`](https://main-api.flutter.dev/flutter/widgets/WidgetStateTextStyle/WidgetStateTextStyle.resolveWith.html) documentation is wrong.

```dart
final Color color = WidgetStateColor.resolveWith((_) => Colors.red);
print(color.value == Colors.red.value); // true

const emptyStyle = TextStyle();
const redText = TextStyle(color: Colors.red);
final TextStyle style = WidgetStateTextStyle.resolveWith((_) => redText);
print(style.color == redText.color); // false
print(style.color == emptyStyle.color); // true
```

<br>

## Unsafe types

The `num` class creates a beautiful type hierarchy.

<p align="center">
  <img 
    src="https://github.com/user-attachments/assets/e6940aab-3478-40cf-9d7f-bb93e5cc117c"
    alt="sealed num"
    width="50%"
  />
</p>

```dart
sealed class num {}
class int implements num {}
class double implements num {}
```

<br>

Imagine if this hierarchy were flipped upside down:

<p align="center">
  <img 
    src="https://github.com/user-attachments/assets/49a2357a-3656-4ed0-af05-50a7fc0dff3f"
    alt="upside down hierarchy"
    width="50%"
  />
</p>

```dart
class int {}
class double {}

/// The [num] class allows a [double] to be used anywhere that accepts an [int],
/// but this should only be done with classes that explicitly support
/// both integer and non-integer values.
class num extends int implements double {}

class MyClass {
  const MyClass(this.decimal, this.integer, this.either);

  /// A floating-point decimal.
  final double decimal;

  /// You could pass a [num] here, but it might cause an error!
  final int integer;

  /// This can be either an integer or a decimal.
  /// Passing an [int] works fine, but for a [double],
  /// you have to type out `num(3.14)` in order to avoid a syntax error.
  final int either;
}
```

<br>

Replace `int`, `double`, and `num` with `Color`, `WidgetStateProperty<Color>` and `WidgetStateColor` respectively, and that's what we're currently working with.

From the `WidgetStateColor` docs:

```dart
/// [WidgetStateColor] should only be used with widgets that document
/// their support, like [TimePickerThemeData.dayPeriodColor].
```

<br>

It'd be great if, instead of `WidgetStateColor` / `WidgetStateTextStyle` / `WidgetStateMouseCursor` / `WidgetStateBorderSide`, we had 1 generic type to cover all the bases.

It'd also be great to do something about the type hierarchy:

<p align="center">
  <img 
    src="https://github.com/user-attachments/assets/4d864e2a-3e78-4061-b6ca-429a11c0780f"
    alt="WidgetStateColor hierarchy"
    width="75%"
  />
</p>

<br>

# Solution

```dart
typedef WidgetStatePropertyOr<T> = T | WidgetStateProperty<T>;
```

Much like how `num` is a 100% straightforward way to accept either an `int` or a `double`, the `WidgetStatePropertyOr` type would allow complete flexibility between e.g. `Color` and `WidgetStateProperty<Color>`.

Currently, we have 2 ways to create a `ButtonStyle`:

```dart
const ButtonStyle({
  WidgetStateProperty<Color?>? backgroundColor,
  WidgetStateProperty<Color?>? foregroundColor,
});

static ButtonStyle styleFrom({
  Color? backgroundColor,
  Color? foregroundColor,
}) {
  // ...
}
```

But `Property<T>` makes it easy to consolidate:

```dart
const ButtonStyle({
  WidgetStatePropertyOr<Color?>? backgroundColor,
  WidgetStatePropertyOr<Color?>? foregroundColor,
})
```

This has 2 huge benefits:
1. Along with the several flaws listed above, `WidgetStateColor` implements the `WidgetStateProperty<Color>` interface, so it doesn't have the added flexibility of a nullable `WidgetStateProperty<Color?>`. Instead of defining a new `WidgetStateNullableColor` type, we can just add a `?` to the existing type argument.
2. This creates a meaningful distinction between `T` and `WidgetStatePropertyAll<T>`:
  a. `Colors.blue`: make it blue, but default to gray if disabled
  b. `const WidgetStatePropertyAll(Colors.blue)`: make it blue no matter what

<br>

### Caveat

The ability to specify a union type `WidgetStatePropertyOr<T> = T | WidgetStateProperty<T>` is amazing, since it implicitly applies to every class declaration.

Or, another great option would be an "implicit" constructor:

```dart
sealed class WidgetStatePropertyOr<T> {
  implicit const factory Property.fromValue(T value) = ValueProperty<T>;

  T resolve(Set<WidgetState> states);
}

class ValueProperty<T> implements WidgetStatePropertyOr<T> {
  const ValueProperty(this.value);
  final T value;

  @override
  T resolve(Set<WidgetState> states) => value;
}

abstract class WidgetStateProperty<T> implements WidgetStatePropertyOr<T> {
  // no changes necessary for existing WidgetStateProperty API
}
```

But as of yet, neither is supported by Dart syntax.

- https://github.com/dart-lang/language/issues/83
- https://github.com/dart-lang/language/issues/108

If somebody is interested in working with the Dart team to implement one of these features, that'd be awesome. ~~Or if someone on the Dart team opens up a Patreon~~ never mind about that idea… maybe we can try to get 500 `👍` reactions on those dart-lang issues!

<br><br>

**TL;DR**: the goal here is to deprecate & remove `WidgetStateColor`, `WidgetStateBorderSide`, `WidgetStateTextStyle`, etc. in favor of a more useful generic type. The ideal solution would be something like

```dart
typedef WidgetStatePropertyOr<T> = T | WidgetStateProperty<T>;
```

…but it requires [a language feature](https://github.com/dart-lang/language/issues/83) that we don't currently have.