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
3eeadc28
Unverified
Commit
3eeadc28
authored
Apr 14, 2021
by
Shi-Hao Hong
Committed by
GitHub
Apr 14, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[State Restoration] Restorable FormField and TextFormField (#78835)
* Restorable FormField and TextFormField
parent
10d5ec87
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
403 additions
and
94 deletions
+403
-94
text_form_field.dart
packages/flutter/lib/src/material/text_form_field.dart
+115
-77
form.dart
packages/flutter/lib/src/widgets/form.dart
+37
-17
text_form_field_restoration_test.dart
...utter/test/material/text_form_field_restoration_test.dart
+251
-0
No files found.
packages/flutter/lib/src/material/text_form_field.dart
View file @
3eeadc28
...
...
@@ -195,6 +195,7 @@ class TextFormField extends FormField<String> {
Iterable
<
String
>?
autofillHints
,
AutovalidateMode
?
autovalidateMode
,
ScrollController
?
scrollController
,
String
?
restorationId
,
})
:
assert
(
initialValue
==
null
||
controller
==
null
),
assert
(
textAlign
!=
null
),
assert
(
autofocus
!=
null
),
...
...
@@ -231,6 +232,7 @@ class TextFormField extends FormField<String> {
assert
(
enableInteractiveSelection
!=
null
),
super
(
key:
key
,
restorationId:
restorationId
,
initialValue:
controller
!=
null
?
controller
.
text
:
(
initialValue
??
''
),
onSaved:
onSaved
,
validator:
validator
,
...
...
@@ -248,7 +250,10 @@ class TextFormField extends FormField<String> {
onChanged
(
value
);
}
}
return
TextField
(
return
UnmanagedRestorationScope
(
bucket:
field
.
bucket
,
child:
TextField
(
restorationId:
restorationId
,
controller:
state
.
_effectiveController
,
focusNode:
focusNode
,
decoration:
effectiveDecoration
.
copyWith
(
errorText:
field
.
errorText
),
...
...
@@ -294,6 +299,7 @@ class TextFormField extends FormField<String> {
buildCounter:
buildCounter
,
autofillHints:
autofillHints
,
scrollController:
scrollController
,
),
);
},
);
...
...
@@ -309,18 +315,44 @@ class TextFormField extends FormField<String> {
}
class
_TextFormFieldState
extends
FormFieldState
<
String
>
{
TextEditingController
?
_controller
;
Restorable
TextEditingController
?
_controller
;
TextEditingController
?
get
_effectiveController
=>
widget
.
controller
??
_controller
;
TextEditingController
get
_effectiveController
=>
widget
.
controller
??
_controller
!.
value
;
@override
TextFormField
get
widget
=>
super
.
widget
as
TextFormField
;
@override
void
restoreState
(
RestorationBucket
?
oldBucket
,
bool
initialRestore
)
{
super
.
restoreState
(
oldBucket
,
initialRestore
);
if
(
_controller
!=
null
)
{
_registerController
();
}
// Make sure to update the internal [FormFieldState] value to sync up with
// text editing controller value.
setValue
(
_effectiveController
.
text
);
}
void
_registerController
()
{
assert
(
_controller
!=
null
);
registerForRestoration
(
_controller
!,
'controller'
);
}
void
_createLocalController
([
TextEditingValue
?
value
])
{
assert
(
_controller
==
null
);
_controller
=
value
==
null
?
RestorableTextEditingController
()
:
RestorableTextEditingController
.
fromValue
(
value
);
if
(!
restorePending
)
{
_registerController
();
}
}
@override
void
initState
()
{
super
.
initState
();
if
(
widget
.
controller
==
null
)
{
_c
ontroller
=
TextEditingController
(
text:
widget
.
initialValue
);
_c
reateLocalController
(
widget
.
initialValue
!=
null
?
TextEditingValue
(
text:
widget
.
initialValue
!)
:
null
);
}
else
{
widget
.
controller
!.
addListener
(
_handleControllerChanged
);
}
...
...
@@ -333,19 +365,25 @@ class _TextFormFieldState extends FormFieldState<String> {
oldWidget
.
controller
?.
removeListener
(
_handleControllerChanged
);
widget
.
controller
?.
addListener
(
_handleControllerChanged
);
if
(
oldWidget
.
controller
!=
null
&&
widget
.
controller
==
null
)
_controller
=
TextEditingController
.
fromValue
(
oldWidget
.
controller
!.
value
);
if
(
oldWidget
.
controller
!=
null
&&
widget
.
controller
==
null
)
{
_createLocalController
(
oldWidget
.
controller
!.
value
);
}
if
(
widget
.
controller
!=
null
)
{
setValue
(
widget
.
controller
!.
text
);
if
(
oldWidget
.
controller
==
null
)
if
(
oldWidget
.
controller
==
null
)
{
unregisterFromRestoration
(
_controller
!);
_controller
!.
dispose
();
_controller
=
null
;
}
}
}
}
@override
void
dispose
()
{
widget
.
controller
?.
removeListener
(
_handleControllerChanged
);
_controller
?.
dispose
();
super
.
dispose
();
}
...
...
@@ -353,15 +391,15 @@ class _TextFormFieldState extends FormFieldState<String> {
void
didChange
(
String
?
value
)
{
super
.
didChange
(
value
);
if
(
_effectiveController
!
.
text
!=
value
)
_effectiveController
!
.
text
=
value
??
''
;
if
(
_effectiveController
.
text
!=
value
)
_effectiveController
.
text
=
value
??
''
;
}
@override
void
reset
()
{
// setState will be called in the superclass, so even though state is being
// manipulated, no setState call is needed here.
_effectiveController
!
.
text
=
widget
.
initialValue
??
''
;
_effectiveController
.
text
=
widget
.
initialValue
??
''
;
super
.
reset
();
}
...
...
@@ -373,7 +411,7 @@ class _TextFormFieldState extends FormFieldState<String> {
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
if
(
_effectiveController
!
.
text
!=
value
)
didChange
(
_effectiveController
!
.
text
);
if
(
_effectiveController
.
text
!=
value
)
didChange
(
_effectiveController
.
text
);
}
}
packages/flutter/lib/src/widgets/form.dart
View file @
3eeadc28
...
...
@@ -4,6 +4,8 @@
import
'framework.dart'
;
import
'navigator.dart'
;
import
'restoration.dart'
;
import
'restoration_properties.dart'
;
import
'will_pop_scope.dart'
;
/// An optional container for grouping together multiple form field widgets
...
...
@@ -170,7 +172,7 @@ class FormState extends State<Form> {
widget
.
onChanged
?.
call
();
_hasInteractedByUser
=
_fields
.
any
((
FormFieldState
<
dynamic
>
field
)
=>
field
.
_hasInteractedByUser
);
.
any
((
FormFieldState
<
dynamic
>
field
)
=>
field
.
_hasInteractedByUser
.
value
);
_forceRebuild
();
}
...
...
@@ -331,6 +333,7 @@ class FormField<T> extends StatefulWidget {
this
.
autovalidate
=
false
,
this
.
enabled
=
true
,
AutovalidateMode
?
autovalidateMode
,
this
.
restorationId
,
})
:
assert
(
builder
!=
null
),
assert
(
autovalidate
==
false
||
...
...
@@ -399,16 +402,30 @@ class FormField<T> extends StatefulWidget {
)
final
bool
autovalidate
;
/// Restoration ID to save and restore the state of the form field.
///
/// Setting the restoration ID to a non-null value results in whether or not
/// the form field validation persists.
///
/// 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
FormFieldState
<
T
>
createState
()
=>
FormFieldState
<
T
>();
}
/// The current state of a [FormField]. Passed to the [FormFieldBuilder] method
/// for use in constructing the form field's widget.
class
FormFieldState
<
T
>
extends
State
<
FormField
<
T
>>
{
T
?
_v
alue
;
String
?
_errorText
;
bool
_hasInteractedByUser
=
false
;
class
FormFieldState
<
T
>
extends
State
<
FormField
<
T
>>
with
RestorationMixin
{
late
T
?
_value
=
widget
.
initialV
alue
;
final
RestorableStringN
_errorText
=
RestorableStringN
(
null
)
;
final
RestorableBool
_hasInteractedByUser
=
RestorableBool
(
false
)
;
/// The current value of the form field.
T
?
get
value
=>
_value
;
...
...
@@ -416,10 +433,10 @@ class FormFieldState<T> extends State<FormField<T>> {
/// The current validation error returned by the [FormField.validator]
/// callback, or null if no errors have been triggered. This only updates when
/// [validate] is called.
String
?
get
errorText
=>
_errorText
;
String
?
get
errorText
=>
_errorText
.
value
;
/// True if this field has any validation errors.
bool
get
hasError
=>
_errorText
!=
null
;
bool
get
hasError
=>
_errorText
.
value
!=
null
;
/// True if the current value is valid.
///
...
...
@@ -440,8 +457,8 @@ class FormFieldState<T> extends State<FormField<T>> {
void
reset
()
{
setState
(()
{
_value
=
widget
.
initialValue
;
_hasInteractedByUser
=
false
;
_errorText
=
null
;
_hasInteractedByUser
.
value
=
false
;
_errorText
.
value
=
null
;
});
Form
.
of
(
context
)?.
_fieldDidChange
();
}
...
...
@@ -462,7 +479,7 @@ class FormFieldState<T> extends State<FormField<T>> {
void
_validate
()
{
if
(
widget
.
validator
!=
null
)
_errorText
=
widget
.
validator
!(
_value
);
_errorText
.
value
=
widget
.
validator
!(
_value
);
}
/// Updates this field's state to the new value. Useful for responding to
...
...
@@ -474,7 +491,7 @@ class FormFieldState<T> extends State<FormField<T>> {
void
didChange
(
T
?
value
)
{
setState
(()
{
_value
=
value
;
_hasInteractedByUser
=
true
;
_hasInteractedByUser
.
value
=
true
;
});
Form
.
of
(
context
)?.
_fieldDidChange
();
}
...
...
@@ -492,9 +509,12 @@ class FormFieldState<T> extends State<FormField<T>> {
}
@override
void
initState
()
{
super
.
initState
();
_value
=
widget
.
initialValue
;
String
?
get
restorationId
=>
widget
.
restorationId
;
@override
void
restoreState
(
RestorationBucket
?
oldBucket
,
bool
initialRestore
)
{
registerForRestoration
(
_errorText
,
'error_text'
);
registerForRestoration
(
_hasInteractedByUser
,
'has_interacted_by_user'
);
}
@override
...
...
@@ -511,7 +531,7 @@ class FormFieldState<T> extends State<FormField<T>> {
_validate
();
break
;
case
AutovalidateMode
.
onUserInteraction
:
if
(
_hasInteractedByUser
)
{
if
(
_hasInteractedByUser
.
value
)
{
_validate
();
}
break
;
...
...
packages/flutter/test/material/text_form_field_restoration_test.dart
0 → 100644
View file @
3eeadc28
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
const
String
text
=
'Hello World! How are you? Life is good!'
;
const
String
alternativeText
=
'Everything is awesome!!'
;
void
main
(
)
{
testWidgets
(
'TextField restoration'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
restorationScopeId:
'app'
,
home:
TestWidget
(),
),
);
await
restoreAndVerify
(
tester
);
});
testWidgets
(
'TextField restoration with external controller'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
restorationScopeId:
'root'
,
home:
TestWidget
(
useExternal:
true
,
),
),
);
await
restoreAndVerify
(
tester
);
});
testWidgets
(
'State restoration (No Form ancestor) - onUserInteraction error text validation'
,
(
WidgetTester
tester
)
async
{
String
?
errorText
(
String
?
value
)
=>
'
$value
/error'
;
late
GlobalKey
<
FormFieldState
<
String
>>
formState
;
Widget
builder
()
{
return
MaterialApp
(
restorationScopeId:
'app'
,
home:
MediaQuery
(
data:
const
MediaQueryData
(
devicePixelRatio:
1.0
),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
state
)
{
formState
=
GlobalKey
<
FormFieldState
<
String
>>();
return
Material
(
child:
TextFormField
(
key:
formState
,
autovalidateMode:
AutovalidateMode
.
onUserInteraction
,
restorationId:
'text_form_field'
,
initialValue:
'foo'
,
validator:
errorText
,
),
);
},
),
),
),
),
);
}
await
tester
.
pumpWidget
(
builder
());
// No error text is visible yet.
expect
(
find
.
text
(
errorText
(
'foo'
)!),
findsNothing
);
await
tester
.
enterText
(
find
.
byType
(
TextFormField
),
'bar'
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorText
(
'bar'
)!),
findsOneWidget
);
final
TestRestorationData
data
=
await
tester
.
getRestorationData
();
await
tester
.
restartAndRestore
();
// Error text should be present after restart and restore.
expect
(
find
.
text
(
errorText
(
'bar'
)!),
findsOneWidget
);
// Resetting the form state should remove the error text.
formState
.
currentState
!.
reset
();
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorText
(
'bar'
)!),
findsNothing
);
await
tester
.
restartAndRestore
();
// Error text should still be removed after restart and restore.
expect
(
find
.
text
(
errorText
(
'bar'
)!),
findsNothing
);
await
tester
.
restoreFrom
(
data
);
expect
(
find
.
text
(
errorText
(
'bar'
)!),
findsOneWidget
);
});
testWidgets
(
'State Restoration (No Form ancestor) - validator sets the error text only when validate is called'
,
(
WidgetTester
tester
)
async
{
String
?
errorText
(
String
?
value
)
=>
'
$value
/error'
;
late
GlobalKey
<
FormFieldState
<
String
>>
formState
;
Widget
builder
(
AutovalidateMode
mode
)
{
return
MaterialApp
(
restorationScopeId:
'app'
,
home:
MediaQuery
(
data:
const
MediaQueryData
(
devicePixelRatio:
1.0
),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
state
)
{
formState
=
GlobalKey
<
FormFieldState
<
String
>>();
return
Material
(
child:
TextFormField
(
key:
formState
,
restorationId:
'form_field'
,
autovalidateMode:
mode
,
initialValue:
'foo'
,
validator:
errorText
,
),
);
},
),
),
),
),
);
}
// Start off not autovalidating.
await
tester
.
pumpWidget
(
builder
(
AutovalidateMode
.
disabled
));
Future
<
void
>
checkErrorText
(
String
testValue
)
async
{
formState
.
currentState
!.
reset
();
await
tester
.
pumpWidget
(
builder
(
AutovalidateMode
.
disabled
));
await
tester
.
enterText
(
find
.
byType
(
TextFormField
),
testValue
);
await
tester
.
pump
();
// We have to manually validate if we're not autovalidating.
expect
(
find
.
text
(
errorText
(
testValue
)!),
findsNothing
);
formState
.
currentState
!.
validate
();
await
tester
.
pump
();
expect
(
find
.
text
(
errorText
(
testValue
)!),
findsOneWidget
);
final
TestRestorationData
data
=
await
tester
.
getRestorationData
();
await
tester
.
restartAndRestore
();
// Error text should be present after restart and restore.
expect
(
find
.
text
(
errorText
(
testValue
)!),
findsOneWidget
);
formState
.
currentState
!.
reset
();
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorText
(
testValue
)!),
findsNothing
);
await
tester
.
restoreFrom
(
data
);
expect
(
find
.
text
(
errorText
(
testValue
)!),
findsOneWidget
);
// Try again with autovalidation. Should validate immediately.
formState
.
currentState
!.
reset
();
await
tester
.
pumpWidget
(
builder
(
AutovalidateMode
.
always
));
await
tester
.
enterText
(
find
.
byType
(
TextFormField
),
testValue
);
await
tester
.
pump
();
expect
(
find
.
text
(
errorText
(
testValue
)!),
findsOneWidget
);
await
tester
.
restartAndRestore
();
// Error text should be present after restart and restore.
expect
(
find
.
text
(
errorText
(
testValue
)!),
findsOneWidget
);
}
await
checkErrorText
(
'Test'
);
await
checkErrorText
(
''
);
});
}
Future
<
void
>
restoreAndVerify
(
WidgetTester
tester
)
async
{
expect
(
find
.
text
(
text
),
findsNothing
);
expect
(
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
pixels
,
0
);
await
tester
.
enterText
(
find
.
byType
(
TextFormField
),
text
);
await
skipPastScrollingAnimation
(
tester
);
expect
(
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
pixels
,
0
);
await
tester
.
drag
(
find
.
byType
(
Scrollable
),
const
Offset
(
0
,
-
80
));
await
skipPastScrollingAnimation
(
tester
);
expect
(
find
.
text
(
text
),
findsOneWidget
);
expect
(
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
pixels
,
60
);
await
tester
.
restartAndRestore
();
expect
(
find
.
text
(
text
),
findsOneWidget
);
expect
(
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
pixels
,
60
);
final
TestRestorationData
data
=
await
tester
.
getRestorationData
();
await
tester
.
enterText
(
find
.
byType
(
TextFormField
),
alternativeText
);
await
skipPastScrollingAnimation
(
tester
);
await
tester
.
drag
(
find
.
byType
(
Scrollable
),
const
Offset
(
0
,
80
));
await
skipPastScrollingAnimation
(
tester
);
expect
(
find
.
text
(
text
),
findsNothing
);
expect
(
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
pixels
,
isNot
(
60
));
await
tester
.
restoreFrom
(
data
);
expect
(
find
.
text
(
text
),
findsOneWidget
);
expect
(
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
pixels
,
60
);
}
class
TestWidget
extends
StatefulWidget
{
const
TestWidget
({
Key
?
key
,
this
.
useExternal
=
false
})
:
super
(
key:
key
);
final
bool
useExternal
;
@override
TestWidgetState
createState
()
=>
TestWidgetState
();
}
class
TestWidgetState
extends
State
<
TestWidget
>
with
RestorationMixin
{
final
RestorableTextEditingController
controller
=
RestorableTextEditingController
();
@override
String
get
restorationId
=>
'widget'
;
@override
void
restoreState
(
RestorationBucket
?
oldBucket
,
bool
initialRestore
)
{
registerForRestoration
(
controller
,
'controller'
);
}
@override
void
dispose
()
{
controller
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Material
(
child:
Align
(
alignment:
Alignment
.
center
,
child:
SizedBox
(
width:
50
,
child:
TextFormField
(
restorationId:
'text'
,
maxLines:
3
,
controller:
widget
.
useExternal
?
controller
.
value
:
null
,
),
),
),
);
}
}
Future
<
void
>
skipPastScrollingAnimation
(
WidgetTester
tester
)
async
{
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
}
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