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

[State Restoration] Material Date Picker (#77879)

parent 640ae7a3
......@@ -358,6 +358,33 @@ class RestorableStringN extends _RestorablePrimitiveValueN<String?> {
RestorableStringN(String? defaultValue) : super(defaultValue);
}
/// A [RestorableValue] that knows how to save and restore [DateTime].
///
/// {@macro flutter.widgets.RestorableNum}.
class RestorableDateTime extends RestorableValue<DateTime> {
/// Creates a [RestorableDateTime].
///
/// {@macro flutter.widgets.RestorableNum.constructor}
RestorableDateTime(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) => DateTime.fromMillisecondsSinceEpoch(data! as int);
@override
Object? toPrimitives() => value.millisecondsSinceEpoch;
}
/// A base class for creating a [RestorableProperty] that stores and restores a
/// [Listenable].
///
......
......@@ -198,7 +198,7 @@ void main() {
await tester.pumpAndSettle();
expect(find.text('Invalid format.'), findsOneWidget);
// Toggle to calender mode and then back to input mode
// Toggle to calendar mode and then back to input mode
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.edit));
......@@ -1125,6 +1125,187 @@ void main() {
expect(tester.takeException(), isNull);
});
});
testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
restorationScopeId: 'app',
home: _RestorableDatePickerDialogTestWidget(),
),
);
// The date picker should be closed.
expect(find.byType(DatePickerDialog), findsNothing);
expect(find.text('25/7/2021'), findsOneWidget);
// Open the date picker.
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(DatePickerDialog), findsOneWidget);
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
// The date picker should be open after restoring.
expect(find.byType(DatePickerDialog), findsOneWidget);
// Tap on the barrier.
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.
expect(find.byType(DatePickerDialog), findsNothing);
expect(find.text('25/7/2021'), findsOneWidget);
// The date picker should be open after restoring.
await tester.restoreFrom(restorationData);
expect(find.byType(DatePickerDialog), findsOneWidget);
// Select a different date and close the date picker.
await tester.tap(find.text('30'));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
// The date picker should be closed, the text value updated to the
// newly selected date.
expect(find.byType(DatePickerDialog), findsNothing);
expect(find.text('30/7/2021'), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('DatePickerDialog state restoration - DatePickerEntryMode', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
restorationScopeId: 'app',
home: _RestorableDatePickerDialogTestWidget(
datePickerEntryMode: DatePickerEntryMode.calendarOnly,
),
),
);
// The date picker should be closed.
expect(find.byType(DatePickerDialog), findsNothing);
expect(find.text('25/7/2021'), findsOneWidget);
// Open the date picker.
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(DatePickerDialog), 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 picker should be open after restoring.
expect(find.byType(DatePickerDialog), 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.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.
expect(find.byType(DatePickerDialog), findsNothing);
expect(find.text('25/7/2021'), findsOneWidget);
// The date picker should be open after restoring.
await tester.restoreFrom(restorationData);
expect(find.byType(DatePickerDialog), 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 _RestorableDatePickerDialogTestWidget extends StatefulWidget {
const _RestorableDatePickerDialogTestWidget({
Key? key,
this.datePickerEntryMode = DatePickerEntryMode.calendar,
}) : super(key: key);
final DatePickerEntryMode datePickerEntryMode;
@override
_RestorableDatePickerDialogTestWidgetState createState() => _RestorableDatePickerDialogTestWidgetState();
}
class _RestorableDatePickerDialogTestWidgetState extends State<_RestorableDatePickerDialogTestWidget> with RestorationMixin {
@override
String? get restorationId => 'scaffold_state';
final RestorableDateTime _selectedDate = RestorableDateTime(DateTime(2021, 7, 25));
late final RestorableRouteFuture<DateTime?> _restorableDatePickerRouteFuture = RestorableRouteFuture<DateTime?>(
onComplete: _selectDate,
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(
_datePickerRoute,
arguments: <String, dynamic>{
'selectedDate': _selectedDate.value.millisecondsSinceEpoch,
'datePickerEntryMode': widget.datePickerEntryMode.index,
}
);
},
);
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_selectedDate, 'selected_date');
registerForRestoration(_restorableDatePickerRouteFuture, 'date_picker_route_future');
}
void _selectDate(DateTime? newSelectedDate) {
if (newSelectedDate != null) {
setState(() { _selectedDate.value = newSelectedDate; });
}
}
static Route<DateTime> _datePickerRoute(
BuildContext context,
Object? arguments,
) {
return DialogRoute<DateTime>(
context: context,
builder: (BuildContext context) {
final Map<dynamic, dynamic> args = arguments! as Map<dynamic, dynamic>;
return DatePickerDialog(
restorationId: 'date_picker_dialog',
initialEntryMode: DatePickerEntryMode.values[args['datePickerEntryMode'] as int],
initialDate: DateTime.fromMillisecondsSinceEpoch(args['selectedDate'] as int),
firstDate: DateTime(2021, 1, 1),
lastDate: DateTime(2022, 1, 1),
);
},
);
}
@override
Widget build(BuildContext context) {
final DateTime selectedDateTime = _selectedDate.value;
// Example: "25/7/1994"
final String selectedDateTimeString = '${selectedDateTime.day}/${selectedDateTime.month}/${selectedDateTime.year}';
return Scaffold(
body: Center(
child: Column(
children: <Widget>[
OutlinedButton(
onPressed: () {
_restorableDatePickerRouteFuture.present();
},
child: const Text('X'),
),
Text(selectedDateTimeString),
],
),
),
);
}
}
class _DatePickerObserver extends NavigatorObserver {
......
......@@ -18,6 +18,7 @@ void main() {
expect(() => RestorableStringN('hello').value, throwsAssertionError);
expect(() => RestorableBoolN(true).value, throwsAssertionError);
expect(() => RestorableTextEditingController().value, throwsAssertionError);
expect(() => RestorableDateTime(DateTime(2020, 4, 3)).value, throwsAssertionError);
expect(() => _TestRestorableValue().value, throwsAssertionError);
});
......@@ -34,6 +35,7 @@ void main() {
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.objectValue.value, 55);
// Modify values.
......@@ -44,6 +46,7 @@ void main() {
state.stringValue.value = 'guten tag';
state.boolValue.value = true;
state.controllerValue.value.text = 'blabla';
state.dateTimeValue.value = DateTime(2020, 7, 4);
state.objectValue.value = 53;
});
await tester.pump();
......@@ -54,6 +57,7 @@ void main() {
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.objectValue.value, 53);
expect(find.text('guten tag'), findsOneWidget);
});
......@@ -74,6 +78,7 @@ void main() {
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.objectValue.value, 55);
// Modify values.
......@@ -84,6 +89,7 @@ void main() {
state.stringValue.value = 'guten tag';
state.boolValue.value = true;
state.controllerValue.value.text = 'blabla';
state.dateTimeValue.value = DateTime(2020, 7, 4);
state.objectValue.value = 53;
});
await tester.pump();
......@@ -94,6 +100,7 @@ void main() {
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.objectValue.value, 53);
expect(find.text('guten tag'), findsOneWidget);
......@@ -109,6 +116,7 @@ void main() {
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.objectValue.value, 53);
expect(find.text('guten tag'), findsOneWidget);
});
......@@ -135,6 +143,7 @@ void main() {
state.nullableStringValue.value = 'hullo';
state.nullableBoolValue.value = false;
state.controllerValue.value.text = 'blabla';
state.dateTimeValue.value = DateTime(2020, 7, 4);
state.objectValue.value = 53;
});
await tester.pump();
......@@ -155,6 +164,7 @@ void main() {
state.nullableStringValue.value = 'ni hao';
state.nullableBoolValue.value = null;
state.controllerValue.value.text = 'blub';
state.dateTimeValue.value = DateTime(2020, 3, 2);
state.objectValue.value = 20;
});
await tester.pump();
......@@ -174,6 +184,7 @@ void main() {
expect(state.nullableStringValue.value, 'hullo');
expect(state.nullableBoolValue.value, false);
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)));
......@@ -191,6 +202,7 @@ void main() {
expect(state.nullableStringValue.value, null);
expect(state.nullableBoolValue.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);
});
......@@ -390,6 +402,7 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi
final RestorableStringN nullableStringValue = RestorableStringN(null);
final RestorableBoolN nullableBoolValue = RestorableBoolN(null);
final RestorableTextEditingController controllerValue = RestorableTextEditingController(text: 'FooBar');
final RestorableDateTime dateTimeValue = RestorableDateTime(DateTime(2021, 3, 16));
final _TestRestorableValue objectValue = _TestRestorableValue();
@override
......@@ -405,6 +418,7 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi
registerForRestoration(nullableStringValue, 'nullableString');
registerForRestoration(nullableBoolValue, 'nullableBool');
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