Commit c08bac83 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Form needs to clean up its scopedWillPopCallback (#7936)

parent 47666af5
...@@ -64,29 +64,36 @@ class Form extends StatefulWidget { ...@@ -64,29 +64,36 @@ class Form extends StatefulWidget {
class FormState extends State<Form> { class FormState extends State<Form> {
int _generation = 0; int _generation = 0;
Set<FormFieldState<dynamic>> _fields = new Set<FormFieldState<dynamic>>(); Set<FormFieldState<dynamic>> _fields = new Set<FormFieldState<dynamic>>();
ModalRoute<dynamic> _route;
@override @override
void dependenciesChanged() { void dependenciesChanged() {
super.dependenciesChanged(); super.dependenciesChanged();
final ModalRoute<dynamic> route = ModalRoute.of(context); if (_route != null && config.onWillPop != null)
if (route != null && config.onWillPop != null) { _route.removeScopedWillPopCallback(config.onWillPop);
// Avoid adding our callback twice by removing it first. _route = ModalRoute.of(context);
route.removeScopedWillPopCallback(config.onWillPop); if (_route != null && config.onWillPop != null)
route.addScopedWillPopCallback(config.onWillPop); _route.addScopedWillPopCallback(config.onWillPop);
}
} }
@override @override
void didUpdateConfig(Form oldConfig) { void didUpdateConfig(Form oldConfig) {
final ModalRoute<dynamic> route = ModalRoute.of(context); assert(_route == ModalRoute.of(context));
if (config.onWillPop != oldConfig.onWillPop && route != null) { if (config.onWillPop != oldConfig.onWillPop && _route != null) {
if (oldConfig.onWillPop != null) if (oldConfig.onWillPop != null)
route.removeScopedWillPopCallback(oldConfig.onWillPop); _route.removeScopedWillPopCallback(oldConfig.onWillPop);
if (config.onWillPop != null) if (config.onWillPop != null)
route.addScopedWillPopCallback(config.onWillPop); _route.addScopedWillPopCallback(config.onWillPop);
} }
} }
@override
void dispose() {
if (_route != null && config.onWillPop != null)
_route.removeScopedWillPopCallback(config.onWillPop);
super.dispose();
}
// Called when a form field has changed. This will cause all form fields // Called when a form field has changed. This will cause all form fields
// to rebuild, useful if form fields have interdependencies. // to rebuild, useful if form fields have interdependencies.
void _fieldDidChange() { void _fieldDidChange() {
......
...@@ -665,10 +665,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -665,10 +665,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// `dependenciesChanged` method: /// `dependenciesChanged` method:
/// ///
/// ```dart /// ```dart
/// ModalRoute<dynamic> _route;
///
/// @override /// @override
/// void dependenciesChanged() { /// void dependenciesChanged() {
/// super.dependenciesChanged(); /// super.dependenciesChanged();
/// ModalRoute.of(context).addScopedWillPopCallback(askTheUserIfTheyAreSure); /// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
/// _route = ModalRoute.of(context);
/// _route?.addScopedWillPopCallback(askTheUserIfTheyAreSure);
/// } /// }
/// ``` /// ```
/// ///
...@@ -679,6 +683,19 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -679,6 +683,19 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// after its route has been disposed. The callback should check [mounted] before /// after its route has been disposed. The callback should check [mounted] before
/// doing anything. /// doing anything.
/// ///
/// A widget that adds a scopedWillPopCallback must ensure that the callback
/// is removed with [removeScopedWillPopCallback] by the time the widget has
/// been disposed. A stateful widget can do this in its dispose method
/// (continuing the previous example):
///
/// ```dart
/// @override
/// void dispose() {
/// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
/// super.dispose();
/// }
/// ```
///
/// See also: /// See also:
/// ///
/// * [Form], which provides an `onWillPop` callback that uses this mechanism. /// * [Form], which provides an `onWillPop` callback that uses this mechanism.
......
...@@ -53,6 +53,15 @@ class SampleForm extends StatelessWidget { ...@@ -53,6 +53,15 @@ class SampleForm extends StatelessWidget {
} }
} }
// Expose the protected hasScopedWillPopCallback getter
class TestPageRoute<T> extends MaterialPageRoute<T> {
TestPageRoute({ WidgetBuilder builder })
: super(builder: builder, maintainState: true, settings: const RouteSettings());
bool get hasCallback => super.hasScopedWillPopCallback;
}
void main() { void main() {
testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async { testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -243,4 +252,63 @@ void main() { ...@@ -243,4 +252,63 @@ void main() {
expect(find.text('Sample Form'), findsNothing); expect(find.text('Sample Form'), findsNothing);
}); });
testWidgets('Route.scopedWillPop callbacks do not accumulate', (WidgetTester tester) async {
StateSetter contentsSetState; // call this to rebuild the route's SampleForm contents
bool contentsEmpty = false; // when true, don't include the SampleForm in the route
TestPageRoute<Null> route = new TestPageRoute<Null>(
builder: (BuildContext context) {
return new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
contentsSetState = setState;
return contentsEmpty ? new Container() : new SampleForm(key: new UniqueKey());
}
);
},
);
Widget buildFrame() {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(title: new Text('Home')),
body: new Builder(
builder: (BuildContext context) {
return new Center(
child: new FlatButton(
child: new Text('X'),
onPressed: () {
Navigator.of(context).push(route);
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Form'), findsOneWidget);
expect(route.hasCallback, isTrue);
// Rebuild the route's SampleForm child an additional 3x for good measure.
contentsSetState(() { });
await tester.pump();
contentsSetState(() { });
await tester.pump();
contentsSetState(() { });
await tester.pump();
// Now build the route's contents without the sample form.
contentsEmpty = true;
contentsSetState(() { });
await tester.pump();
expect(route.hasCallback, isFalse);
});
} }
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