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

[State Restoration] Material DateRangePicker, adds some general state restoration tests (#78506)

parent 2977a346
......@@ -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<MyHomePage> with RestorationMixin {
/// final RestorableDateTimeN _startDate = RestorableDateTimeN(DateTime(2021, 1, 1));
/// final RestorableDateTimeN _endDate = RestorableDateTimeN(DateTime(2021, 1, 5));
/// late final RestorableRouteFuture<DateTimeRange?> _restorableDateRangePickerRouteFuture = RestorableRouteFuture<DateTimeRange?>(
/// onComplete: _selectDateRange,
/// onPresent: (NavigatorState navigator, Object? arguments) {
/// return navigator.restorablePush(
/// _dateRangePickerRoute,
/// arguments: <String, dynamic>{
/// '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<DateTimeRange?> _dateRangePickerRoute(
/// BuildContext context,
/// Object? arguments,
/// ) {
/// return DialogRoute<DateTimeRange?>(
/// context: context,
/// builder: (BuildContext context) {
/// return DateRangePickerDialog(
/// restorationId: 'date_picker_dialog',
/// initialDateRange: _initialDateTimeRange(arguments! as Map<dynamic, dynamic>),
/// firstDate: DateTime(2021, 1, 1),
/// currentDate: DateTime(2021, 1, 25),
/// lastDate: DateTime(2022, 1, 1),
/// );
/// },
/// );
/// }
///
/// static DateTimeRange? _initialDateTimeRange(Map<dynamic, dynamic> 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<DateTimeRange?> 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<DateRangePickerDialog> 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,
......
......@@ -385,6 +385,34 @@ class RestorableDateTime extends RestorableValue<DateTime> {
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<DateTime?> {
/// 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].
///
......
......@@ -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);
......
......@@ -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<DateTimeRange?> _restorableDateRangePickerRouteFuture = RestorableRouteFuture<DateTimeRange?>(
onComplete: _selectDateRange,
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(
_dateRangePickerRoute,
arguments: <String, dynamic>{
'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<DateTimeRange?> _dateRangePickerRoute(
BuildContext context,
Object? arguments,
) {
return DialogRoute<DateTimeRange?>(
context: context,
builder: (BuildContext context) {
final Map<dynamic, dynamic> args = arguments! as Map<dynamic, dynamic>;
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: <Widget>[
OutlinedButton(
onPressed: () {
_restorableDateRangePickerRouteFuture.present();
},
child: const Text('X'),
),
Text('$startDateTimeString to $endDateTimeString'),
],
),
),
);
}
}
......@@ -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<String> notifyLog = <String>[];
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<num?> nullableNumValue = RestorableNumN<num?>(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');
}
......
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