Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
07849778
Unverified
Commit
07849778
authored
Apr 23, 2021
by
Shi-Hao Hong
Committed by
GitHub
Apr 23, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[State Restoration] Restorable `TimePickerDialog` widget, `RestorableTimeOfDay` (#80566)
parent
e6d4b8cf
Changes
4
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
598 additions
and
125 deletions
+598
-125
time.dart
packages/flutter/lib/src/material/time.dart
+35
-0
time_picker.dart
packages/flutter/lib/src/material/time_picker.dart
+246
-100
time_picker_test.dart
packages/flutter/test/material/time_picker_test.dart
+178
-25
time_test.dart
packages/flutter/test/material/time_test.dart
+139
-0
No files found.
packages/flutter/lib/src/material/time.dart
View file @
07849778
...
...
@@ -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.
///
...
...
packages/flutter/lib/src/material/time_picker.dart
View file @
07849778
This diff is collapsed.
Click to expand it.
packages/flutter/test/material/time_picker_test.dart
View file @
07849778
...
...
@@ -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
State
less
Widget
{
class
_TimePickerLauncher
extends
State
ful
Widget
{
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
(
...
...
packages/flutter/test/material/time_test.dart
View file @
07849778
...
...
@@ -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'
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment