Skip to content

Commit db80f4e

Browse files
WidgetState mapping (flutter#146043)
This pull request implements [enhanced enum](https://dart.dev/language/enums#declaring-enhanced-enums) features for the new `WidgetState` enum, in order to improve the developer experience when creating and using `WidgetStateProperty` objects. `WidgetState` now has a `.matchesSet()` method: ```dart // identical to "states.contains(WidgetState.error)" final bool hasError = WidgetState.error.isSatisfiedBy(states); ``` This addition allows for wide variety of `WidgetStateProperty` objects to be constructed in a simple manner. <br><br> ```dart // before final style = MaterialStateTextStyle.resolveWith((states) { if (states.contains(MaterialState.error)) { return TextStyle(color: Colors.red); } else if (states.contains(MaterialState.focused)) { return TextStyle(color: Colors.blue); } return TextStyle(color: Colors.black); }); // after final style = WidgetStateTextStyle.fromMap({ WidgetState.error: TextStyle(color: Colors.red), WidgetState.focused: TextStyle(color: Colors.blue), WidgetState.any: TextStyle(color: Colors.black), // "any" is a static const member, not an enum value }); ``` ```dart // before final color = MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.focused)) { return Colors.blue; } else if (!states.contains(MaterialState.disabled)) { return Colors.black; } return null; }); // after final color = WidgetStateProperty<Color?>.fromMap({ WidgetState.focused: Colors.blue, ~WidgetState.disabled: Colors.black, }); ``` ```dart // before const activeStates = [MaterialState.selected, MaterialState.focused, MaterialState.scrolledUnder]; final color = MaterialStateColor.resolveWith((states) { if (activeStates.any(states.contains)) { if (states.contains(MaterialState.hovered) { return Colors.blueAccent; } return Colors.blue; } return Colors.black; }); // after final active = WidgetState.selected | WidgetState.focused | WidgetState.scrolledUnder; final color = WidgetStateColor.fromMap({ active & WidgetState.hovered: Colors.blueAccent, active: Colors.blue, ~active: Colors.black, }); ``` <br> (fixes flutter#146042, and also fixes flutter#143488)
1 parent af0e01c commit db80f4e

File tree

2 files changed

+305
-10
lines changed

2 files changed

+305
-10
lines changed

packages/flutter/lib/src/widgets/widget_state.dart

Lines changed: 288 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,89 @@ import 'package:flutter/services.dart';
88

99
// Examples can assume:
1010
// late BuildContext context;
11+
// late Set<WidgetState> states;
12+
13+
/// This class allows [WidgetState] enum values to be combined
14+
/// using [WidgetStateOperators].
15+
///
16+
/// A [Map] with [WidgetStatesConstraint] objects as keys can be used
17+
/// in the [WidgetStateProperty.fromMap] constructor to resolve to
18+
/// one of its values, based on the first key that [isSatisfiedBy]
19+
/// the current set of states.
20+
///
21+
/// {@macro flutter.widgets.WidgetStateMap}
22+
abstract interface class WidgetStatesConstraint {
23+
/// Whether the provided [states] satisfy this object's criteria.
24+
///
25+
/// If the constraint is a single [WidgetState] object,
26+
/// it's satisfied by the set if the set contains the object.
27+
///
28+
/// The constraint can also be created using one or more operators, for example:
29+
///
30+
/// {@template flutter.widgets.WidgetStatesConstraint.isSatisfiedBy}
31+
/// ```dart
32+
/// final WidgetStatesConstraint constraint = WidgetState.focused | WidgetState.hovered;
33+
/// ```
34+
///
35+
/// In the above case, `constraint.isSatisfiedBy(states)` is equivalent to:
36+
///
37+
/// ```dart
38+
/// states.contains(WidgetState.focused) || states.contains(WidgetState.hovered);
39+
/// ```
40+
/// {@endtemplate}
41+
bool isSatisfiedBy(Set<WidgetState> states);
42+
}
43+
44+
// A private class, used in [WidgetStateOperators].
45+
class _WidgetStateOperation implements WidgetStatesConstraint {
46+
const _WidgetStateOperation(this._isSatisfiedBy);
47+
48+
final bool Function(Set<WidgetState> states) _isSatisfiedBy;
49+
50+
@override
51+
bool isSatisfiedBy(Set<WidgetState> states) => _isSatisfiedBy(states);
52+
}
53+
54+
/// These operators can be used inside a [WidgetStateMap] to combine states
55+
/// and find a match.
56+
///
57+
/// Example:
58+
///
59+
/// {@macro flutter.widgets.WidgetStatesConstraint.isSatisfiedBy}
60+
///
61+
/// Since enums can't extend other classes, [WidgetState] instead `implements`
62+
/// the [WidgetStatesConstraint] interface. This `extension` ensures that
63+
/// the operators can be used without being directly inherited.
64+
extension WidgetStateOperators on WidgetStatesConstraint {
65+
/// Combines two [WidgetStatesConstraint] values using logical "and".
66+
WidgetStatesConstraint operator &(WidgetStatesConstraint other) {
67+
return _WidgetStateOperation(
68+
(Set<WidgetState> states) => isSatisfiedBy(states) && other.isSatisfiedBy(states),
69+
);
70+
}
71+
72+
/// Combines two [WidgetStatesConstraint] values using logical "or".
73+
WidgetStatesConstraint operator |(WidgetStatesConstraint other) {
74+
return _WidgetStateOperation(
75+
(Set<WidgetState> states) => isSatisfiedBy(states) || other.isSatisfiedBy(states),
76+
);
77+
}
78+
79+
/// Takes a [WidgetStatesConstraint] and applies the logical "not".
80+
WidgetStatesConstraint operator ~() {
81+
return _WidgetStateOperation(
82+
(Set<WidgetState> states) => !isSatisfiedBy(states),
83+
);
84+
}
85+
}
86+
87+
// A private class, used to create [WidgetState.any].
88+
class _AlwaysMatch implements WidgetStatesConstraint {
89+
const _AlwaysMatch();
90+
91+
@override
92+
bool isSatisfiedBy(Set<WidgetState> states) => true;
93+
}
1194

1295
/// Interactive states that some of the widgets can take on when receiving input
1396
/// from the user.
@@ -39,7 +122,7 @@ import 'package:flutter/services.dart';
39122
/// `WidgetStateProperty` which is used in APIs that need to accept either
40123
/// a [TextStyle] or a [WidgetStateProperty<TextStyle>].
41124
/// {@endtemplate}
42-
enum WidgetState {
125+
enum WidgetState implements WidgetStatesConstraint {
43126
/// The state when the user drags their mouse cursor over the given widget.
44127
///
45128
/// See: https://material.io/design/interaction/states.html#hover.
@@ -89,7 +172,17 @@ enum WidgetState {
89172
/// The state when the widget has entered some form of invalid state.
90173
///
91174
/// See https://material.io/design/interaction/states.html#usage.
92-
error,
175+
error;
176+
177+
/// {@template flutter.widgets.WidgetState.any}
178+
/// To prevent a situation where each [WidgetStatesConstraint]
179+
/// isn't satisfied by the given set of states, consier adding
180+
/// [WidgetState.any] as the final [WidgetStateMap] key.
181+
/// {@endtemplate}
182+
static const WidgetStatesConstraint any = _AlwaysMatch();
183+
184+
@override
185+
bool isSatisfiedBy(Set<WidgetState> states) => states.contains(this);
93186
}
94187

95188
/// Signature for the function that returns a value of type `T` based on a given
@@ -112,6 +205,7 @@ typedef WidgetPropertyResolver<T> = T Function(Set<WidgetState> states);
112205
/// 1. Create a subclass of [WidgetStateColor] and implement the abstract `resolve` method.
113206
/// 2. Use [WidgetStateColor.resolveWith] and pass in a callback that
114207
/// will be used to resolve the color in the given states.
208+
/// 3. Use [WidgetStateColor.fromMap] to assign a value using a [WidgetStateMap].
115209
///
116210
/// If a [WidgetStateColor] is used for a property or a parameter that doesn't
117211
/// support resolving [WidgetStateProperty<Color>]s, then its default color
@@ -160,7 +254,17 @@ abstract class WidgetStateColor extends Color implements WidgetStateProperty<Col
160254
///
161255
/// The given callback parameter must return a non-null color in the default
162256
/// state.
163-
static WidgetStateColor resolveWith(WidgetPropertyResolver<Color> callback) => _WidgetStateColor(callback);
257+
factory WidgetStateColor.resolveWith(WidgetPropertyResolver<Color> callback) = _WidgetStateColor;
258+
259+
/// Creates a [WidgetStateColor] from a [WidgetStateMap<Color>].
260+
///
261+
/// {@macro flutter.widgets.WidgetStateProperty.fromMap}
262+
///
263+
/// If used as a regular color, the first key that matches an empty
264+
/// [Set] of [WidgetState]s will be selected.
265+
///
266+
/// {@macro flutter.widgets.WidgetState.any}
267+
factory WidgetStateColor.fromMap(WidgetStateMap<Color> map) = _WidgetStateColorMapper;
164268

165269
/// Returns a [Color] that's to be used when a component is in the specified
166270
/// state.
@@ -182,6 +286,18 @@ class _WidgetStateColor extends WidgetStateColor {
182286
Color resolve(Set<WidgetState> states) => _resolve(states);
183287
}
184288

289+
class _WidgetStateColorMapper extends WidgetStateColor {
290+
_WidgetStateColorMapper(this.map)
291+
: super(_WidgetStateMapper<Color>(map).resolve(_defaultStates).value);
292+
293+
final WidgetStateMap<Color> map;
294+
295+
static const Set<WidgetState> _defaultStates = <WidgetState>{};
296+
297+
@override
298+
Color resolve(Set<WidgetState> states) => _WidgetStateMapper<Color>(map).resolve(states);
299+
}
300+
185301
class _WidgetStateColorTransparent extends WidgetStateColor {
186302
const _WidgetStateColorTransparent() : super(0x00000000);
187303

@@ -348,8 +464,30 @@ abstract class WidgetStateBorderSide extends BorderSide implements WidgetStatePr
348464
/// ```
349465
const factory WidgetStateBorderSide.resolveWith(WidgetPropertyResolver<BorderSide?> callback) = _WidgetStateBorderSide;
350466

351-
/// Returns a [BorderSide] that's to be used when a Material component is
352-
/// in the specified state. Return null to defer to the default value of the
467+
/// Creates a [WidgetStateBorderSide] from a [WidgetStateMap].
468+
///
469+
/// {@macro flutter.widgets.WidgetStateProperty.fromMap}
470+
///
471+
/// If used as a regular [BorderSide], the first key that matches an empty
472+
/// [Set] of [WidgetState]s will be selected.
473+
///
474+
/// Example:
475+
///
476+
/// ```dart
477+
/// const Chip(
478+
/// label: Text('Transceiver'),
479+
/// side: WidgetStateBorderSide.fromMap(<WidgetStatesConstraint, BorderSide?>{
480+
/// WidgetState.selected: BorderSide(color: Colors.red),
481+
/// // returns null if not selected, deferring to default theme/widget value.
482+
/// }),
483+
/// ),
484+
/// ```
485+
///
486+
/// {@macro flutter.widgets.WidgetState.any}
487+
const factory WidgetStateBorderSide.fromMap(WidgetStateMap<BorderSide?> map) = _WidgetBorderSideMapper;
488+
489+
/// Returns a [BorderSide] that's to be used when a Widget is in the
490+
/// specified state. Return null to defer to the default value of the
353491
/// widget or theme.
354492
@override
355493
BorderSide? resolve(Set<WidgetState> states);
@@ -401,6 +539,15 @@ class _WidgetStateBorderSide extends WidgetStateBorderSide {
401539
BorderSide? resolve(Set<WidgetState> states) => _resolve(states);
402540
}
403541

542+
class _WidgetBorderSideMapper extends WidgetStateBorderSide {
543+
const _WidgetBorderSideMapper(this.map);
544+
545+
final WidgetStateMap<BorderSide?> map;
546+
547+
@override
548+
BorderSide? resolve(Set<WidgetState> states) => _WidgetStateMapper<BorderSide?>(map).resolve(states);
549+
}
550+
404551
/// Defines an [OutlinedBorder] whose value depends on a set of [WidgetState]s
405552
/// which represent the interactive state of a component.
406553
///
@@ -452,6 +599,7 @@ abstract class WidgetStateOutlinedBorder extends OutlinedBorder implements Widge
452599
/// 1. Create a subclass of [WidgetStateTextStyle] and implement the abstract `resolve` method.
453600
/// 2. Use [WidgetStateTextStyle.resolveWith] and pass in a callback that
454601
/// will be used to resolve the color in the given states.
602+
/// 3. Use [WidgetStateTextStyle.fromMap] to assign a style using a [WidgetStateMap].
455603
///
456604
/// If a [WidgetStateTextStyle] is used for a property or a parameter that doesn't
457605
/// support resolving [WidgetStateProperty<TextStyle>]s, then its default color
@@ -480,6 +628,16 @@ abstract class WidgetStateTextStyle extends TextStyle implements WidgetStateProp
480628
/// state.
481629
const factory WidgetStateTextStyle.resolveWith(WidgetPropertyResolver<TextStyle> callback) = _WidgetStateTextStyle;
482630

631+
/// Creates a [WidgetStateTextStyle] from a [WidgetStateMap].
632+
///
633+
/// {@macro flutter.widgets.WidgetStateProperty.fromMap}
634+
///
635+
/// If used as a regular text style, the first key that matches an empty
636+
/// [Set] of [WidgetState]s will be selected.
637+
///
638+
/// {@macro flutter.widgets.WidgetState.any}
639+
const factory WidgetStateTextStyle.fromMap(WidgetStateMap<TextStyle> map) = _WidgetTextStyleMapper;
640+
483641
/// Returns a [TextStyle] that's to be used when a component is in the
484642
/// specified state.
485643
@override
@@ -495,6 +653,15 @@ class _WidgetStateTextStyle extends WidgetStateTextStyle {
495653
TextStyle resolve(Set<WidgetState> states) => _resolve(states);
496654
}
497655

656+
class _WidgetTextStyleMapper extends WidgetStateTextStyle {
657+
const _WidgetTextStyleMapper(this.map);
658+
659+
final WidgetStateMap<TextStyle> map;
660+
661+
@override
662+
TextStyle resolve(Set<WidgetState> states) => _WidgetStateMapper<TextStyle>(map).resolve(states);
663+
}
664+
498665
/// Interface for classes that [resolve] to a value of type `T` based
499666
/// on a widget's interactive "state", which is defined as a set
500667
/// of [WidgetState]s.
@@ -519,12 +686,26 @@ class _WidgetStateTextStyle extends WidgetStateTextStyle {
519686
/// `WidgetStateProperty`.
520687
/// {@macro flutter.widgets.WidgetStateProperty.implementations}
521688
abstract class WidgetStateProperty<T> {
522-
/// Returns a value of type `T` that depends on [states].
689+
/// This abstract constructor allows extending the class.
523690
///
524-
/// Widgets like [TextButton] and [ElevatedButton] apply this method to their
525-
/// current [WidgetState]s to compute colors and other visual parameters
526-
/// at build time.
527-
T resolve(Set<WidgetState> states);
691+
/// [WidgetStateProperty] is designed as an interface, so this constructor
692+
/// is only needed for backward compatibility.
693+
WidgetStateProperty();
694+
695+
/// Creates a property that resolves using a [WidgetStateMap].
696+
///
697+
/// {@template flutter.widgets.WidgetStateProperty.fromMap}
698+
/// This constructor's [resolve] method finds the first [MapEntry] whose
699+
/// key is satisfied by the set of states, and returns its associated value.
700+
/// {@endtemplate}
701+
///
702+
/// Returns `null` if no keys match, or if [T] is non-nullable,
703+
/// the method throws an [ArgumentError].
704+
///
705+
/// {@macro flutter.widgets.WidgetState.any}
706+
///
707+
/// {@macro flutter.widgets.WidgetStateMap}
708+
const factory WidgetStateProperty.fromMap(WidgetStateMap<T> map) = _WidgetStateMapper<T>;
528709

529710
/// Resolves the value for the given set of states if `value` is a
530711
/// [WidgetStateProperty], otherwise returns the value itself.
@@ -567,6 +748,13 @@ abstract class WidgetStateProperty<T> {
567748
}
568749
return _LerpProperties<T>(a, b, t, lerpFunction);
569750
}
751+
752+
/// Returns a value of type `T` that depends on [states].
753+
///
754+
/// Widgets like [TextButton] and [ElevatedButton] apply this method to their
755+
/// current [WidgetState]s to compute colors and other visual parameters
756+
/// at build time.
757+
T resolve(Set<WidgetState> states);
570758
}
571759

572760
class _LerpProperties<T> implements WidgetStateProperty<T?> {
@@ -594,6 +782,96 @@ class _WidgetStatePropertyWith<T> implements WidgetStateProperty<T> {
594782
T resolve(Set<WidgetState> states) => _resolve(states);
595783
}
596784

785+
/// A [Map] used to resolve to a single value of type `T` based on
786+
/// the current set of Widget states.
787+
///
788+
/// {@template flutter.widgets.WidgetStateMap}
789+
/// Example:
790+
///
791+
/// ```dart
792+
/// // This WidgetStateMap<Color?> resolves to null if no keys match.
793+
/// WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
794+
/// WidgetState.error: Colors.red,
795+
/// WidgetState.hovered & WidgetState.focused: Colors.blueAccent,
796+
/// WidgetState.focused: Colors.blue,
797+
/// ~WidgetState.disabled: Colors.black,
798+
/// });
799+
///
800+
/// // The same can be accomplished with a WidgetPropertyResolver,
801+
/// // but it's more verbose:
802+
/// WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) {
803+
/// if (states.contains(WidgetState.error)) {
804+
/// return Colors.red;
805+
/// } else if (states.contains(WidgetState.hovered) && states.contains(WidgetState.focused)) {
806+
/// return Colors.blueAccent;
807+
/// } else if (states.contains(WidgetState.focused)) {
808+
/// return Colors.blue;
809+
/// } else if (!states.contains(WidgetState.disabled)) {
810+
/// return Colors.black;
811+
/// }
812+
/// return null;
813+
/// });
814+
/// ```
815+
///
816+
/// A widget state combination can be stored in a variable,
817+
/// and [WidgetState.any] can be used for non-nullable types to ensure
818+
/// that there's a match:
819+
///
820+
/// ```dart
821+
/// final WidgetStatesConstraint selectedError = WidgetState.selected & WidgetState.error;
822+
///
823+
/// final WidgetStateProperty<Color> color = WidgetStateProperty<Color>.fromMap(
824+
/// <WidgetStatesConstraint, Color>{
825+
/// selectedError & WidgetState.hovered: Colors.redAccent,
826+
/// selectedError: Colors.red,
827+
/// WidgetState.any: Colors.black,
828+
/// },
829+
/// );
830+
///
831+
/// // The (more verbose) WidgetPropertyResolver implementation:
832+
/// final WidgetStateProperty<Color> colorResolveWith = WidgetStateProperty.resolveWith<Color>(
833+
/// (Set<WidgetState> states) {
834+
/// if (states.containsAll(<WidgetState>{WidgetState.selected, WidgetState.error})) {
835+
/// if (states.contains(WidgetState.hovered)) {
836+
/// return Colors.redAccent;
837+
/// }
838+
/// return Colors.red;
839+
/// }
840+
/// return Colors.black;
841+
/// },
842+
/// );
843+
/// ```
844+
/// {@endtemplate}
845+
typedef WidgetStateMap<T> = Map<WidgetStatesConstraint, T>;
846+
847+
// A private class, used to create the [WidgetStateProperty.fromMap] constructor.
848+
class _WidgetStateMapper<T> implements WidgetStateProperty<T> {
849+
const _WidgetStateMapper(this.map);
850+
851+
final WidgetStateMap<T> map;
852+
853+
@override
854+
T resolve(Set<WidgetState> states) {
855+
for (final MapEntry<WidgetStatesConstraint, T> entry in map.entries) {
856+
if (entry.key.isSatisfiedBy(states)) {
857+
return entry.value;
858+
}
859+
}
860+
861+
try {
862+
return null as T;
863+
} on TypeError {
864+
throw ArgumentError(
865+
'The current set of material states is $states.\n'
866+
'None of the provided map keys matched this set, '
867+
'and the type "$T" is non-nullable.\n'
868+
'Consider using "WidgetStateProperty<$T?>.fromMap()", '
869+
'or adding the "WidgetState.any" key to this map.',
870+
);
871+
}
872+
}
873+
}
874+
597875
/// Convenience class for creating a [WidgetStateProperty] that
598876
/// resolves to the given value for all states.
599877
///

0 commit comments

Comments
 (0)