mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[State Restoration] Restorable TimePickerDialog widget, RestorableTimeOfDay (#80566)
This commit is contained in:
parent
e6d4b8cf0b
commit
07849778eb
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'debug.dart';
|
||||
import 'material_localizations.dart';
|
||||
@ -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
|
||||
/// lays out the time controls.
|
||||
///
|
||||
|
||||
@ -1293,6 +1293,7 @@ class _TimePickerInput extends StatefulWidget {
|
||||
required this.autofocusHour,
|
||||
required this.autofocusMinute,
|
||||
required this.onChanged,
|
||||
this.restorationId,
|
||||
}) : assert(initialSelectedTime != null),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
@ -1309,19 +1310,32 @@ class _TimePickerInput extends StatefulWidget {
|
||||
|
||||
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
|
||||
_TimePickerInputState createState() => _TimePickerInputState();
|
||||
}
|
||||
|
||||
class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
late TimeOfDay _selectedTime;
|
||||
bool hourHasError = false;
|
||||
bool minuteHasError = false;
|
||||
class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixin {
|
||||
late final RestorableTimeOfDay _selectedTime = RestorableTimeOfDay(widget.initialSelectedTime);
|
||||
final RestorableBool hourHasError = RestorableBool(false);
|
||||
final RestorableBool minuteHasError = RestorableBool(false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedTime = widget.initialSelectedTime;
|
||||
String? get restorationId => widget.restorationId;
|
||||
|
||||
@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) {
|
||||
@ -1340,8 +1354,8 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
}
|
||||
} else {
|
||||
if (newHour > 0 && newHour < 13) {
|
||||
if ((_selectedTime.period == DayPeriod.pm && newHour != 12)
|
||||
|| (_selectedTime.period == DayPeriod.am && newHour == 12)) {
|
||||
if ((_selectedTime.value.period == DayPeriod.pm && newHour != 12)
|
||||
|| (_selectedTime.value.period == DayPeriod.am && newHour == 12)) {
|
||||
newHour = (newHour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
|
||||
}
|
||||
return newHour;
|
||||
@ -1369,8 +1383,8 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
void _handleHourSavedSubmitted(String? value) {
|
||||
final int? newHour = _parseHour(value);
|
||||
if (newHour != null) {
|
||||
_selectedTime = TimeOfDay(hour: newHour, minute: _selectedTime.minute);
|
||||
widget.onChanged(_selectedTime);
|
||||
_selectedTime.value = TimeOfDay(hour: newHour, minute: _selectedTime.value.minute);
|
||||
widget.onChanged(_selectedTime.value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1385,20 +1399,20 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
void _handleMinuteSavedSubmitted(String? value) {
|
||||
final int? newMinute = _parseMinute(value);
|
||||
if (newMinute != null) {
|
||||
_selectedTime = TimeOfDay(hour: _selectedTime.hour, minute: int.parse(value!));
|
||||
widget.onChanged(_selectedTime);
|
||||
_selectedTime.value = TimeOfDay(hour: _selectedTime.value.hour, minute: int.parse(value!));
|
||||
widget.onChanged(_selectedTime.value);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDayPeriodChanged(TimeOfDay value) {
|
||||
_selectedTime = value;
|
||||
widget.onChanged(_selectedTime);
|
||||
_selectedTime.value = value;
|
||||
widget.onChanged(_selectedTime.value);
|
||||
}
|
||||
|
||||
String? _validateHour(String? value) {
|
||||
final int? newHour = _parseHour(value);
|
||||
setState(() {
|
||||
hourHasError = newHour == null;
|
||||
hourHasError.value = newHour == null;
|
||||
});
|
||||
// This is used as the validator for the [TextFormField].
|
||||
// Returning an empty string allows the field to go into an error state.
|
||||
@ -1409,7 +1423,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
String? _validateMinute(String? value) {
|
||||
final int? newMinute = _parseMinute(value);
|
||||
setState(() {
|
||||
minuteHasError = newMinute == null;
|
||||
minuteHasError.value = newMinute == null;
|
||||
});
|
||||
// This is used as the validator for the [TextFormField].
|
||||
// Returning an empty string allows the field to go into an error state.
|
||||
@ -1441,7 +1455,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
children: <Widget>[
|
||||
if (!use24HourDials && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
|
||||
_DayPeriodControl(
|
||||
selectedTime: _selectedTime,
|
||||
selectedTime: _selectedTime.value,
|
||||
orientation: Orientation.portrait,
|
||||
onChanged: _handleDayPeriodChanged,
|
||||
),
|
||||
@ -1459,7 +1473,8 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 8.0),
|
||||
_HourTextField(
|
||||
selectedTime: _selectedTime,
|
||||
restorationId: 'hour_text_field',
|
||||
selectedTime: _selectedTime.value,
|
||||
style: hourMinuteStyle,
|
||||
autofocus: widget.autofocusHour,
|
||||
validator: _validateHour,
|
||||
@ -1467,7 +1482,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
onChanged: _handleHourChanged,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
if (!hourHasError && !minuteHasError)
|
||||
if (!hourHasError.value && !minuteHasError.value)
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
MaterialLocalizations.of(context).timePickerHourLabel,
|
||||
@ -1490,14 +1505,15 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 8.0),
|
||||
_MinuteTextField(
|
||||
selectedTime: _selectedTime,
|
||||
restorationId: 'minute_text_field',
|
||||
selectedTime: _selectedTime.value,
|
||||
style: hourMinuteStyle,
|
||||
autofocus: widget.autofocusMinute,
|
||||
validator: _validateMinute,
|
||||
onSavedSubmitted: _handleMinuteSavedSubmitted,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
if (!hourHasError && !minuteHasError)
|
||||
if (!hourHasError.value && !minuteHasError.value)
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
MaterialLocalizations.of(context).timePickerMinuteLabel,
|
||||
@ -1515,14 +1531,14 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||
if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
|
||||
const SizedBox(width: 12.0),
|
||||
_DayPeriodControl(
|
||||
selectedTime: _selectedTime,
|
||||
selectedTime: _selectedTime.value,
|
||||
orientation: Orientation.portrait,
|
||||
onChanged: _handleDayPeriodChanged,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (hourHasError || minuteHasError)
|
||||
if (hourHasError.value || minuteHasError.value)
|
||||
Text(
|
||||
MaterialLocalizations.of(context).invalidTimeLabel,
|
||||
style: theme.textTheme.bodyText2!.copyWith(color: theme.colorScheme.error),
|
||||
@ -1544,6 +1560,7 @@ class _HourTextField extends StatelessWidget {
|
||||
required this.validator,
|
||||
required this.onSavedSubmitted,
|
||||
required this.onChanged,
|
||||
this.restorationId,
|
||||
}) : super(key: key);
|
||||
|
||||
final TimeOfDay selectedTime;
|
||||
@ -1552,10 +1569,12 @@ class _HourTextField extends StatelessWidget {
|
||||
final FormFieldValidator<String> validator;
|
||||
final ValueChanged<String?> onSavedSubmitted;
|
||||
final ValueChanged<String> onChanged;
|
||||
final String? restorationId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _HourMinuteTextField(
|
||||
restorationId: restorationId,
|
||||
selectedTime: selectedTime,
|
||||
isHour: true,
|
||||
autofocus: autofocus,
|
||||
@ -1576,6 +1595,7 @@ class _MinuteTextField extends StatelessWidget {
|
||||
required this.autofocus,
|
||||
required this.validator,
|
||||
required this.onSavedSubmitted,
|
||||
this.restorationId,
|
||||
}) : super(key: key);
|
||||
|
||||
final TimeOfDay selectedTime;
|
||||
@ -1583,10 +1603,12 @@ class _MinuteTextField extends StatelessWidget {
|
||||
final bool? autofocus;
|
||||
final FormFieldValidator<String> validator;
|
||||
final ValueChanged<String?> onSavedSubmitted;
|
||||
final String? restorationId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _HourMinuteTextField(
|
||||
restorationId: restorationId,
|
||||
selectedTime: selectedTime,
|
||||
isHour: false,
|
||||
autofocus: autofocus,
|
||||
@ -1608,6 +1630,7 @@ class _HourMinuteTextField extends StatefulWidget {
|
||||
required this.semanticHintText,
|
||||
required this.validator,
|
||||
required this.onSavedSubmitted,
|
||||
this.restorationId,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -1619,13 +1642,15 @@ class _HourMinuteTextField extends StatefulWidget {
|
||||
final FormFieldValidator<String> validator;
|
||||
final ValueChanged<String?> onSavedSubmitted;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final String? restorationId;
|
||||
|
||||
@override
|
||||
_HourMinuteTextFieldState createState() => _HourMinuteTextFieldState();
|
||||
}
|
||||
|
||||
class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
|
||||
TextEditingController? controller;
|
||||
class _HourMinuteTextFieldState extends State<_HourMinuteTextField> with RestorationMixin {
|
||||
final RestorableTextEditingController controller = RestorableTextEditingController();
|
||||
final RestorableBool controllerHasBeenSet = RestorableBool(false);
|
||||
late FocusNode focusNode;
|
||||
|
||||
@override
|
||||
@ -1639,7 +1664,21 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
|
||||
@override
|
||||
void 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 {
|
||||
@ -1701,24 +1740,28 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
|
||||
height: _kTimePickerHeaderControlHeight,
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: TextFormField(
|
||||
autofocus: widget.autofocus ?? false,
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
LengthLimitingTextInputFormatter(2),
|
||||
],
|
||||
focusNode: focusNode,
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
style: widget.style.copyWith(color: timePickerTheme.hourMinuteTextColor ?? colorScheme.onSurface),
|
||||
controller: controller,
|
||||
decoration: inputDecoration,
|
||||
validator: widget.validator,
|
||||
onEditingComplete: () => widget.onSavedSubmitted(controller!.text),
|
||||
onSaved: widget.onSavedSubmitted,
|
||||
onFieldSubmitted: widget.onSavedSubmitted,
|
||||
onChanged: widget.onChanged,
|
||||
child: UnmanagedRestorationScope(
|
||||
bucket: bucket,
|
||||
child: TextFormField(
|
||||
restorationId: 'hour_minute_text_form_field',
|
||||
autofocus: widget.autofocus ?? false,
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
LengthLimitingTextInputFormatter(2),
|
||||
],
|
||||
focusNode: focusNode,
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
style: widget.style.copyWith(color: timePickerTheme.hourMinuteTextColor ?? colorScheme.onSurface),
|
||||
controller: controller.value,
|
||||
decoration: inputDecoration,
|
||||
validator: widget.validator,
|
||||
onEditingComplete: () => widget.onSavedSubmitted(controller.value.text),
|
||||
onSaved: widget.onSavedSubmitted,
|
||||
onFieldSubmitted: widget.onSavedSubmitted,
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -1731,16 +1774,17 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
|
||||
/// 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
|
||||
/// [Navigator.pop].
|
||||
class _TimePickerDialog extends StatefulWidget {
|
||||
class TimePickerDialog extends StatefulWidget {
|
||||
/// Creates a material time picker.
|
||||
///
|
||||
/// [initialTime] must not be null.
|
||||
const _TimePickerDialog({
|
||||
const TimePickerDialog({
|
||||
Key? key,
|
||||
required this.initialTime,
|
||||
required this.cancelText,
|
||||
required this.confirmText,
|
||||
required this.helpText,
|
||||
this.cancelText,
|
||||
this.confirmText,
|
||||
this.helpText,
|
||||
this.restorationId,
|
||||
this.initialEntryMode = TimePickerEntryMode.dial,
|
||||
}) : assert(initialTime != null),
|
||||
super(key: key);
|
||||
@ -1764,21 +1808,115 @@ class _TimePickerDialog extends StatefulWidget {
|
||||
/// Optionally provide your own help text to the header of the time picker.
|
||||
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
|
||||
_TimePickerDialogState createState() => _TimePickerDialogState();
|
||||
}
|
||||
|
||||
class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
// A restorable [TimePickerEntryMode] value.
|
||||
//
|
||||
// This serializes each entry as a unique `int` value.
|
||||
class _RestorableTimePickerEntryMode extends RestorableValue<TimePickerEntryMode> {
|
||||
_RestorableTimePickerEntryMode(
|
||||
TimePickerEntryMode defaultValue,
|
||||
) : _defaultValue = defaultValue;
|
||||
|
||||
final TimePickerEntryMode _defaultValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedTime = widget.initialTime;
|
||||
_entryMode = widget.initialEntryMode;
|
||||
_autoValidate = false;
|
||||
TimePickerEntryMode createDefaultValue() => _defaultValue;
|
||||
|
||||
@override
|
||||
void didUpdateValue(TimePickerEntryMode? oldValue) {
|
||||
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
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@ -1787,15 +1925,23 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
_announceModeOnce();
|
||||
}
|
||||
|
||||
late TimePickerEntryMode _entryMode;
|
||||
_TimePickerMode _mode = _TimePickerMode.hour;
|
||||
_TimePickerMode? _lastModeAnnounced;
|
||||
late bool _autoValidate;
|
||||
bool? _autofocusHour;
|
||||
bool? _autofocusMinute;
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
|
||||
TimeOfDay get selectedTime => _selectedTime;
|
||||
late TimeOfDay _selectedTime;
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
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');
|
||||
}
|
||||
|
||||
RestorableTimeOfDay get selectedTime => _selectedTime;
|
||||
late final RestorableTimeOfDay _selectedTime = RestorableTimeOfDay(widget.initialTime);
|
||||
|
||||
Timer? _vibrateTimer;
|
||||
late MaterialLocalizations localizations;
|
||||
@ -1821,35 +1967,35 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
void _handleModeChanged(_TimePickerMode mode) {
|
||||
_vibrate();
|
||||
setState(() {
|
||||
_mode = mode;
|
||||
_mode.value = mode;
|
||||
_announceModeOnce();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleEntryModeToggle() {
|
||||
setState(() {
|
||||
switch (_entryMode) {
|
||||
switch (_entryMode.value) {
|
||||
case TimePickerEntryMode.dial:
|
||||
_autoValidate = false;
|
||||
_entryMode = TimePickerEntryMode.input;
|
||||
_autoValidate.value = false;
|
||||
_entryMode.value = TimePickerEntryMode.input;
|
||||
break;
|
||||
case TimePickerEntryMode.input:
|
||||
_formKey.currentState!.save();
|
||||
_autofocusHour = false;
|
||||
_autofocusMinute = false;
|
||||
_entryMode = TimePickerEntryMode.dial;
|
||||
_autofocusHour.value = false;
|
||||
_autofocusMinute.value = false;
|
||||
_entryMode.value = TimePickerEntryMode.dial;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _announceModeOnce() {
|
||||
if (_lastModeAnnounced == _mode) {
|
||||
if (_lastModeAnnounced.value == _mode.value) {
|
||||
// Already announced it.
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_mode) {
|
||||
switch (_mode.value) {
|
||||
case _TimePickerMode.hour:
|
||||
_announceToAccessibility(context, localizations.timePickerHourModeAnnouncement);
|
||||
break;
|
||||
@ -1857,13 +2003,11 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
_announceToAccessibility(context, localizations.timePickerMinuteModeAnnouncement);
|
||||
break;
|
||||
}
|
||||
_lastModeAnnounced = _mode;
|
||||
_lastModeAnnounced.value = _mode.value;
|
||||
}
|
||||
|
||||
bool _announcedInitialTime = false;
|
||||
|
||||
void _announceInitialTimeOnce() {
|
||||
if (_announcedInitialTime)
|
||||
if (_announcedInitialTime.value)
|
||||
return;
|
||||
|
||||
final MediaQueryData media = MediaQuery.of(context);
|
||||
@ -1872,29 +2016,29 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
context,
|
||||
localizations.formatTimeOfDay(widget.initialTime, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
|
||||
);
|
||||
_announcedInitialTime = true;
|
||||
_announcedInitialTime.value = true;
|
||||
}
|
||||
|
||||
void _handleTimeChanged(TimeOfDay value) {
|
||||
_vibrate();
|
||||
setState(() {
|
||||
_selectedTime = value;
|
||||
_selectedTime.value = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleHourDoubleTapped() {
|
||||
_autofocusHour = true;
|
||||
_autofocusHour.value = true;
|
||||
_handleEntryModeToggle();
|
||||
}
|
||||
|
||||
void _handleMinuteDoubleTapped() {
|
||||
_autofocusMinute = true;
|
||||
_autofocusMinute.value = true;
|
||||
_handleEntryModeToggle();
|
||||
}
|
||||
|
||||
void _handleHourSelected() {
|
||||
setState(() {
|
||||
_mode = _TimePickerMode.minute;
|
||||
_mode.value = _TimePickerMode.minute;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1903,15 +2047,15 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
}
|
||||
|
||||
void _handleOk() {
|
||||
if (_entryMode == TimePickerEntryMode.input) {
|
||||
if (_entryMode.value == TimePickerEntryMode.input) {
|
||||
final FormState form = _formKey.currentState!;
|
||||
if (!form.validate()) {
|
||||
setState(() { _autoValidate = true; });
|
||||
setState(() { _autoValidate.value = true; });
|
||||
return;
|
||||
}
|
||||
form.save();
|
||||
}
|
||||
Navigator.pop(context, _selectedTime);
|
||||
Navigator.pop(context, _selectedTime.value);
|
||||
}
|
||||
|
||||
Size _dialogSize(BuildContext context) {
|
||||
@ -1924,7 +2068,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
|
||||
final double timePickerWidth;
|
||||
final double timePickerHeight;
|
||||
switch (_entryMode) {
|
||||
switch (_entryMode.value) {
|
||||
case TimePickerEntryMode.dial:
|
||||
switch (orientation) {
|
||||
case Orientation.portrait:
|
||||
@ -1967,8 +2111,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
theme.colorScheme.brightness == Brightness.dark ? 1.0 : 0.6,
|
||||
),
|
||||
onPressed: _handleEntryModeToggle,
|
||||
icon: Icon(_entryMode == TimePickerEntryMode.dial ? Icons.keyboard : Icons.access_time),
|
||||
tooltip: _entryMode == TimePickerEntryMode.dial
|
||||
icon: Icon(_entryMode.value == TimePickerEntryMode.dial ? Icons.keyboard : Icons.access_time),
|
||||
tooltip: _entryMode.value == TimePickerEntryMode.dial
|
||||
? MaterialLocalizations.of(context).inputTimeModeButtonLabel
|
||||
: MaterialLocalizations.of(context).dialModeButtonLabel,
|
||||
),
|
||||
@ -1997,7 +2141,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
);
|
||||
|
||||
final Widget picker;
|
||||
switch (_entryMode) {
|
||||
switch (_entryMode.value) {
|
||||
case TimePickerEntryMode.dial:
|
||||
final Widget dial = Padding(
|
||||
padding: orientation == Orientation.portrait ? const EdgeInsets.symmetric(horizontal: 36, vertical: 24) : const EdgeInsets.all(24),
|
||||
@ -2005,9 +2149,9 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: _Dial(
|
||||
mode: _mode,
|
||||
mode: _mode.value,
|
||||
use24HourDials: use24HourDials,
|
||||
selectedTime: _selectedTime,
|
||||
selectedTime: _selectedTime.value,
|
||||
onChanged: _handleTimeChanged,
|
||||
onHourSelected: _handleHourSelected,
|
||||
),
|
||||
@ -2016,8 +2160,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
);
|
||||
|
||||
final Widget header = _TimePickerHeader(
|
||||
selectedTime: _selectedTime,
|
||||
mode: _mode,
|
||||
selectedTime: _selectedTime.value,
|
||||
mode: _mode.value,
|
||||
orientation: orientation,
|
||||
onModeChanged: _handleModeChanged,
|
||||
onChanged: _handleTimeChanged,
|
||||
@ -2067,17 +2211,19 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
case TimePickerEntryMode.input:
|
||||
picker = Form(
|
||||
key: _formKey,
|
||||
autovalidate: _autoValidate,
|
||||
autovalidate: _autoValidate.value,
|
||||
child: SingleChildScrollView(
|
||||
restorationId: 'time_picker_scroll_view',
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_TimePickerInput(
|
||||
initialSelectedTime: _selectedTime,
|
||||
initialSelectedTime: _selectedTime.value,
|
||||
helpText: widget.helpText,
|
||||
autofocusHour: _autofocusHour,
|
||||
autofocusMinute: _autofocusMinute,
|
||||
autofocusHour: _autofocusHour.value,
|
||||
autofocusMinute: _autofocusMinute.value,
|
||||
onChanged: _handleTimeChanged,
|
||||
restorationId: 'time_picker_input',
|
||||
),
|
||||
actions,
|
||||
],
|
||||
@ -2093,7 +2239,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
backgroundColor: TimePickerTheme.of(context).backgroundColor ?? theme.colorScheme.surface,
|
||||
insetPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: _entryMode == TimePickerEntryMode.input ? 0.0 : 24.0,
|
||||
vertical: _entryMode.value == TimePickerEntryMode.input ? 0.0 : 24.0,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
width: dialogSize.width,
|
||||
@ -2204,7 +2350,7 @@ Future<TimeOfDay?> showTimePicker({
|
||||
assert(initialEntryMode != null);
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
|
||||
final Widget dialog = _TimePickerDialog(
|
||||
final Widget dialog = TimePickerDialog(
|
||||
initialTime: initialTime,
|
||||
initialEntryMode: initialEntryMode,
|
||||
cancelText: cancelText,
|
||||
|
||||
@ -12,40 +12,88 @@ import 'feedback_tester.dart';
|
||||
|
||||
final Finder _hourControl = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
|
||||
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({
|
||||
Key? key,
|
||||
required this.onChanged,
|
||||
this.locale,
|
||||
this.entryMode = TimePickerEntryMode.dial,
|
||||
this.restorationId,
|
||||
}) : super(key: key);
|
||||
|
||||
final ValueChanged<TimeOfDay?> onChanged;
|
||||
final Locale? locale;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
locale: locale,
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
child: const Text('X'),
|
||||
onPressed: () async {
|
||||
onChanged(await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
||||
initialEntryMode: entryMode,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
return Material(
|
||||
child: Center(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
child: const Text('X'),
|
||||
onPressed: () async {
|
||||
if (widget.restorationId == null) {
|
||||
widget.onChanged(await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
||||
initialEntryMode: widget.entryMode,
|
||||
));
|
||||
} else {
|
||||
_restorableTimePickerRouteFuture.present();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -56,8 +104,17 @@ Future<Offset?> startPicker(
|
||||
WidgetTester tester,
|
||||
ValueChanged<TimeOfDay?> onChanged, {
|
||||
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
|
||||
String? restorationId,
|
||||
}) 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.pumpAndSettle(const Duration(seconds: 1));
|
||||
return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null;
|
||||
@ -428,7 +485,7 @@ void _tests() {
|
||||
|
||||
// Ensure we preserve day period as we roll over.
|
||||
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(
|
||||
initialValue: '1',
|
||||
@ -493,7 +550,7 @@ void _tests() {
|
||||
|
||||
// Ensure we preserve hour period as we roll over.
|
||||
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(
|
||||
initialValue: '00',
|
||||
@ -939,6 +996,102 @@ void _testsInput() {
|
||||
expect(hourFieldTop, 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(
|
||||
|
||||
@ -26,4 +26,143 @@ void main() {
|
||||
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';
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user