[time_picker] refactor: Distinguish widgets for dial mode only (#173188)

The code for time picker involves ambiguous widget names, which makes
understanding the widget structure more challenging.
I tried to use the namings of the [M3
specs](https://m3.material.io/components/time-pickers/specs) to make it
a bit clearer.
This also removes a code section which is not needed, as the widget is
only meant for "dial" and not "input" mode.

I didn't raise an issue, but I can, if that's still necessary.

This PR is test-exempt, as it doesn't add or fix any functionality.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Victor Sanni <victorsanniay@gmail.com>
This commit is contained in:
August 2025-09-25 21:48:07 +02:00 committed by GitHub
parent bf97b09609
commit aa56148767
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 71 deletions

View File

@ -238,13 +238,15 @@ class _TimePickerModel extends InheritedModel<_TimePickerAspect> {
}
}
class _TimePickerHeader extends StatelessWidget {
const _TimePickerHeader({required this.helpText});
/// The header for the time picker in dial mode.
class _DialTimePickerHeader extends StatelessWidget {
const _DialTimePickerHeader({required this.helpText});
final String helpText;
@override
Widget build(BuildContext context) {
assert(_debugDialTimePickerEntryMode(context));
final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(
context,
).timeOfDayFormat(alwaysUse24HourFormat: _TimePickerModel.use24HourFormatOf(context));
@ -285,9 +287,9 @@ class _TimePickerHeader extends StatelessWidget {
// Hour/minutes should not change positions in RTL locales.
textDirection: TextDirection.ltr,
children: <Widget>[
const Expanded(child: _HourControl()),
const Expanded(child: _DialHourControl()),
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
const Expanded(child: _MinuteControl()),
const Expanded(child: _DialMinuteControl()),
],
),
),
@ -316,9 +318,9 @@ class _TimePickerHeader extends StatelessWidget {
// Hour/minutes should not change positions in RTL locales.
textDirection: TextDirection.ltr,
children: <Widget>[
const Expanded(child: _HourControl()),
const Expanded(child: _DialHourControl()),
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
const Expanded(child: _MinuteControl()),
const Expanded(child: _DialMinuteControl()),
],
),
if (hourDialType == _HourDialType.twelveHour) const _DayPeriodControl(),
@ -339,8 +341,9 @@ class _TimePickerHeader extends StatelessWidget {
}
}
class _HourMinuteControl extends StatelessWidget {
const _HourMinuteControl({
/// The control label for the time selector in dial mode.
class _DialTimeSelectorControl extends StatelessWidget {
const _DialTimeSelectorControl({
required this.text,
required this.onTap,
required this.onDoubleTap,
@ -354,6 +357,7 @@ class _HourMinuteControl extends StatelessWidget {
@override
Widget build(BuildContext context) {
assert(_debugDialTimePickerEntryMode(context));
final TimePickerThemeData timePickerTheme = _TimePickerModel.themeOf(context);
final _TimePickerDefaults defaultTheme = _TimePickerModel.defaultThemeOf(context);
final Color backgroundColor = timePickerTheme.hourMinuteColor ?? defaultTheme.hourMinuteColor;
@ -370,18 +374,8 @@ class _HourMinuteControl extends StatelessWidget {
states,
).copyWith(color: effectiveTextColor);
final double height;
switch (_TimePickerModel.entryModeOf(context)) {
case TimePickerEntryMode.dial:
case TimePickerEntryMode.dialOnly:
height = defaultTheme.hourMinuteSize.height;
case TimePickerEntryMode.input:
case TimePickerEntryMode.inputOnly:
height = defaultTheme.hourMinuteInputSize.height;
}
return SizedBox(
height: height,
height: defaultTheme.hourMinuteSize.height,
child: Material(
color: WidgetStateProperty.resolveAs(backgroundColor, states),
clipBehavior: Clip.antiAlias,
@ -398,14 +392,15 @@ class _HourMinuteControl extends StatelessWidget {
}
}
/// Displays the hour fragment.
/// Displays the hour fragment in dial mode.
///
/// When tapped changes time picker dial mode to [_HourMinuteMode.hour].
class _HourControl extends StatelessWidget {
const _HourControl();
class _DialHourControl extends StatelessWidget {
const _DialHourControl();
@override
Widget build(BuildContext context) {
assert(_debugDialTimePickerEntryMode(context));
assert(debugCheckHasMediaQuery(context));
final bool alwaysUse24HourFormat = MediaQuery.alwaysUse24HourFormatOf(context);
final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
@ -453,7 +448,7 @@ class _HourControl extends StatelessWidget {
onDecrease: () {
_TimePickerModel.setSelectedTime(context, previousHour);
},
child: _HourMinuteControl(
child: _DialTimeSelectorControl(
isSelected: _TimePickerModel.hourMinuteModeOf(context) == _HourMinuteMode.hour,
text: formattedHour,
onTap: () => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.hour),
@ -538,14 +533,15 @@ class _TimeSelectorSeparator extends StatelessWidget {
}
}
/// Displays the minute fragment.
/// Displays the minute fragment in dial mode.
///
/// When tapped changes time picker dial mode to [_HourMinuteMode.minute].
class _MinuteControl extends StatelessWidget {
const _MinuteControl();
class _DialMinuteControl extends StatelessWidget {
const _DialMinuteControl();
@override
Widget build(BuildContext context) {
assert(_debugDialTimePickerEntryMode(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
final String formattedMinute = localizations.formatMinute(selectedTime);
@ -569,7 +565,7 @@ class _MinuteControl extends StatelessWidget {
onDecrease: () {
_TimePickerModel.setSelectedTime(context, previousMinute);
},
child: _HourMinuteControl(
child: _DialTimeSelectorControl(
isSelected: _TimePickerModel.hourMinuteModeOf(context) == _HourMinuteMode.minute,
text: formattedMinute,
onTap: () => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.minute),
@ -1612,6 +1608,7 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
assert(_debugDialTimePickerEntryMode(context));
final ThemeData theme = Theme.of(context);
final TimePickerThemeData timePickerTheme = TimePickerTheme.of(context);
final _TimePickerDefaults defaultTheme = theme.useMaterial3
@ -3064,7 +3061,7 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: theme.useMaterial3 ? 0 : 16),
child: _TimePickerHeader(helpText: helpText),
child: _DialTimePickerHeader(helpText: helpText),
),
Expanded(
child: Column(
@ -3091,7 +3088,7 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_TimePickerHeader(helpText: helpText),
_DialTimePickerHeader(helpText: helpText),
Expanded(child: dial),
],
),
@ -3621,6 +3618,12 @@ class _TimePickerDefaultsM2 extends _TimePickerDefaults {
}
}
/// Ensure the widget is called in [TimePickerEntryMode.dial] or [TimePickerEntryMode.dialOnly] mode.
bool _debugDialTimePickerEntryMode(BuildContext context) {
final TimePickerEntryMode entryMode = _TimePickerModel.entryModeOf(context);
return entryMode == TimePickerEntryMode.dial || entryMode == TimePickerEntryMode.dialOnly;
}
// BEGIN GENERATED TOKEN PROPERTIES - TimePicker
// Do not edit by hand. The code between the "BEGIN GENERATED" and

View File

@ -1589,13 +1589,13 @@ void main() {
final SemanticsNode elevenHours = semantics
.nodesWith(
value: 'Select hours $initialValue',
ancestor: tester.renderObject(_hourControl).debugSemantics,
ancestor: tester.renderObject(_dialHourControl).debugSemantics,
)
.single;
tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
await tester.pumpAndSettle();
expect(
find.descendant(of: _hourControl, matching: find.text(finalValue)),
find.descendant(of: _dialHourControl, matching: find.text(finalValue)),
findsOneWidget,
);
}
@ -1643,13 +1643,13 @@ void main() {
final SemanticsNode elevenHours = semantics
.nodesWith(
value: 'Select minutes $initialValue',
ancestor: tester.renderObject(_minuteControl).debugSemantics,
ancestor: tester.renderObject(_dialMinuteControl).debugSemantics,
)
.single;
tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
await tester.pumpAndSettle();
expect(
find.descendant(of: _minuteControl, matching: find.text(finalValue)),
find.descendant(of: _dialMinuteControl, matching: find.text(finalValue)),
findsOneWidget,
);
}
@ -2660,11 +2660,11 @@ Future<void> mediaQueryBoilerplate(
await tester.pumpAndSettle();
}
final Finder _hourControl = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_HourControl',
final Finder _dialHourControl = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialHourControl',
);
final Finder _minuteControl = find.byWidgetPredicate(
(Widget widget) => '${widget.runtimeType}' == '_MinuteControl',
final Finder _dialMinuteControl = find.byWidgetPredicate(
(Widget widget) => '${widget.runtimeType}' == '_DialMinuteControl',
);
final Finder _timePicker = find.byWidgetPredicate(
(Widget widget) => '${widget.runtimeType}' == '_TimePicker',

View File

@ -26,11 +26,11 @@ void main() {
matching: find.byType(Text),
)
.first;
final Finder hourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_HourControl',
final Finder dialHourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialHourControl',
);
final Finder minuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_MinuteControl',
final Finder dialMinuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialMinuteControl',
);
final Finder dayPeriodControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DayPeriodControl',
@ -54,8 +54,8 @@ void main() {
useMaterial3: false,
);
final Text stringFragmentText = tester.widget(timeSelectorSeparatorFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double hourLeftOffset = tester.getTopLeft(dialHourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(dialMinuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(timeSelectorSeparatorFinder).dx;
if (locale == const Locale('en', 'US')) {
@ -111,11 +111,11 @@ void main() {
matching: find.byType(Text),
)
.first;
final Finder hourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_HourControl',
final Finder dialHourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialHourControl',
);
final Finder minuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_MinuteControl',
final Finder dialMinuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialMinuteControl',
);
final Finder dayPeriodControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DayPeriodControl',
@ -139,8 +139,8 @@ void main() {
useMaterial3: true,
);
final Text stringFragmentText = tester.widget(timeSelectorSeparatorFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double hourLeftOffset = tester.getTopLeft(dialHourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(dialMinuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(timeSelectorSeparatorFinder).dx;
if (locale == const Locale('en', 'US')) {
@ -196,11 +196,11 @@ void main() {
matching: find.byType(Text),
)
.first;
final Finder hourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_HourControl',
final Finder dialHourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialHourControl',
);
final Finder minuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_MinuteControl',
final Finder dialMinuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialMinuteControl',
);
final Finder dayPeriodControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DayPeriodControl',
@ -224,9 +224,9 @@ void main() {
useMaterial3: false,
);
final Text stringFragmentText = tester.widget(timeSelectorSeparatorFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double hourTopOffset = tester.getTopLeft(hourControlFinder).dy;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double hourLeftOffset = tester.getTopLeft(dialHourControlFinder).dx;
final double hourTopOffset = tester.getTopLeft(dialHourControlFinder).dy;
final double minuteLeftOffset = tester.getTopLeft(dialMinuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(timeSelectorSeparatorFinder).dx;
if (locale == const Locale('en', 'US')) {
@ -286,11 +286,11 @@ void main() {
matching: find.byType(Text),
)
.first;
final Finder hourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_HourControl',
final Finder dialHourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialHourControl',
);
final Finder minuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_MinuteControl',
final Finder dialMinuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DialMinuteControl',
);
final Finder dayPeriodControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_DayPeriodControl',
@ -314,9 +314,9 @@ void main() {
useMaterial3: true,
);
final Text stringFragmentText = tester.widget(timeSelectorSeparatorFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double hourTopOffset = tester.getTopLeft(hourControlFinder).dy;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double hourLeftOffset = tester.getTopLeft(dialHourControlFinder).dx;
final double hourTopOffset = tester.getTopLeft(dialHourControlFinder).dy;
final double minuteLeftOffset = tester.getTopLeft(dialMinuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(timeSelectorSeparatorFinder).dx;
if (locale == const Locale('en', 'US')) {
@ -365,10 +365,10 @@ void main() {
testWidgets('Material2 - can localize input mode in all known formats', (
WidgetTester tester,
) async {
final Finder hourControlFinder = find.byWidgetPredicate(
final Finder inputHourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_HourTextField',
);
final Finder minuteControlFinder = find.byWidgetPredicate(
final Finder inputMinuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_MinuteTextField',
);
final Finder dayPeriodControlFinder = find.byWidgetPredicate(
@ -404,8 +404,8 @@ void main() {
await tester.pumpAndSettle(const Duration(seconds: 1));
final Text stringFragmentText = tester.widget(timeSelectorSeparatorFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double hourLeftOffset = tester.getTopLeft(inputHourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(inputMinuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(timeSelectorSeparatorFinder).dx;
if (locale == const Locale('en', 'US')) {
@ -450,10 +450,10 @@ void main() {
testWidgets('Material3 - can localize input mode in all known formats', (
WidgetTester tester,
) async {
final Finder hourControlFinder = find.byWidgetPredicate(
final Finder inputHourControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_HourTextField',
);
final Finder minuteControlFinder = find.byWidgetPredicate(
final Finder inputMinuteControlFinder = find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_MinuteTextField',
);
final Finder dayPeriodControlFinder = find.byWidgetPredicate(
@ -489,8 +489,8 @@ void main() {
await tester.pumpAndSettle(const Duration(seconds: 1));
final Text stringFragmentText = tester.widget(timeSelectorSeparatorFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double hourLeftOffset = tester.getTopLeft(inputHourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(inputMinuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(timeSelectorSeparatorFinder).dx;
if (locale == const Locale('en', 'US')) {