From e58ee0fbc75811ebfd503350dc66a9bc063791a0 Mon Sep 17 00:00:00 2001 From: Shi-Hao Hong Date: Mon, 22 Mar 2021 13:06:06 +0800 Subject: [PATCH] [State Restoration] Material DateRangePicker, adds some general state restoration tests (#78506) --- .../flutter/lib/src/material/date_picker.dart | 334 +++++++++++++++--- .../src/widgets/restoration_properties.dart | 28 ++ .../test/material/date_picker_test.dart | 11 +- .../test/material/date_range_picker_test.dart | 194 ++++++++++ .../widgets/restorable_property_test.dart | 148 +++++++- 5 files changed, 652 insertions(+), 63 deletions(-) diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 2d07c30280..aff5458298 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -901,14 +901,14 @@ class _DatePickerHeader extends StatelessWidget { /// returned. /// /// If [initialDateRange] is non-null, then it will be used as the initially -/// selected date range. If it is provided, [initialDateRange.start] must be -/// before or on [initialDateRange.end]. +/// selected date range. If it is provided, `initialDateRange.start` must be +/// before or on `initialDateRange.end`. /// /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest /// allowable date. Both must be non-null. /// -/// If an initial date range is provided, [initialDateRange.start] -/// and [initialDateRange.end] must both fall between or on [firstDate] and +/// If an initial date range is provided, `initialDateRange.start` +/// and `initialDateRange.end` must both fall between or on [firstDate] and /// [lastDate]. For all of these [DateTime] values, only their dates are /// considered. Their time fields are ignored. /// @@ -958,6 +958,133 @@ class _DatePickerHeader extends StatelessWidget { /// The [builder] parameter can be used to wrap the dialog widget /// to add inherited widgets like [Theme]. /// +/// ### State Restoration +/// +/// Using this method will not enable state restoration for the date range picker. +/// In order to enable state restoration for a date range picker, use +/// [Navigator.restorablePush] or [Navigator.restorablePushNamed] with +/// [DateRangePickerDialog]. +/// +/// For more information about state restoration, see [RestorationManager]. +/// +/// {@macro flutter.widgets.RestorationManager} +/// +/// {@tool sample --template=freeform} +/// +/// This sample demonstrates how to create a restorable Material date range picker. +/// This is accomplished by enabling state restoration by specifying +/// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to +/// push [DateRangePickerDialog] when the button is tapped. +/// +/// ```dart imports +/// import 'package:flutter/material.dart'; +/// ``` +/// +/// ```dart +/// void main() { +/// runApp(const MyApp()); +/// } +/// +/// class MyApp extends StatelessWidget { +/// const MyApp({Key? key}) : super(key: key); +/// +/// @override +/// Widget build(BuildContext context) { +/// return const MaterialApp( +/// restorationScopeId: 'app', +/// title: 'Restorable Date Range Picker Demo', +/// home: MyHomePage(), +/// ); +/// } +/// } +/// +/// class MyHomePage extends StatefulWidget { +/// const MyHomePage({Key? key}) : super(key: key); +/// +/// @override +/// _MyHomePageState createState() => _MyHomePageState(); +/// } +/// +/// class _MyHomePageState extends State with RestorationMixin { +/// final RestorableDateTimeN _startDate = RestorableDateTimeN(DateTime(2021, 1, 1)); +/// final RestorableDateTimeN _endDate = RestorableDateTimeN(DateTime(2021, 1, 5)); +/// late final RestorableRouteFuture _restorableDateRangePickerRouteFuture = RestorableRouteFuture( +/// onComplete: _selectDateRange, +/// onPresent: (NavigatorState navigator, Object? arguments) { +/// return navigator.restorablePush( +/// _dateRangePickerRoute, +/// arguments: { +/// 'initialStartDate': _startDate.value?.millisecondsSinceEpoch, +/// 'initialEndDate': _endDate.value?.millisecondsSinceEpoch, +/// } +/// ); +/// }, +/// ); +/// +/// void _selectDateRange(DateTimeRange? newSelectedDate) { +/// if (newSelectedDate != null) { +/// setState(() { +/// _startDate.value = newSelectedDate.start; +/// _endDate.value = newSelectedDate.end; +/// }); +/// } +/// } +/// +/// @override +/// String get restorationId => 'scaffold_state'; +/// +/// @override +/// void restoreState(RestorationBucket? oldBucket, bool initialRestore) { +/// registerForRestoration(_startDate, 'start_date'); +/// registerForRestoration(_endDate, 'end_date'); +/// registerForRestoration(_restorableDateRangePickerRouteFuture, 'date_picker_route_future'); +/// } +/// +/// static Route _dateRangePickerRoute( +/// BuildContext context, +/// Object? arguments, +/// ) { +/// return DialogRoute( +/// context: context, +/// builder: (BuildContext context) { +/// return DateRangePickerDialog( +/// restorationId: 'date_picker_dialog', +/// initialDateRange: _initialDateTimeRange(arguments! as Map), +/// firstDate: DateTime(2021, 1, 1), +/// currentDate: DateTime(2021, 1, 25), +/// lastDate: DateTime(2022, 1, 1), +/// ); +/// }, +/// ); +/// } +/// +/// static DateTimeRange? _initialDateTimeRange(Map arguments) { +/// if (arguments['initialStartDate'] != null && arguments['initialEndDate'] != null) { +/// return DateTimeRange( +/// start: DateTime.fromMillisecondsSinceEpoch(arguments['initialStartDate'] as int), +/// end: DateTime.fromMillisecondsSinceEpoch(arguments['initialEndDate'] as int), +/// ); +/// } +/// +/// return null; +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: Center( +/// child: OutlinedButton( +/// onPressed: () { _restorableDateRangePickerRouteFuture.present(); }, +/// child: const Text('Open Date Range Picker'), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// {@end-tool} +/// /// See also: /// /// * [showDatePicker], which shows a material design date picker used to @@ -1027,7 +1154,7 @@ Future showDateRangePicker({ assert(useRootNavigator != null); assert(debugCheckHasMaterialLocalizations(context)); - Widget dialog = _DateRangePickerDialog( + Widget dialog = DateRangePickerDialog( initialDateRange: initialDateRange, firstDate: firstDate, lastDate: lastDate, @@ -1100,8 +1227,18 @@ String _formatRangeEndDate(MaterialLocalizations localizations, DateTime? startD : localizations.formatShortDate(endDate); } -class _DateRangePickerDialog extends StatefulWidget { - const _DateRangePickerDialog({ +/// A Material-style date range picker dialog. +/// +/// It is used internally by [showDateRangePicker] or can be directly pushed +/// onto the [Navigator] stack to enable state restoration. See +/// [showDateRangePicker] for a state restoration app example. +/// +/// See also: +/// +/// * [showDateRangePicker], which is a way to display the date picker. +class DateRangePickerDialog extends StatefulWidget { + /// A Material-style date range picker dialog. + const DateRangePickerDialog({ Key? key, this.initialDateRange, required this.firstDate, @@ -1119,58 +1256,161 @@ class _DateRangePickerDialog extends StatefulWidget { this.fieldEndHintText, this.fieldStartLabelText, this.fieldEndLabelText, + this.restorationId, }) : super(key: key); + /// The date range that the date range picker starts with when it opens. + /// + /// If an initial date range is provided, `initialDateRange.start` + /// and `initialDateRange.end` must both fall between or on [firstDate] and + /// [lastDate]. For all of these [DateTime] values, only their dates are + /// considered. Their time fields are ignored. + /// + /// If [initialDateRange] is non-null, then it will be used as the initially + /// selected date range. If it is provided, `initialDateRange.start` must be + /// before or on `initialDateRange.end`. final DateTimeRange? initialDateRange; + + /// The earliest allowable date on the date range. final DateTime firstDate; + + /// The latest allowable date on the date range. final DateTime lastDate; + + /// The [currentDate] represents the current day (i.e. today). + /// + /// This date will be highlighted in the day grid. + /// + /// If `null`, the date of `DateTime.now()` will be used. final DateTime? currentDate; + + /// The initial date range picker entry mode. + /// + /// The date range has two main modes: [DatePickerEntryMode.calendar] (a + /// scrollable calendar month grid) or [DatePickerEntryMode.input] (two text + /// input fields) mode. + /// + /// It defaults to [DatePickerEntryMode.calendar] and must be non-null. final DatePickerEntryMode initialEntryMode; + + /// The label on the cancel button for the text input mode. + /// + /// If null, the localized value of + /// [MaterialLocalizations.cancelButtonLabel] is used. final String? cancelText; + + /// The label on the "OK" button for the text input mode. + /// + /// If null, the localized value of + /// [MaterialLocalizations.okButtonLabel] is used. final String? confirmText; + + /// The label on the save button for the fullscreen calendar mode. + /// + /// If null, the localized value of + /// [MaterialLocalizations.saveButtonLabel] is used. final String? saveText; + + /// The label displayed at the top of the dialog. + /// + /// If null, the localized value of + /// [MaterialLocalizations.dateRangePickerHelpText] is used. final String? helpText; + + /// The message used when the date range is invalid (e.g. start date is after + /// end date). + /// + /// If null, the localized value of + /// [MaterialLocalizations.invalidDateRangeLabel] is used. final String? errorInvalidRangeText; + + /// The message used when an input text isn't in a proper date format. + /// + /// If null, the localized value of + /// [MaterialLocalizations.invalidDateFormatLabel] is used. final String? errorFormatText; + + /// The message used when an input text isn't a selectable date. + /// + /// If null, the localized value of + /// [MaterialLocalizations.dateOutOfRangeLabel] is used. final String? errorInvalidText; + + /// The text used to prompt the user when no text has been entered in the + /// start field. + /// + /// If null, the localized value of + /// [MaterialLocalizations.dateHelpText] is used. final String? fieldStartHintText; + + /// The text used to prompt the user when no text has been entered in the + /// end field. + /// + /// If null, the localized value of [MaterialLocalizations.dateHelpText] is + /// used. final String? fieldEndHintText; + + /// The label for the start date text input field. + /// + /// If null, the localized value of [MaterialLocalizations.dateRangeStartLabel] + /// is used. final String? fieldStartLabelText; + + /// The label for the end date text input field. + /// + /// If null, the localized value of [MaterialLocalizations.dateRangeEndLabel] + /// is used. final String? fieldEndLabelText; + /// Restoration ID to save and restore the state of the [DateRangePickerDialog]. + /// + /// If it is non-null, the date range picker will persist and restore the + /// date range selected on the dialog. + /// + /// 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 _DateRangePickerDialogState createState() => _DateRangePickerDialogState(); } -class _DateRangePickerDialogState extends State<_DateRangePickerDialog> { - late DatePickerEntryMode _entryMode; - DateTime? _selectedStart; - DateTime? _selectedEnd; - late bool _autoValidate; +class _DateRangePickerDialogState extends State with RestorationMixin { + late final _RestorableDatePickerEntryMode _entryMode = _RestorableDatePickerEntryMode(widget.initialEntryMode); + late final RestorableDateTimeN _selectedStart = RestorableDateTimeN(widget.initialDateRange?.start); + late final RestorableDateTimeN _selectedEnd = RestorableDateTimeN(widget.initialDateRange?.end); + final RestorableBool _autoValidate = RestorableBool(false); final GlobalKey _calendarPickerKey = GlobalKey(); final GlobalKey<_InputDateRangePickerState> _inputPickerKey = GlobalKey<_InputDateRangePickerState>(); @override - void initState() { - super.initState(); - _selectedStart = widget.initialDateRange?.start; - _selectedEnd = widget.initialDateRange?.end; - _entryMode = widget.initialEntryMode; - _autoValidate = false; + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_entryMode, 'entry_mode'); + registerForRestoration(_selectedStart, 'selected_start'); + registerForRestoration(_selectedEnd, 'selected_end'); + registerForRestoration(_autoValidate, 'autovalidate'); } void _handleOk() { - if (_entryMode == DatePickerEntryMode.input || _entryMode == DatePickerEntryMode.inputOnly) { + if (_entryMode.value == DatePickerEntryMode.input || _entryMode.value == DatePickerEntryMode.inputOnly) { final _InputDateRangePickerState picker = _inputPickerKey.currentState!; if (!picker.validate()) { setState(() { - _autoValidate = true; + _autoValidate.value = true; }); return; } } final DateTimeRange? selectedRange = _hasSelectedDateRange - ? DateTimeRange(start: _selectedStart!, end: _selectedEnd!) + ? DateTimeRange(start: _selectedStart.value!, end: _selectedEnd.value!) : null; Navigator.pop(context, selectedRange); @@ -1182,29 +1422,29 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> { void _handleEntryModeToggle() { setState(() { - switch (_entryMode) { + switch (_entryMode.value) { case DatePickerEntryMode.calendar: - _autoValidate = false; - _entryMode = DatePickerEntryMode.input; + _autoValidate.value = false; + _entryMode.value = DatePickerEntryMode.input; break; case DatePickerEntryMode.input: // Validate the range dates - if (_selectedStart != null && - (_selectedStart!.isBefore(widget.firstDate) || _selectedStart!.isAfter(widget.lastDate))) { - _selectedStart = null; + if (_selectedStart.value != null && + (_selectedStart.value!.isBefore(widget.firstDate) || _selectedStart.value!.isAfter(widget.lastDate))) { + _selectedStart.value = null; // With no valid start date, having an end date makes no sense for the UI. - _selectedEnd = null; + _selectedEnd.value = null; } - if (_selectedEnd != null && - (_selectedEnd!.isBefore(widget.firstDate) || _selectedEnd!.isAfter(widget.lastDate))) { - _selectedEnd = null; + if (_selectedEnd.value != null && + (_selectedEnd.value!.isBefore(widget.firstDate) || _selectedEnd.value!.isAfter(widget.lastDate))) { + _selectedEnd.value = null; } // If invalid range (start after end), then just use the start date - if (_selectedStart != null && _selectedEnd != null && _selectedStart!.isAfter(_selectedEnd!)) { - _selectedEnd = null; + if (_selectedStart.value != null && _selectedEnd.value != null && _selectedStart.value!.isAfter(_selectedEnd.value!)) { + _selectedEnd.value = null; } - _entryMode = DatePickerEntryMode.calendar; + _entryMode.value = DatePickerEntryMode.calendar; break; case DatePickerEntryMode.calendarOnly: @@ -1216,14 +1456,14 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> { } void _handleStartDateChanged(DateTime? date) { - setState(() => _selectedStart = date); + setState(() => _selectedStart.value = date); } void _handleEndDateChanged(DateTime? date) { - setState(() => _selectedEnd = date); + setState(() => _selectedEnd.value = date); } - bool get _hasSelectedDateRange => _selectedStart != null && _selectedEnd != null; + bool get _hasSelectedDateRange => _selectedStart.value != null && _selectedEnd.value != null; @override Widget build(BuildContext context) { @@ -1242,15 +1482,15 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> { final double elevation; final EdgeInsets insetPadding; final bool showEntryModeButton = - _entryMode == DatePickerEntryMode.calendar || - _entryMode == DatePickerEntryMode.input; - switch (_entryMode) { + _entryMode.value == DatePickerEntryMode.calendar || + _entryMode.value == DatePickerEntryMode.input; + switch (_entryMode.value) { case DatePickerEntryMode.calendar: case DatePickerEntryMode.calendarOnly: contents = _CalendarRangePickerDialog( key: _calendarPickerKey, - selectedStartDate: _selectedStart, - selectedEndDate: _selectedEnd, + selectedStartDate: _selectedStart.value, + selectedEndDate: _selectedEnd.value, firstDate: widget.firstDate, lastDate: widget.lastDate, currentDate: widget.currentDate, @@ -1279,8 +1519,8 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> { case DatePickerEntryMode.input: case DatePickerEntryMode.inputOnly: contents = _InputDateRangePickerDialog( - selectedStartDate: _selectedStart, - selectedEndDate: _selectedEnd, + selectedStartDate: _selectedStart.value, + selectedEndDate: _selectedEnd.value, currentDate: widget.currentDate, picker: Container( padding: const EdgeInsets.symmetric(horizontal: 24), @@ -1292,14 +1532,14 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> { const Spacer(), _InputDateRangePicker( key: _inputPickerKey, - initialStartDate: _selectedStart, - initialEndDate: _selectedEnd, + initialStartDate: _selectedStart.value, + initialEndDate: _selectedEnd.value, firstDate: widget.firstDate, lastDate: widget.lastDate, onStartDateChanged: _handleStartDateChanged, onEndDateChanged: _handleEndDateChanged, autofocus: true, - autovalidate: _autoValidate, + autovalidate: _autoValidate.value, helpText: widget.helpText, errorInvalidRangeText: widget.errorInvalidRangeText, errorFormatText: widget.errorFormatText, diff --git a/packages/flutter/lib/src/widgets/restoration_properties.dart b/packages/flutter/lib/src/widgets/restoration_properties.dart index 0629b707b8..d51fdc5a1b 100644 --- a/packages/flutter/lib/src/widgets/restoration_properties.dart +++ b/packages/flutter/lib/src/widgets/restoration_properties.dart @@ -385,6 +385,34 @@ class RestorableDateTime extends RestorableValue { Object? toPrimitives() => value.millisecondsSinceEpoch; } +/// A [RestorableValue] that knows how to save and restore [DateTime] that is +/// nullable. +/// +/// {@macro flutter.widgets.RestorableNum}. +class RestorableDateTimeN extends RestorableValue { + /// Creates a [RestorableDateTime]. + /// + /// {@macro flutter.widgets.RestorableNum.constructor} + RestorableDateTimeN(DateTime? defaultValue) : _defaultValue = defaultValue; + + final DateTime? _defaultValue; + + @override + DateTime? createDefaultValue() => _defaultValue; + + @override + void didUpdateValue(DateTime? oldValue) { + assert(debugIsSerializableForRestoration(value?.millisecondsSinceEpoch)); + notifyListeners(); + } + + @override + DateTime? fromPrimitives(Object? data) => data != null ? DateTime.fromMillisecondsSinceEpoch(data as int) : null; + + @override + Object? toPrimitives() => value?.millisecondsSinceEpoch; +} + /// A base class for creating a [RestorableProperty] that stores and restores a /// [Listenable]. /// diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index d7bab83a9e..cf011e2589 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -1162,9 +1162,14 @@ void main() { await tester.restoreFrom(restorationData); expect(find.byType(DatePickerDialog), findsOneWidget); - // Select a different date and close the date picker. + // Select a different date. await tester.tap(find.text('30')); await tester.pumpAndSettle(); + + // Restart after the new selection. It should remain selected. + await tester.restartAndRestore(); + + // Close the date picker. await tester.tap(find.text('OK')); await tester.pumpAndSettle(); @@ -1210,8 +1215,8 @@ void main() { await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(); - // The date picker should be closed, the text value updated to the - // newly selected date. + // The date picker should be closed, the text value should be the same + // as before. expect(find.byType(DatePickerDialog), findsNothing); expect(find.text('25/7/2021'), findsOneWidget); diff --git a/packages/flutter/test/material/date_range_picker_test.dart b/packages/flutter/test/material/date_range_picker_test.dart index 3cc028c5ed..cfd1099617 100644 --- a/packages/flutter/test/material/date_range_picker_test.dart +++ b/packages/flutter/test/material/date_range_picker_test.dart @@ -853,4 +853,198 @@ void main() { _testInputDecorator(tester.widget(borderContainers.last), border, Colors.transparent); }); }); + + testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + restorationScopeId: 'app', + home: _RestorableDateRangePickerDialogTestWidget(), + ), + ); + + // The date range picker should be closed. + expect(find.byType(DateRangePickerDialog), findsNothing); + expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); + + // Open the date range picker. + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + expect(find.byType(DateRangePickerDialog), findsOneWidget); + + final TestRestorationData restorationData = await tester.getRestorationData(); + await tester.restartAndRestore(); + + // The date range picker should be open after restoring. + expect(find.byType(DateRangePickerDialog), findsOneWidget); + + // Close the date range picker. + await tester.tap(find.byIcon(Icons.close)); + await tester.pumpAndSettle(); + + // The date range picker should be closed, the text value updated to the + // newly selected date. + expect(find.byType(DateRangePickerDialog), findsNothing); + expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); + + // The date range picker should be open after restoring. + await tester.restoreFrom(restorationData); + expect(find.byType(DateRangePickerDialog), findsOneWidget); + + // // Select a different date and close the date range picker. + await tester.tap(find.text('12').first); + await tester.pumpAndSettle(); + await tester.tap(find.text('14').first); + await tester.pumpAndSettle(); + + // Restart after the new selection. It should remain selected. + await tester.restartAndRestore(); + + // Close the date range picker. + await tester.tap(find.text('SAVE')); + await tester.pumpAndSettle(); + + // The date range picker should be closed, the text value updated to the + // newly selected date. + expect(find.byType(DateRangePickerDialog), findsNothing); + expect(find.text('12/1/2021 to 14/1/2021'), findsOneWidget); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 + + testWidgets('DateRangePickerDialog state restoration - DatePickerEntryMode', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + restorationScopeId: 'app', + home: _RestorableDateRangePickerDialogTestWidget( + datePickerEntryMode: DatePickerEntryMode.calendarOnly, + ), + ), + ); + + // The date range picker should be closed. + expect(find.byType(DateRangePickerDialog), findsNothing); + expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); + + // Open the date range picker. + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + expect(find.byType(DateRangePickerDialog), findsOneWidget); + + // Only in calendar mode and cannot switch out. + expect(find.byType(TextField), findsNothing); + expect(find.byIcon(Icons.edit), findsNothing); + + final TestRestorationData restorationData = await tester.getRestorationData(); + await tester.restartAndRestore(); + + // The date range picker should be open after restoring. + expect(find.byType(DateRangePickerDialog), findsOneWidget); + // Only in calendar mode and cannot switch out. + expect(find.byType(TextField), findsNothing); + expect(find.byIcon(Icons.edit), findsNothing); + + // Tap on the barrier. + await tester.tap(find.byIcon(Icons.close)); + await tester.pumpAndSettle(); + + // The date range picker should be closed, the text value should be the same + // as before. + expect(find.byType(DateRangePickerDialog), findsNothing); + expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); + + // The date range picker should be open after restoring. + await tester.restoreFrom(restorationData); + expect(find.byType(DateRangePickerDialog), findsOneWidget); + // Only in calendar mode and cannot switch out. + expect(find.byType(TextField), findsNothing); + expect(find.byIcon(Icons.edit), findsNothing); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 +} + +class _RestorableDateRangePickerDialogTestWidget extends StatefulWidget { + const _RestorableDateRangePickerDialogTestWidget({ + Key? key, + this.datePickerEntryMode = DatePickerEntryMode.calendar, + }) : super(key: key); + + final DatePickerEntryMode datePickerEntryMode; + + @override + _RestorableDateRangePickerDialogTestWidgetState createState() => _RestorableDateRangePickerDialogTestWidgetState(); +} + +class _RestorableDateRangePickerDialogTestWidgetState extends State<_RestorableDateRangePickerDialogTestWidget> with RestorationMixin { + @override + String? get restorationId => 'scaffold_state'; + + final RestorableDateTimeN _startDate = RestorableDateTimeN(DateTime(2021, 1, 1)); + final RestorableDateTimeN _endDate = RestorableDateTimeN(DateTime(2021, 1, 5)); + late final RestorableRouteFuture _restorableDateRangePickerRouteFuture = RestorableRouteFuture( + onComplete: _selectDateRange, + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush( + _dateRangePickerRoute, + arguments: { + 'datePickerEntryMode': widget.datePickerEntryMode.index, + } + ); + }, + ); + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_startDate, 'start_date'); + registerForRestoration(_endDate, 'end_date'); + registerForRestoration(_restorableDateRangePickerRouteFuture, 'date_picker_route_future'); + } + + void _selectDateRange(DateTimeRange? newSelectedDate) { + if (newSelectedDate != null) { + setState(() { + _startDate.value = newSelectedDate.start; + _endDate.value = newSelectedDate.end; + }); + } + } + + static Route _dateRangePickerRoute( + BuildContext context, + Object? arguments, + ) { + return DialogRoute( + context: context, + builder: (BuildContext context) { + final Map args = arguments! as Map; + return DateRangePickerDialog( + restorationId: 'date_picker_dialog', + initialEntryMode: DatePickerEntryMode.values[args['datePickerEntryMode'] as int], + firstDate: DateTime(2021, 1, 1), + currentDate: DateTime(2021, 1, 25), + lastDate: DateTime(2022, 1, 1), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final DateTime? startDateTime = _startDate.value; + final DateTime? endDateTime = _endDate.value; + // Example: "25/7/1994" + final String startDateTimeString = '${startDateTime?.day}/${startDateTime?.month}/${startDateTime?.year}'; + final String endDateTimeString = '${endDateTime?.day}/${endDateTime?.month}/${endDateTime?.year}'; + return Scaffold( + body: Center( + child: Column( + children: [ + OutlinedButton( + onPressed: () { + _restorableDateRangePickerRouteFuture.present(); + }, + child: const Text('X'), + ), + Text('$startDateTimeString to $endDateTimeString'), + ], + ), + ), + ); + } } diff --git a/packages/flutter/test/widgets/restorable_property_test.dart b/packages/flutter/test/widgets/restorable_property_test.dart index 6c84b47565..2b5dc5d6db 100644 --- a/packages/flutter/test/widgets/restorable_property_test.dart +++ b/packages/flutter/test/widgets/restorable_property_test.dart @@ -19,6 +19,7 @@ void main() { expect(() => RestorableBoolN(true).value, throwsAssertionError); expect(() => RestorableTextEditingController().value, throwsAssertionError); expect(() => RestorableDateTime(DateTime(2020, 4, 3)).value, throwsAssertionError); + expect(() => RestorableDateTimeN(DateTime(2020, 4, 3)).value, throwsAssertionError); expect(() => _TestRestorableValue().value, throwsAssertionError); }); @@ -34,8 +35,14 @@ void main() { expect(state.intValue.value, 42); expect(state.stringValue.value, 'hello world'); expect(state.boolValue.value, false); - expect(state.controllerValue.value.text, 'FooBar'); expect(state.dateTimeValue.value, DateTime(2021, 3, 16)); + expect(state.nullableNumValue.value, null); + expect(state.nullableDoubleValue.value, null); + expect(state.nullableIntValue.value, null); + expect(state.nullableStringValue.value, null); + expect(state.nullableBoolValue.value, null); + expect(state.nullableDateTimeValue.value, null); + expect(state.controllerValue.value.text, 'FooBar'); expect(state.objectValue.value, 55); // Modify values. @@ -45,8 +52,14 @@ void main() { state.intValue.value = 10; state.stringValue.value = 'guten tag'; state.boolValue.value = true; - state.controllerValue.value.text = 'blabla'; state.dateTimeValue.value = DateTime(2020, 7, 4); + state.nullableNumValue.value = 5.0; + state.nullableDoubleValue.value = 2.0; + state.nullableIntValue.value = 1; + state.nullableStringValue.value = 'hullo'; + state.nullableBoolValue.value = false; + state.nullableDateTimeValue.value = DateTime(2020, 4, 4); + state.controllerValue.value.text = 'blabla'; state.objectValue.value = 53; }); await tester.pump(); @@ -56,8 +69,14 @@ void main() { expect(state.intValue.value, 10); expect(state.stringValue.value, 'guten tag'); expect(state.boolValue.value, true); - expect(state.controllerValue.value.text, 'blabla'); expect(state.dateTimeValue.value, DateTime(2020, 7, 4)); + expect(state.nullableNumValue.value, 5.0); + expect(state.nullableDoubleValue.value, 2.0); + expect(state.nullableIntValue.value, 1); + expect(state.nullableStringValue.value, 'hullo'); + expect(state.nullableBoolValue.value, false); + expect(state.nullableDateTimeValue.value, DateTime(2020, 4, 4)); + expect(state.controllerValue.value.text, 'blabla'); expect(state.objectValue.value, 53); expect(find.text('guten tag'), findsOneWidget); }); @@ -77,8 +96,14 @@ void main() { expect(state.intValue.value, 42); expect(state.stringValue.value, 'hello world'); expect(state.boolValue.value, false); - expect(state.controllerValue.value.text, 'FooBar'); expect(state.dateTimeValue.value, DateTime(2021, 3, 16)); + expect(state.nullableNumValue.value, null); + expect(state.nullableDoubleValue.value, null); + expect(state.nullableIntValue.value, null); + expect(state.nullableStringValue.value, null); + expect(state.nullableBoolValue.value, null); + expect(state.nullableDateTimeValue.value, null); + expect(state.controllerValue.value.text, 'FooBar'); expect(state.objectValue.value, 55); // Modify values. @@ -88,8 +113,14 @@ void main() { state.intValue.value = 10; state.stringValue.value = 'guten tag'; state.boolValue.value = true; - state.controllerValue.value.text = 'blabla'; state.dateTimeValue.value = DateTime(2020, 7, 4); + state.nullableNumValue.value = 5.0; + state.nullableDoubleValue.value = 2.0; + state.nullableIntValue.value = 1; + state.nullableStringValue.value = 'hullo'; + state.nullableBoolValue.value = false; + state.nullableDateTimeValue.value = DateTime(2020, 4, 4); + state.controllerValue.value.text = 'blabla'; state.objectValue.value = 53; }); await tester.pump(); @@ -99,8 +130,14 @@ void main() { expect(state.intValue.value, 10); expect(state.stringValue.value, 'guten tag'); expect(state.boolValue.value, true); - expect(state.controllerValue.value.text, 'blabla'); expect(state.dateTimeValue.value, DateTime(2020, 7, 4)); + expect(state.nullableNumValue.value, 5.0); + expect(state.nullableDoubleValue.value, 2.0); + expect(state.nullableIntValue.value, 1); + expect(state.nullableStringValue.value, 'hullo'); + expect(state.nullableBoolValue.value, false); + expect(state.nullableDateTimeValue.value, DateTime(2020, 4, 4)); + expect(state.controllerValue.value.text, 'blabla'); expect(state.objectValue.value, 53); expect(find.text('guten tag'), findsOneWidget); @@ -115,8 +152,14 @@ void main() { expect(state.intValue.value, 10); expect(state.stringValue.value, 'guten tag'); expect(state.boolValue.value, true); - expect(state.controllerValue.value.text, 'blabla'); expect(state.dateTimeValue.value, DateTime(2020, 7, 4)); + expect(state.nullableNumValue.value, 5.0); + expect(state.nullableDoubleValue.value, 2.0); + expect(state.nullableIntValue.value, 1); + expect(state.nullableStringValue.value, 'hullo'); + expect(state.nullableBoolValue.value, false); + expect(state.nullableDateTimeValue.value, DateTime(2020, 4, 4)); + expect(state.controllerValue.value.text, 'blabla'); expect(state.objectValue.value, 53); expect(find.text('guten tag'), findsOneWidget); }); @@ -137,13 +180,14 @@ void main() { state.intValue.value = 10; state.stringValue.value = 'guten tag'; state.boolValue.value = true; + state.dateTimeValue.value = DateTime(2020, 7, 4); state.nullableNumValue.value = 5.0; state.nullableDoubleValue.value = 2.0; state.nullableIntValue.value = 1; state.nullableStringValue.value = 'hullo'; state.nullableBoolValue.value = false; + state.nullableDateTimeValue.value = DateTime(2020, 4, 4); state.controllerValue.value.text = 'blabla'; - state.dateTimeValue.value = DateTime(2020, 7, 4); state.objectValue.value = 53; }); await tester.pump(); @@ -158,13 +202,14 @@ void main() { state.intValue.value = 20; state.stringValue.value = 'ciao'; state.boolValue.value = false; + state.dateTimeValue.value = DateTime(2020, 3, 2); state.nullableNumValue.value = 20.0; state.nullableDoubleValue.value = 20.0; state.nullableIntValue.value = 20; state.nullableStringValue.value = 'ni hao'; state.nullableBoolValue.value = null; + state.nullableDateTimeValue.value = DateTime(2020, 5, 5); state.controllerValue.value.text = 'blub'; - state.dateTimeValue.value = DateTime(2020, 3, 2); state.objectValue.value = 20; }); await tester.pump(); @@ -178,13 +223,14 @@ void main() { expect(state.intValue.value, 10); expect(state.stringValue.value, 'guten tag'); expect(state.boolValue.value, true); + expect(state.dateTimeValue.value, DateTime(2020, 7, 4)); expect(state.nullableNumValue.value, 5.0); expect(state.nullableDoubleValue.value, 2.0); expect(state.nullableIntValue.value, 1); expect(state.nullableStringValue.value, 'hullo'); expect(state.nullableBoolValue.value, false); + expect(state.nullableDateTimeValue.value, DateTime(2020, 4, 4)); expect(state.controllerValue.value.text, 'blabla'); - expect(state.dateTimeValue.value, DateTime(2020, 7, 4)); expect(state.objectValue.value, 53); expect(find.text('guten tag'), findsOneWidget); expect(state.controllerValue.value, isNot(same(controller))); @@ -196,13 +242,14 @@ void main() { expect(state.intValue.value, 42); expect(state.stringValue.value, 'hello world'); expect(state.boolValue.value, false); + expect(state.dateTimeValue.value, DateTime(2021, 3, 16)); expect(state.nullableNumValue.value, null); expect(state.nullableDoubleValue.value, null); expect(state.nullableIntValue.value, null); expect(state.nullableStringValue.value, null); expect(state.nullableBoolValue.value, null); + expect(state.nullableDateTimeValue.value, null); expect(state.controllerValue.value.text, 'FooBar'); - expect(state.dateTimeValue.value, DateTime(2021, 3, 16)); expect(state.objectValue.value, 55); expect(find.text('hello world'), findsOneWidget); }); @@ -217,6 +264,7 @@ void main() { final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget)); final List notifyLog = []; + state.numValue.addListener(() { notifyLog.add('num'); }); @@ -232,6 +280,27 @@ void main() { state.boolValue.addListener(() { notifyLog.add('bool'); }); + state.dateTimeValue.addListener(() { + notifyLog.add('date-time'); + }); + state.nullableNumValue.addListener(() { + notifyLog.add('nullable-num'); + }); + state.nullableDoubleValue.addListener(() { + notifyLog.add('nullable-double'); + }); + state.nullableIntValue.addListener(() { + notifyLog.add('nullable-int'); + }); + state.nullableStringValue.addListener(() { + notifyLog.add('nullable-string'); + }); + state.nullableBoolValue.addListener(() { + notifyLog.add('nullable-bool'); + }); + state.nullableDateTimeValue.addListener(() { + notifyLog.add('nullable-date-time'); + }); state.controllerValue.addListener(() { notifyLog.add('controller'); }); @@ -269,6 +338,48 @@ void main() { expect(notifyLog.single, 'bool'); notifyLog.clear(); + state.setProperties(() { + state.dateTimeValue.value = DateTime(2020, 7, 4); + }); + expect(notifyLog.single, 'date-time'); + notifyLog.clear(); + + state.setProperties(() { + state.nullableNumValue.value = 42.2; + }); + expect(notifyLog.single, 'nullable-num'); + notifyLog.clear(); + + state.setProperties(() { + state.nullableDoubleValue.value = 42.2; + }); + expect(notifyLog.single, 'nullable-double'); + notifyLog.clear(); + + state.setProperties(() { + state.nullableIntValue.value = 45; + }); + expect(notifyLog.single, 'nullable-int'); + notifyLog.clear(); + + state.setProperties(() { + state.nullableStringValue.value = 'bar'; + }); + expect(notifyLog.single, 'nullable-string'); + notifyLog.clear(); + + state.setProperties(() { + state.nullableBoolValue.value = true; + }); + expect(notifyLog.single, 'nullable-bool'); + notifyLog.clear(); + + state.setProperties(() { + state.nullableDateTimeValue.value = DateTime(2020, 4, 4); + }); + expect(notifyLog.single, 'nullable-date-time'); + notifyLog.clear(); + state.setProperties(() { state.controllerValue.value.text = 'foo'; }); @@ -291,8 +402,17 @@ void main() { state.intValue.value = 45; state.stringValue.value = 'bar'; state.boolValue.value = true; + state.dateTimeValue.value = DateTime(2020, 7, 4); + state.nullableNumValue.value = 42.2; + state.nullableDoubleValue.value = 42.2; + state.nullableIntValue.value = 45; + state.nullableStringValue.value = 'bar'; + state.nullableBoolValue.value = true; + state.nullableDateTimeValue.value = DateTime(2020, 4, 4); state.controllerValue.value.text = 'foo'; + state.objectValue.value = 42; }); + expect(notifyLog, isEmpty); }); @@ -396,13 +516,14 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi final RestorableInt intValue = RestorableInt(42); final RestorableString stringValue = RestorableString('hello world'); final RestorableBool boolValue = RestorableBool(false); + final RestorableDateTime dateTimeValue = RestorableDateTime(DateTime(2021, 3, 16)); final RestorableNumN nullableNumValue = RestorableNumN(null); final RestorableDoubleN nullableDoubleValue = RestorableDoubleN(null); final RestorableIntN nullableIntValue = RestorableIntN(null); final RestorableStringN nullableStringValue = RestorableStringN(null); final RestorableBoolN nullableBoolValue = RestorableBoolN(null); + final RestorableDateTimeN nullableDateTimeValue = RestorableDateTimeN(null); final RestorableTextEditingController controllerValue = RestorableTextEditingController(text: 'FooBar'); - final RestorableDateTime dateTimeValue = RestorableDateTime(DateTime(2021, 3, 16)); final _TestRestorableValue objectValue = _TestRestorableValue(); @override @@ -412,13 +533,14 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi registerForRestoration(intValue, 'int'); registerForRestoration(stringValue, 'string'); registerForRestoration(boolValue, 'bool'); + registerForRestoration(dateTimeValue, 'dateTime'); registerForRestoration(nullableNumValue, 'nullableNum'); registerForRestoration(nullableDoubleValue, 'nullableDouble'); registerForRestoration(nullableIntValue, 'nullableInt'); registerForRestoration(nullableStringValue, 'nullableString'); registerForRestoration(nullableBoolValue, 'nullableBool'); + registerForRestoration(nullableDateTimeValue, 'nullableDateTime'); registerForRestoration(controllerValue, 'controller'); - registerForRestoration(dateTimeValue, 'dateTime'); registerForRestoration(objectValue, 'object'); }