Unverified Commit 07849778 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

[State Restoration] Restorable `TimePickerDialog` widget, `RestorableTimeOfDay` (#80566)

parent e6d4b8cf
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'debug.dart'; import 'debug.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
...@@ -134,6 +135,40 @@ class TimeOfDay { ...@@ -134,6 +135,40 @@ class TimeOfDay {
} }
} }
/// A [RestorableValue] that knows how to save and restore [TimeOfDay].
///
/// {@macro flutter.widgets.RestorableNum}.
class RestorableTimeOfDay extends RestorableValue<TimeOfDay> {
/// Creates a [RestorableTimeOfDay].
///
/// {@macro flutter.widgets.RestorableNum.constructor}
RestorableTimeOfDay(TimeOfDay defaultValue) : _defaultValue = defaultValue;
final TimeOfDay _defaultValue;
@override
TimeOfDay createDefaultValue() => _defaultValue;
@override
void didUpdateValue(TimeOfDay? oldValue) {
assert(debugIsSerializableForRestoration(value.hour));
assert(debugIsSerializableForRestoration(value.minute));
notifyListeners();
}
@override
TimeOfDay fromPrimitives(Object? data) {
final List<Object?> timeData = data! as List<Object?>;
return TimeOfDay(
minute: timeData[0]! as int,
hour: timeData[1]! as int,
);
}
@override
Object? toPrimitives() => <int>[value.minute, value.hour];
}
/// Determines how the time picker invoked using [showTimePicker] formats and /// Determines how the time picker invoked using [showTimePicker] formats and
/// lays out the time controls. /// lays out the time controls.
/// ///
......
...@@ -1293,6 +1293,7 @@ class _TimePickerInput extends StatefulWidget { ...@@ -1293,6 +1293,7 @@ class _TimePickerInput extends StatefulWidget {
required this.autofocusHour, required this.autofocusHour,
required this.autofocusMinute, required this.autofocusMinute,
required this.onChanged, required this.onChanged,
this.restorationId,
}) : assert(initialSelectedTime != null), }) : assert(initialSelectedTime != null),
assert(onChanged != null), assert(onChanged != null),
super(key: key); super(key: key);
...@@ -1309,19 +1310,32 @@ class _TimePickerInput extends StatefulWidget { ...@@ -1309,19 +1310,32 @@ class _TimePickerInput extends StatefulWidget {
final ValueChanged<TimeOfDay> onChanged; final ValueChanged<TimeOfDay> onChanged;
/// Restoration ID to save and restore the state of the time picker input
/// widget.
///
/// If it is non-null, the widget will persist and restore its state
///
/// The state of this widget is persisted in a [RestorationBucket] claimed
/// from the surrounding [RestorationScope] using the provided restoration ID.
final String? restorationId;
@override @override
_TimePickerInputState createState() => _TimePickerInputState(); _TimePickerInputState createState() => _TimePickerInputState();
} }
class _TimePickerInputState extends State<_TimePickerInput> { class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixin {
late TimeOfDay _selectedTime; late final RestorableTimeOfDay _selectedTime = RestorableTimeOfDay(widget.initialSelectedTime);
bool hourHasError = false; final RestorableBool hourHasError = RestorableBool(false);
bool minuteHasError = false; final RestorableBool minuteHasError = RestorableBool(false);
@override @override
void initState() { String? get restorationId => widget.restorationId;
super.initState();
_selectedTime = widget.initialSelectedTime; @override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_selectedTime, 'selected_time');
registerForRestoration(hourHasError, 'hour_has_error');
registerForRestoration(minuteHasError, 'minute_has_error');
} }
int? _parseHour(String? value) { int? _parseHour(String? value) {
...@@ -1340,8 +1354,8 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1340,8 +1354,8 @@ class _TimePickerInputState extends State<_TimePickerInput> {
} }
} else { } else {
if (newHour > 0 && newHour < 13) { if (newHour > 0 && newHour < 13) {
if ((_selectedTime.period == DayPeriod.pm && newHour != 12) if ((_selectedTime.value.period == DayPeriod.pm && newHour != 12)
|| (_selectedTime.period == DayPeriod.am && newHour == 12)) { || (_selectedTime.value.period == DayPeriod.am && newHour == 12)) {
newHour = (newHour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay; newHour = (newHour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
} }
return newHour; return newHour;
...@@ -1369,8 +1383,8 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1369,8 +1383,8 @@ class _TimePickerInputState extends State<_TimePickerInput> {
void _handleHourSavedSubmitted(String? value) { void _handleHourSavedSubmitted(String? value) {
final int? newHour = _parseHour(value); final int? newHour = _parseHour(value);
if (newHour != null) { if (newHour != null) {
_selectedTime = TimeOfDay(hour: newHour, minute: _selectedTime.minute); _selectedTime.value = TimeOfDay(hour: newHour, minute: _selectedTime.value.minute);
widget.onChanged(_selectedTime); widget.onChanged(_selectedTime.value);
} }
} }
...@@ -1385,20 +1399,20 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1385,20 +1399,20 @@ class _TimePickerInputState extends State<_TimePickerInput> {
void _handleMinuteSavedSubmitted(String? value) { void _handleMinuteSavedSubmitted(String? value) {
final int? newMinute = _parseMinute(value); final int? newMinute = _parseMinute(value);
if (newMinute != null) { if (newMinute != null) {
_selectedTime = TimeOfDay(hour: _selectedTime.hour, minute: int.parse(value!)); _selectedTime.value = TimeOfDay(hour: _selectedTime.value.hour, minute: int.parse(value!));
widget.onChanged(_selectedTime); widget.onChanged(_selectedTime.value);
} }
} }
void _handleDayPeriodChanged(TimeOfDay value) { void _handleDayPeriodChanged(TimeOfDay value) {
_selectedTime = value; _selectedTime.value = value;
widget.onChanged(_selectedTime); widget.onChanged(_selectedTime.value);
} }
String? _validateHour(String? value) { String? _validateHour(String? value) {
final int? newHour = _parseHour(value); final int? newHour = _parseHour(value);
setState(() { setState(() {
hourHasError = newHour == null; hourHasError.value = newHour == null;
}); });
// This is used as the validator for the [TextFormField]. // This is used as the validator for the [TextFormField].
// Returning an empty string allows the field to go into an error state. // Returning an empty string allows the field to go into an error state.
...@@ -1409,7 +1423,7 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1409,7 +1423,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
String? _validateMinute(String? value) { String? _validateMinute(String? value) {
final int? newMinute = _parseMinute(value); final int? newMinute = _parseMinute(value);
setState(() { setState(() {
minuteHasError = newMinute == null; minuteHasError.value = newMinute == null;
}); });
// This is used as the validator for the [TextFormField]. // This is used as the validator for the [TextFormField].
// Returning an empty string allows the field to go into an error state. // Returning an empty string allows the field to go into an error state.
...@@ -1441,7 +1455,7 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1441,7 +1455,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
children: <Widget>[ children: <Widget>[
if (!use24HourDials && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[ if (!use24HourDials && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
_DayPeriodControl( _DayPeriodControl(
selectedTime: _selectedTime, selectedTime: _selectedTime.value,
orientation: Orientation.portrait, orientation: Orientation.portrait,
onChanged: _handleDayPeriodChanged, onChanged: _handleDayPeriodChanged,
), ),
...@@ -1459,7 +1473,8 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1459,7 +1473,8 @@ class _TimePickerInputState extends State<_TimePickerInput> {
children: <Widget>[ children: <Widget>[
const SizedBox(height: 8.0), const SizedBox(height: 8.0),
_HourTextField( _HourTextField(
selectedTime: _selectedTime, restorationId: 'hour_text_field',
selectedTime: _selectedTime.value,
style: hourMinuteStyle, style: hourMinuteStyle,
autofocus: widget.autofocusHour, autofocus: widget.autofocusHour,
validator: _validateHour, validator: _validateHour,
...@@ -1467,7 +1482,7 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1467,7 +1482,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
onChanged: _handleHourChanged, onChanged: _handleHourChanged,
), ),
const SizedBox(height: 8.0), const SizedBox(height: 8.0),
if (!hourHasError && !minuteHasError) if (!hourHasError.value && !minuteHasError.value)
ExcludeSemantics( ExcludeSemantics(
child: Text( child: Text(
MaterialLocalizations.of(context).timePickerHourLabel, MaterialLocalizations.of(context).timePickerHourLabel,
...@@ -1490,14 +1505,15 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1490,14 +1505,15 @@ class _TimePickerInputState extends State<_TimePickerInput> {
children: <Widget>[ children: <Widget>[
const SizedBox(height: 8.0), const SizedBox(height: 8.0),
_MinuteTextField( _MinuteTextField(
selectedTime: _selectedTime, restorationId: 'minute_text_field',
selectedTime: _selectedTime.value,
style: hourMinuteStyle, style: hourMinuteStyle,
autofocus: widget.autofocusMinute, autofocus: widget.autofocusMinute,
validator: _validateMinute, validator: _validateMinute,
onSavedSubmitted: _handleMinuteSavedSubmitted, onSavedSubmitted: _handleMinuteSavedSubmitted,
), ),
const SizedBox(height: 8.0), const SizedBox(height: 8.0),
if (!hourHasError && !minuteHasError) if (!hourHasError.value && !minuteHasError.value)
ExcludeSemantics( ExcludeSemantics(
child: Text( child: Text(
MaterialLocalizations.of(context).timePickerMinuteLabel, MaterialLocalizations.of(context).timePickerMinuteLabel,
...@@ -1515,14 +1531,14 @@ class _TimePickerInputState extends State<_TimePickerInput> { ...@@ -1515,14 +1531,14 @@ class _TimePickerInputState extends State<_TimePickerInput> {
if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[ if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
const SizedBox(width: 12.0), const SizedBox(width: 12.0),
_DayPeriodControl( _DayPeriodControl(
selectedTime: _selectedTime, selectedTime: _selectedTime.value,
orientation: Orientation.portrait, orientation: Orientation.portrait,
onChanged: _handleDayPeriodChanged, onChanged: _handleDayPeriodChanged,
), ),
], ],
], ],
), ),
if (hourHasError || minuteHasError) if (hourHasError.value || minuteHasError.value)
Text( Text(
MaterialLocalizations.of(context).invalidTimeLabel, MaterialLocalizations.of(context).invalidTimeLabel,
style: theme.textTheme.bodyText2!.copyWith(color: theme.colorScheme.error), style: theme.textTheme.bodyText2!.copyWith(color: theme.colorScheme.error),
...@@ -1544,6 +1560,7 @@ class _HourTextField extends StatelessWidget { ...@@ -1544,6 +1560,7 @@ class _HourTextField extends StatelessWidget {
required this.validator, required this.validator,
required this.onSavedSubmitted, required this.onSavedSubmitted,
required this.onChanged, required this.onChanged,
this.restorationId,
}) : super(key: key); }) : super(key: key);
final TimeOfDay selectedTime; final TimeOfDay selectedTime;
...@@ -1552,10 +1569,12 @@ class _HourTextField extends StatelessWidget { ...@@ -1552,10 +1569,12 @@ class _HourTextField extends StatelessWidget {
final FormFieldValidator<String> validator; final FormFieldValidator<String> validator;
final ValueChanged<String?> onSavedSubmitted; final ValueChanged<String?> onSavedSubmitted;
final ValueChanged<String> onChanged; final ValueChanged<String> onChanged;
final String? restorationId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _HourMinuteTextField( return _HourMinuteTextField(
restorationId: restorationId,
selectedTime: selectedTime, selectedTime: selectedTime,
isHour: true, isHour: true,
autofocus: autofocus, autofocus: autofocus,
...@@ -1576,6 +1595,7 @@ class _MinuteTextField extends StatelessWidget { ...@@ -1576,6 +1595,7 @@ class _MinuteTextField extends StatelessWidget {
required this.autofocus, required this.autofocus,
required this.validator, required this.validator,
required this.onSavedSubmitted, required this.onSavedSubmitted,
this.restorationId,
}) : super(key: key); }) : super(key: key);
final TimeOfDay selectedTime; final TimeOfDay selectedTime;
...@@ -1583,10 +1603,12 @@ class _MinuteTextField extends StatelessWidget { ...@@ -1583,10 +1603,12 @@ class _MinuteTextField extends StatelessWidget {
final bool? autofocus; final bool? autofocus;
final FormFieldValidator<String> validator; final FormFieldValidator<String> validator;
final ValueChanged<String?> onSavedSubmitted; final ValueChanged<String?> onSavedSubmitted;
final String? restorationId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _HourMinuteTextField( return _HourMinuteTextField(
restorationId: restorationId,
selectedTime: selectedTime, selectedTime: selectedTime,
isHour: false, isHour: false,
autofocus: autofocus, autofocus: autofocus,
...@@ -1608,6 +1630,7 @@ class _HourMinuteTextField extends StatefulWidget { ...@@ -1608,6 +1630,7 @@ class _HourMinuteTextField extends StatefulWidget {
required this.semanticHintText, required this.semanticHintText,
required this.validator, required this.validator,
required this.onSavedSubmitted, required this.onSavedSubmitted,
this.restorationId,
this.onChanged, this.onChanged,
}) : super(key: key); }) : super(key: key);
...@@ -1619,13 +1642,15 @@ class _HourMinuteTextField extends StatefulWidget { ...@@ -1619,13 +1642,15 @@ class _HourMinuteTextField extends StatefulWidget {
final FormFieldValidator<String> validator; final FormFieldValidator<String> validator;
final ValueChanged<String?> onSavedSubmitted; final ValueChanged<String?> onSavedSubmitted;
final ValueChanged<String>? onChanged; final ValueChanged<String>? onChanged;
final String? restorationId;
@override @override
_HourMinuteTextFieldState createState() => _HourMinuteTextFieldState(); _HourMinuteTextFieldState createState() => _HourMinuteTextFieldState();
} }
class _HourMinuteTextFieldState extends State<_HourMinuteTextField> { class _HourMinuteTextFieldState extends State<_HourMinuteTextField> with RestorationMixin {
TextEditingController? controller; final RestorableTextEditingController controller = RestorableTextEditingController();
final RestorableBool controllerHasBeenSet = RestorableBool(false);
late FocusNode focusNode; late FocusNode focusNode;
@override @override
...@@ -1639,7 +1664,21 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> { ...@@ -1639,7 +1664,21 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
controller ??= TextEditingController(text: _formattedValue); // Only set the text value if it has not been populated with a localized
// version yet.
if (!controllerHasBeenSet.value) {
controllerHasBeenSet.value = true;
controller.value.text = _formattedValue;
}
}
@override
String? get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(controller, 'text_editing_controller');
registerForRestoration(controllerHasBeenSet, 'has_controller_been_set');
} }
String get _formattedValue { String get _formattedValue {
...@@ -1701,24 +1740,28 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> { ...@@ -1701,24 +1740,28 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
height: _kTimePickerHeaderControlHeight, height: _kTimePickerHeaderControlHeight,
child: MediaQuery( child: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: TextFormField( child: UnmanagedRestorationScope(
autofocus: widget.autofocus ?? false, bucket: bucket,
expands: true, child: TextFormField(
maxLines: null, restorationId: 'hour_minute_text_form_field',
inputFormatters: <TextInputFormatter>[ autofocus: widget.autofocus ?? false,
LengthLimitingTextInputFormatter(2), expands: true,
], maxLines: null,
focusNode: focusNode, inputFormatters: <TextInputFormatter>[
textAlign: TextAlign.center, LengthLimitingTextInputFormatter(2),
keyboardType: TextInputType.number, ],
style: widget.style.copyWith(color: timePickerTheme.hourMinuteTextColor ?? colorScheme.onSurface), focusNode: focusNode,
controller: controller, textAlign: TextAlign.center,
decoration: inputDecoration, keyboardType: TextInputType.number,
validator: widget.validator, style: widget.style.copyWith(color: timePickerTheme.hourMinuteTextColor ?? colorScheme.onSurface),
onEditingComplete: () => widget.onSavedSubmitted(controller!.text), controller: controller.value,
onSaved: widget.onSavedSubmitted, decoration: inputDecoration,
onFieldSubmitted: widget.onSavedSubmitted, validator: widget.validator,
onChanged: widget.onChanged, onEditingComplete: () => widget.onSavedSubmitted(controller.value.text),
onSaved: widget.onSavedSubmitted,
onFieldSubmitted: widget.onSavedSubmitted,
onChanged: widget.onChanged,
),
), ),
), ),
); );
...@@ -1731,16 +1774,17 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> { ...@@ -1731,16 +1774,17 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
/// selected [TimeOfDay] if the user taps the "OK" button, or null if the user /// selected [TimeOfDay] if the user taps the "OK" button, or null if the user
/// taps the "CANCEL" button. The selected time is reported by calling /// taps the "CANCEL" button. The selected time is reported by calling
/// [Navigator.pop]. /// [Navigator.pop].
class _TimePickerDialog extends StatefulWidget { class TimePickerDialog extends StatefulWidget {
/// Creates a material time picker. /// Creates a material time picker.
/// ///
/// [initialTime] must not be null. /// [initialTime] must not be null.
const _TimePickerDialog({ const TimePickerDialog({
Key? key, Key? key,
required this.initialTime, required this.initialTime,
required this.cancelText, this.cancelText,
required this.confirmText, this.confirmText,
required this.helpText, this.helpText,
this.restorationId,
this.initialEntryMode = TimePickerEntryMode.dial, this.initialEntryMode = TimePickerEntryMode.dial,
}) : assert(initialTime != null), }) : assert(initialTime != null),
super(key: key); super(key: key);
...@@ -1764,21 +1808,115 @@ class _TimePickerDialog extends StatefulWidget { ...@@ -1764,21 +1808,115 @@ class _TimePickerDialog extends StatefulWidget {
/// Optionally provide your own help text to the header of the time picker. /// Optionally provide your own help text to the header of the time picker.
final String? helpText; final String? helpText;
/// Restoration ID to save and restore the state of the [TimePickerDialog].
///
/// If it is non-null, the time picker will persist and restore the
/// dialog's state.
///
/// The state of this widget is persisted in a [RestorationBucket] claimed
/// from the surrounding [RestorationScope] using the provided restoration ID.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationId;
@override @override
_TimePickerDialogState createState() => _TimePickerDialogState(); _TimePickerDialogState createState() => _TimePickerDialogState();
} }
class _TimePickerDialogState extends State<_TimePickerDialog> { // A restorable [TimePickerEntryMode] value.
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); //
// This serializes each entry as a unique `int` value.
class _RestorableTimePickerEntryMode extends RestorableValue<TimePickerEntryMode> {
_RestorableTimePickerEntryMode(
TimePickerEntryMode defaultValue,
) : _defaultValue = defaultValue;
final TimePickerEntryMode _defaultValue;
@override @override
void initState() { TimePickerEntryMode createDefaultValue() => _defaultValue;
super.initState();
_selectedTime = widget.initialTime; @override
_entryMode = widget.initialEntryMode; void didUpdateValue(TimePickerEntryMode? oldValue) {
_autoValidate = false; assert(debugIsSerializableForRestoration(value.index));
notifyListeners();
} }
@override
TimePickerEntryMode fromPrimitives(Object? data) => TimePickerEntryMode.values[data! as int];
@override
Object? toPrimitives() => value.index;
}
// A restorable [_RestorableTimePickerEntryMode] value.
//
// This serializes each entry as a unique `int` value.
class _RestorableTimePickerMode extends RestorableValue<_TimePickerMode> {
_RestorableTimePickerMode(
_TimePickerMode defaultValue,
) : _defaultValue = defaultValue;
final _TimePickerMode _defaultValue;
@override
_TimePickerMode createDefaultValue() => _defaultValue;
@override
void didUpdateValue(_TimePickerMode? oldValue) {
assert(debugIsSerializableForRestoration(value.index));
notifyListeners();
}
@override
_TimePickerMode fromPrimitives(Object? data) => _TimePickerMode.values[data! as int];
@override
Object? toPrimitives() => value.index;
}
// A restorable [_RestorableTimePickerEntryMode] value.
//
// This serializes each entry as a unique `int` value.
//
// This value can be null.
class _RestorableTimePickerModeN extends RestorableValue<_TimePickerMode?> {
_RestorableTimePickerModeN(
_TimePickerMode? defaultValue,
) : _defaultValue = defaultValue;
final _TimePickerMode? _defaultValue;
@override
_TimePickerMode? createDefaultValue() => _defaultValue;
@override
void didUpdateValue(_TimePickerMode? oldValue) {
assert(debugIsSerializableForRestoration(value?.index));
notifyListeners();
}
@override
_TimePickerMode fromPrimitives(Object? data) => _TimePickerMode.values[data! as int];
@override
Object? toPrimitives() => value?.index;
}
class _TimePickerDialogState extends State<TimePickerDialog> with RestorationMixin {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late final _RestorableTimePickerEntryMode _entryMode = _RestorableTimePickerEntryMode(widget.initialEntryMode);
final _RestorableTimePickerMode _mode = _RestorableTimePickerMode(_TimePickerMode.hour);
final _RestorableTimePickerModeN _lastModeAnnounced = _RestorableTimePickerModeN(null);
final RestorableBool _autoValidate = RestorableBool(false);
final RestorableBoolN _autofocusHour = RestorableBoolN(null);
final RestorableBoolN _autofocusMinute = RestorableBoolN(null);
final RestorableBool _announcedInitialTime = RestorableBool(false);
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
...@@ -1787,15 +1925,23 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1787,15 +1925,23 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
_announceModeOnce(); _announceModeOnce();
} }
late TimePickerEntryMode _entryMode; @override
_TimePickerMode _mode = _TimePickerMode.hour; String? get restorationId => widget.restorationId;
_TimePickerMode? _lastModeAnnounced;
late bool _autoValidate; @override
bool? _autofocusHour; void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
bool? _autofocusMinute; registerForRestoration(_entryMode, 'entry_mode');
registerForRestoration(_mode, 'mode');
registerForRestoration(_lastModeAnnounced, 'last_mode_announced');
registerForRestoration(_autoValidate, 'autovalidate');
registerForRestoration(_autofocusHour, 'autofocus_hour');
registerForRestoration(_autofocusMinute, 'autofocus_minute');
registerForRestoration(_announcedInitialTime, 'announced_initial_time');
registerForRestoration(_selectedTime, 'selected_time');
}
TimeOfDay get selectedTime => _selectedTime; RestorableTimeOfDay get selectedTime => _selectedTime;
late TimeOfDay _selectedTime; late final RestorableTimeOfDay _selectedTime = RestorableTimeOfDay(widget.initialTime);
Timer? _vibrateTimer; Timer? _vibrateTimer;
late MaterialLocalizations localizations; late MaterialLocalizations localizations;
...@@ -1821,35 +1967,35 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1821,35 +1967,35 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
void _handleModeChanged(_TimePickerMode mode) { void _handleModeChanged(_TimePickerMode mode) {
_vibrate(); _vibrate();
setState(() { setState(() {
_mode = mode; _mode.value = mode;
_announceModeOnce(); _announceModeOnce();
}); });
} }
void _handleEntryModeToggle() { void _handleEntryModeToggle() {
setState(() { setState(() {
switch (_entryMode) { switch (_entryMode.value) {
case TimePickerEntryMode.dial: case TimePickerEntryMode.dial:
_autoValidate = false; _autoValidate.value = false;
_entryMode = TimePickerEntryMode.input; _entryMode.value = TimePickerEntryMode.input;
break; break;
case TimePickerEntryMode.input: case TimePickerEntryMode.input:
_formKey.currentState!.save(); _formKey.currentState!.save();
_autofocusHour = false; _autofocusHour.value = false;
_autofocusMinute = false; _autofocusMinute.value = false;
_entryMode = TimePickerEntryMode.dial; _entryMode.value = TimePickerEntryMode.dial;
break; break;
} }
}); });
} }
void _announceModeOnce() { void _announceModeOnce() {
if (_lastModeAnnounced == _mode) { if (_lastModeAnnounced.value == _mode.value) {
// Already announced it. // Already announced it.
return; return;
} }
switch (_mode) { switch (_mode.value) {
case _TimePickerMode.hour: case _TimePickerMode.hour:
_announceToAccessibility(context, localizations.timePickerHourModeAnnouncement); _announceToAccessibility(context, localizations.timePickerHourModeAnnouncement);
break; break;
...@@ -1857,13 +2003,11 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1857,13 +2003,11 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
_announceToAccessibility(context, localizations.timePickerMinuteModeAnnouncement); _announceToAccessibility(context, localizations.timePickerMinuteModeAnnouncement);
break; break;
} }
_lastModeAnnounced = _mode; _lastModeAnnounced.value = _mode.value;
} }
bool _announcedInitialTime = false;
void _announceInitialTimeOnce() { void _announceInitialTimeOnce() {
if (_announcedInitialTime) if (_announcedInitialTime.value)
return; return;
final MediaQueryData media = MediaQuery.of(context); final MediaQueryData media = MediaQuery.of(context);
...@@ -1872,29 +2016,29 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1872,29 +2016,29 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
context, context,
localizations.formatTimeOfDay(widget.initialTime, alwaysUse24HourFormat: media.alwaysUse24HourFormat), localizations.formatTimeOfDay(widget.initialTime, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
); );
_announcedInitialTime = true; _announcedInitialTime.value = true;
} }
void _handleTimeChanged(TimeOfDay value) { void _handleTimeChanged(TimeOfDay value) {
_vibrate(); _vibrate();
setState(() { setState(() {
_selectedTime = value; _selectedTime.value = value;
}); });
} }
void _handleHourDoubleTapped() { void _handleHourDoubleTapped() {
_autofocusHour = true; _autofocusHour.value = true;
_handleEntryModeToggle(); _handleEntryModeToggle();
} }
void _handleMinuteDoubleTapped() { void _handleMinuteDoubleTapped() {
_autofocusMinute = true; _autofocusMinute.value = true;
_handleEntryModeToggle(); _handleEntryModeToggle();
} }
void _handleHourSelected() { void _handleHourSelected() {
setState(() { setState(() {
_mode = _TimePickerMode.minute; _mode.value = _TimePickerMode.minute;
}); });
} }
...@@ -1903,15 +2047,15 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1903,15 +2047,15 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
} }
void _handleOk() { void _handleOk() {
if (_entryMode == TimePickerEntryMode.input) { if (_entryMode.value == TimePickerEntryMode.input) {
final FormState form = _formKey.currentState!; final FormState form = _formKey.currentState!;
if (!form.validate()) { if (!form.validate()) {
setState(() { _autoValidate = true; }); setState(() { _autoValidate.value = true; });
return; return;
} }
form.save(); form.save();
} }
Navigator.pop(context, _selectedTime); Navigator.pop(context, _selectedTime.value);
} }
Size _dialogSize(BuildContext context) { Size _dialogSize(BuildContext context) {
...@@ -1924,7 +2068,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1924,7 +2068,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
final double timePickerWidth; final double timePickerWidth;
final double timePickerHeight; final double timePickerHeight;
switch (_entryMode) { switch (_entryMode.value) {
case TimePickerEntryMode.dial: case TimePickerEntryMode.dial:
switch (orientation) { switch (orientation) {
case Orientation.portrait: case Orientation.portrait:
...@@ -1967,8 +2111,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1967,8 +2111,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
theme.colorScheme.brightness == Brightness.dark ? 1.0 : 0.6, theme.colorScheme.brightness == Brightness.dark ? 1.0 : 0.6,
), ),
onPressed: _handleEntryModeToggle, onPressed: _handleEntryModeToggle,
icon: Icon(_entryMode == TimePickerEntryMode.dial ? Icons.keyboard : Icons.access_time), icon: Icon(_entryMode.value == TimePickerEntryMode.dial ? Icons.keyboard : Icons.access_time),
tooltip: _entryMode == TimePickerEntryMode.dial tooltip: _entryMode.value == TimePickerEntryMode.dial
? MaterialLocalizations.of(context).inputTimeModeButtonLabel ? MaterialLocalizations.of(context).inputTimeModeButtonLabel
: MaterialLocalizations.of(context).dialModeButtonLabel, : MaterialLocalizations.of(context).dialModeButtonLabel,
), ),
...@@ -1997,7 +2141,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1997,7 +2141,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
); );
final Widget picker; final Widget picker;
switch (_entryMode) { switch (_entryMode.value) {
case TimePickerEntryMode.dial: case TimePickerEntryMode.dial:
final Widget dial = Padding( final Widget dial = Padding(
padding: orientation == Orientation.portrait ? const EdgeInsets.symmetric(horizontal: 36, vertical: 24) : const EdgeInsets.all(24), padding: orientation == Orientation.portrait ? const EdgeInsets.symmetric(horizontal: 36, vertical: 24) : const EdgeInsets.all(24),
...@@ -2005,9 +2149,9 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -2005,9 +2149,9 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
child: AspectRatio( child: AspectRatio(
aspectRatio: 1.0, aspectRatio: 1.0,
child: _Dial( child: _Dial(
mode: _mode, mode: _mode.value,
use24HourDials: use24HourDials, use24HourDials: use24HourDials,
selectedTime: _selectedTime, selectedTime: _selectedTime.value,
onChanged: _handleTimeChanged, onChanged: _handleTimeChanged,
onHourSelected: _handleHourSelected, onHourSelected: _handleHourSelected,
), ),
...@@ -2016,8 +2160,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -2016,8 +2160,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
); );
final Widget header = _TimePickerHeader( final Widget header = _TimePickerHeader(
selectedTime: _selectedTime, selectedTime: _selectedTime.value,
mode: _mode, mode: _mode.value,
orientation: orientation, orientation: orientation,
onModeChanged: _handleModeChanged, onModeChanged: _handleModeChanged,
onChanged: _handleTimeChanged, onChanged: _handleTimeChanged,
...@@ -2067,17 +2211,19 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -2067,17 +2211,19 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
case TimePickerEntryMode.input: case TimePickerEntryMode.input:
picker = Form( picker = Form(
key: _formKey, key: _formKey,
autovalidate: _autoValidate, autovalidate: _autoValidate.value,
child: SingleChildScrollView( child: SingleChildScrollView(
restorationId: 'time_picker_scroll_view',
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
_TimePickerInput( _TimePickerInput(
initialSelectedTime: _selectedTime, initialSelectedTime: _selectedTime.value,
helpText: widget.helpText, helpText: widget.helpText,
autofocusHour: _autofocusHour, autofocusHour: _autofocusHour.value,
autofocusMinute: _autofocusMinute, autofocusMinute: _autofocusMinute.value,
onChanged: _handleTimeChanged, onChanged: _handleTimeChanged,
restorationId: 'time_picker_input',
), ),
actions, actions,
], ],
...@@ -2093,7 +2239,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -2093,7 +2239,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
backgroundColor: TimePickerTheme.of(context).backgroundColor ?? theme.colorScheme.surface, backgroundColor: TimePickerTheme.of(context).backgroundColor ?? theme.colorScheme.surface,
insetPadding: EdgeInsets.symmetric( insetPadding: EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
vertical: _entryMode == TimePickerEntryMode.input ? 0.0 : 24.0, vertical: _entryMode.value == TimePickerEntryMode.input ? 0.0 : 24.0,
), ),
child: AnimatedContainer( child: AnimatedContainer(
width: dialogSize.width, width: dialogSize.width,
...@@ -2204,7 +2350,7 @@ Future<TimeOfDay?> showTimePicker({ ...@@ -2204,7 +2350,7 @@ Future<TimeOfDay?> showTimePicker({
assert(initialEntryMode != null); assert(initialEntryMode != null);
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
final Widget dialog = _TimePickerDialog( final Widget dialog = TimePickerDialog(
initialTime: initialTime, initialTime: initialTime,
initialEntryMode: initialEntryMode, initialEntryMode: initialEntryMode,
cancelText: cancelText, cancelText: cancelText,
......
...@@ -12,40 +12,88 @@ import 'feedback_tester.dart'; ...@@ -12,40 +12,88 @@ import 'feedback_tester.dart';
final Finder _hourControl = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl'); final Finder _hourControl = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_MinuteControl'); final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_MinuteControl');
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_TimePickerDialog'); final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == 'TimePickerDialog');
class _TimePickerLauncher extends StatelessWidget { class _TimePickerLauncher extends StatefulWidget {
const _TimePickerLauncher({ const _TimePickerLauncher({
Key? key, Key? key,
required this.onChanged, required this.onChanged,
this.locale,
this.entryMode = TimePickerEntryMode.dial, this.entryMode = TimePickerEntryMode.dial,
this.restorationId,
}) : super(key: key); }) : super(key: key);
final ValueChanged<TimeOfDay?> onChanged; final ValueChanged<TimeOfDay?> onChanged;
final Locale? locale;
final TimePickerEntryMode entryMode; final TimePickerEntryMode entryMode;
final String? restorationId;
@override
_TimePickerLauncherState createState() => _TimePickerLauncherState();
}
class _TimePickerLauncherState extends State<_TimePickerLauncher> with RestorationMixin {
@override
String? get restorationId => widget.restorationId;
late final RestorableRouteFuture<TimeOfDay?> _restorableTimePickerRouteFuture = RestorableRouteFuture<TimeOfDay?>(
onComplete: _selectTime,
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(
_timePickerRoute,
arguments: <String, int>{
'entryMode': widget.entryMode.index,
},
);
},
);
static Route<TimeOfDay> _timePickerRoute(
BuildContext context,
Object? arguments,
) {
final Map<dynamic, dynamic> args = arguments! as Map<dynamic, dynamic>;
final TimePickerEntryMode entryMode = TimePickerEntryMode.values[args['entryMode'] as int];
return DialogRoute<TimeOfDay>(
context: context,
builder: (BuildContext context) {
return TimePickerDialog(
restorationId: 'time_picker_dialog',
initialTime: const TimeOfDay(hour: 7, minute: 0),
initialEntryMode: entryMode,
);
},
);
}
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_restorableTimePickerRouteFuture, 'time_picker_route_future');
}
void _selectTime(TimeOfDay? newSelectedTime) {
widget.onChanged(newSelectedTime);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return Material(
locale: locale, child: Center(
home: Material( child: Builder(
child: Center( builder: (BuildContext context) {
child: Builder( return ElevatedButton(
builder: (BuildContext context) { child: const Text('X'),
return ElevatedButton( onPressed: () async {
child: const Text('X'), if (widget.restorationId == null) {
onPressed: () async { widget.onChanged(await showTimePicker(
onChanged(await showTimePicker( context: context,
context: context, initialTime: const TimeOfDay(hour: 7, minute: 0),
initialTime: const TimeOfDay(hour: 7, minute: 0), initialEntryMode: widget.entryMode,
initialEntryMode: entryMode, ));
)); } else {
}, _restorableTimePickerRouteFuture.present();
); }
} },
), );
},
), ),
), ),
); );
...@@ -56,8 +104,17 @@ Future<Offset?> startPicker( ...@@ -56,8 +104,17 @@ Future<Offset?> startPicker(
WidgetTester tester, WidgetTester tester,
ValueChanged<TimeOfDay?> onChanged, { ValueChanged<TimeOfDay?> onChanged, {
TimePickerEntryMode entryMode = TimePickerEntryMode.dial, TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
String? restorationId,
}) async { }) async {
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US'), entryMode: entryMode)); await tester.pumpWidget(MaterialApp(
restorationScopeId: 'app',
locale: const Locale('en', 'US'),
home: _TimePickerLauncher(
onChanged: onChanged,
entryMode: entryMode,
restorationId: restorationId,
),
));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null; return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null;
...@@ -428,7 +485,7 @@ void _tests() { ...@@ -428,7 +485,7 @@ void _tests() {
// Ensure we preserve day period as we roll over. // Ensure we preserve day period as we roll over.
final dynamic pickerState = tester.state(_timePickerDialog); final dynamic pickerState = tester.state(_timePickerDialog);
expect(pickerState.selectedTime, const TimeOfDay(hour: 1, minute: 0)); expect(pickerState.selectedTime.value, const TimeOfDay(hour: 1, minute: 0));
await actAndExpect( await actAndExpect(
initialValue: '1', initialValue: '1',
...@@ -493,7 +550,7 @@ void _tests() { ...@@ -493,7 +550,7 @@ void _tests() {
// Ensure we preserve hour period as we roll over. // Ensure we preserve hour period as we roll over.
final dynamic pickerState = tester.state(_timePickerDialog); final dynamic pickerState = tester.state(_timePickerDialog);
expect(pickerState.selectedTime, const TimeOfDay(hour: 11, minute: 0)); expect(pickerState.selectedTime.value, const TimeOfDay(hour: 11, minute: 0));
await actAndExpect( await actAndExpect(
initialValue: '00', initialValue: '00',
...@@ -939,6 +996,102 @@ void _testsInput() { ...@@ -939,6 +996,102 @@ void _testsInput() {
expect(hourFieldTop, separatorTop); expect(hourFieldTop, separatorTop);
expect(minuteFieldTop, separatorTop); expect(minuteFieldTop, separatorTop);
}); });
testWidgets('Time Picker state restoration test - dial mode', (WidgetTester tester) async {
TimeOfDay? result;
final Offset center = (await startPicker(
tester,
(TimeOfDay? time) { result = time; },
restorationId: 'restorable_time_picker',
))!;
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)
await tester.tapAt(hour6);
await tester.pump(const Duration(milliseconds: 50));
await tester.restartAndRestore();
await tester.tapAt(min45);
await tester.pump(const Duration(milliseconds: 50));
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
// Setting to PM adds 12 hours (18:45)
await tester.tap(find.text('PM'));
await tester.pump(const Duration(milliseconds: 50));
await tester.restartAndRestore();
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 18, minute: 45)));
// Test restoring from before PM was selected (6:45)
await tester.restoreFrom(restorationData);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
});
testWidgets('Time Picker state restoration test - input mode', (WidgetTester tester) async {
TimeOfDay? result;
await startPicker(
tester,
(TimeOfDay? time) { result = time; },
entryMode: TimePickerEntryMode.input,
restorationId: 'restorable_time_picker',
);
await tester.enterText(find.byType(TextField).first, '9');
await tester.pump(const Duration(milliseconds: 50));
await tester.restartAndRestore();
await tester.enterText(find.byType(TextField).last, '12');
await tester.pump(const Duration(milliseconds: 50));
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
// Setting to PM adds 12 hours (21:12)
await tester.tap(find.text('PM'));
await tester.pump(const Duration(milliseconds: 50));
await tester.restartAndRestore();
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 21, minute: 12)));
// Restoring from before PM was set (9:12)
await tester.restoreFrom(restorationData);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
});
testWidgets('Time Picker state restoration test - switching modes', (WidgetTester tester) async {
TimeOfDay? result;
final Offset center = (await startPicker(
tester,
(TimeOfDay? time) { result = time; },
restorationId: 'restorable_time_picker',
))!;
final TestRestorationData restorationData = await tester.getRestorationData();
// Switch to input mode from dial mode.
await tester.tap(find.byIcon(Icons.keyboard));
await tester.pump(const Duration(milliseconds: 50));
await tester.restartAndRestore();
// Select time using input mode controls.
await tester.enterText(find.byType(TextField).first, '9');
await tester.enterText(find.byType(TextField).last, '12');
await tester.pump(const Duration(milliseconds: 50));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
// Restoring from dial mode.
await tester.restoreFrom(restorationData);
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)
await tester.tapAt(hour6);
await tester.pump(const Duration(milliseconds: 50));
await tester.restartAndRestore();
await tester.tapAt(min45);
await tester.pump(const Duration(milliseconds: 50));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
});
} }
final Finder findDialPaint = find.descendant( final Finder findDialPaint = find.descendant(
......
...@@ -26,4 +26,143 @@ void main() { ...@@ -26,4 +26,143 @@ void main() {
expect(await pumpTest(true), '07:00'); expect(await pumpTest(true), '07:00');
}); });
}); });
group('RestorableTimeOfDay tests', () {
testWidgets('value is not accessible when not registered', (WidgetTester tester) async {
expect(() => RestorableTimeOfDay(const TimeOfDay(hour: 20, minute: 4)).value, throwsAssertionError);
});
testWidgets('work when not in restoration scope', (WidgetTester tester) async {
await tester.pumpWidget(const _RestorableWidget());
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
// Initialized to default values.
expect(state.timeOfDay.value, const TimeOfDay(hour: 10, minute: 5));
// Modify values.
state.setProperties(() {
state.timeOfDay.value = const TimeOfDay(hour: 2, minute: 2);
});
await tester.pump();
expect(state.timeOfDay.value, const TimeOfDay(hour: 2, minute: 2));
});
testWidgets('restart and restore', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
_RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
// Initialized to default values.
expect(state.timeOfDay.value, const TimeOfDay(hour: 10, minute: 5));
// Modify values.
state.setProperties(() {
state.timeOfDay.value = const TimeOfDay(hour: 2, minute: 2);
});
await tester.pump();
expect(state.timeOfDay.value, const TimeOfDay(hour: 2, minute: 2));
// Restores to previous values.
await tester.restartAndRestore();
final _RestorableWidgetState oldState = state;
state = tester.state(find.byType(_RestorableWidget));
expect(state, isNot(same(oldState)));
expect(state.timeOfDay.value, const TimeOfDay(hour: 2, minute: 2));
});
testWidgets('restore to older state', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
// Modify values.
state.setProperties(() {
state.timeOfDay.value = const TimeOfDay(hour: 2, minute: 2);
});
await tester.pump();
final TestRestorationData restorationData = await tester.getRestorationData();
// Modify values.
state.setProperties(() {
state.timeOfDay.value = const TimeOfDay(hour: 4, minute: 4);
});
await tester.pump();
// Restore to previous.
await tester.restoreFrom(restorationData);
expect(state.timeOfDay.value, const TimeOfDay(hour: 2, minute: 2));
// Restore to empty data will re-initialize to default values.
await tester.restoreFrom(TestRestorationData.empty);
expect(state.timeOfDay.value, const TimeOfDay(hour: 10, minute: 5));
});
testWidgets('call notifiers when value changes', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
final List<String> notifyLog = <String>[];
state.timeOfDay.addListener(() {
notifyLog.add('hello world');
});
state.setProperties(() {
state.timeOfDay.value = const TimeOfDay(hour: 2, minute: 2);
});
expect(notifyLog.single, 'hello world');
notifyLog.clear();
await tester.pump();
// Does not notify when set to same value.
state.setProperties(() {
state.timeOfDay.value = const TimeOfDay(hour: 2, minute: 2);
});
expect(notifyLog, isEmpty);
});
});
}
class _RestorableWidget extends StatefulWidget {
const _RestorableWidget({Key? key}) : super(key: key);
@override
State<_RestorableWidget> createState() => _RestorableWidgetState();
}
class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMixin {
final RestorableTimeOfDay timeOfDay = RestorableTimeOfDay(const TimeOfDay(hour: 10, minute: 5));
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(timeOfDay, 'time_of_day');
}
void setProperties(VoidCallback callback) {
setState(callback);
}
@override
Widget build(BuildContext context) {
return const SizedBox();
}
@override
String get restorationId => 'widget';
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment