Unverified Commit ddca78b4 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Fix memory leak in TransitionRoute (#42777)

parent 1faf6a9a
...@@ -228,30 +228,48 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -228,30 +228,48 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) { if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
final Animation<double> current = _secondaryAnimation.parent; final Animation<double> current = _secondaryAnimation.parent;
if (current != null) { if (current != null) {
if (current is TrainHoppingAnimation) { final Animation<double> currentTrain = current is TrainHoppingAnimation ? current.currentTrain : current;
final Animation<double> nextTrain = nextRoute._animation;
if (currentTrain.value == nextTrain.value) {
_setSecondaryAnimation(nextTrain, nextRoute.completed);
} else {
TrainHoppingAnimation newAnimation; TrainHoppingAnimation newAnimation;
newAnimation = TrainHoppingAnimation( newAnimation = TrainHoppingAnimation(
current.currentTrain, currentTrain,
nextRoute._animation, nextTrain,
onSwitchedTrain: () { onSwitchedTrain: () {
assert(_secondaryAnimation.parent == newAnimation); assert(_secondaryAnimation.parent == newAnimation);
assert(newAnimation.currentTrain == nextRoute._animation); assert(newAnimation.currentTrain == nextRoute._animation);
_secondaryAnimation.parent = newAnimation.currentTrain; _setSecondaryAnimation(newAnimation.currentTrain, nextRoute.completed);
newAnimation.dispose(); newAnimation.dispose();
}, },
); );
_secondaryAnimation.parent = newAnimation; _setSecondaryAnimation(newAnimation, nextRoute.completed);
}
if (current is TrainHoppingAnimation) {
current.dispose(); current.dispose();
} else {
_secondaryAnimation.parent = TrainHoppingAnimation(current, nextRoute._animation);
} }
} else { } else {
_secondaryAnimation.parent = nextRoute._animation; _setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
} }
} else { } else {
_setSecondaryAnimation(kAlwaysDismissedAnimation);
}
}
void _setSecondaryAnimation(Animation<double> animation, [Future<dynamic> disposed]) {
_secondaryAnimation.parent = animation;
// Release the reference to the next route's animation when that route
// is disposed.
disposed?.then((dynamic _) {
if (_secondaryAnimation.parent == animation) {
_secondaryAnimation.parent = kAlwaysDismissedAnimation; _secondaryAnimation.parent = kAlwaysDismissedAnimation;
if (animation is TrainHoppingAnimation) {
animation.dispose();
} }
} }
});
}
/// Returns true if this route supports a transition animation that runs /// Returns true if this route supports a transition animation that runs
/// when [nextRoute] is pushed on top of it or when [nextRoute] is popped /// when [nextRoute] is pushed on top of it or when [nextRoute] is popped
......
...@@ -504,6 +504,7 @@ void main() { ...@@ -504,6 +504,7 @@ void main() {
verifyNoMoreInteractions(pageRouteAware); verifyNoMoreInteractions(pageRouteAware);
}); });
}); });
testWidgets('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async { testWidgets('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -532,6 +533,262 @@ void main() { ...@@ -532,6 +533,262 @@ void main() {
expect(focusNode.hasPrimaryFocus, isTrue); expect(focusNode.hasPrimaryFocus, isTrue);
}); });
group('TrasitionRoute', () {
testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation;
animationPageOne = animation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation secondaryAnimationProxyPageTwo;
ProxyAnimation animationPageTwo;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageTwo = secondaryAnimation;
animationPageTwo = animation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent;
expect(animationPageTwo.value, 1.0);
expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation);
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Pop page two, the secondary animation of page one becomes
// kAlwaysDismissedAnimation.
navigator.currentState.pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
await tester.pumpAndSettle();
expect(animationPageTwo.value, 0.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
});
testWidgets('secondary animation is kDismissed when next route is removed', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation;
animationPageOne = animation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation secondaryAnimationProxyPageTwo;
ProxyAnimation animationPageTwo;
Route<void> secondRoute;
navigator.currentState.push(
secondRoute = PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageTwo = secondaryAnimation;
animationPageTwo = animation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent;
expect(animationPageTwo.value, 1.0);
expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation);
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Remove the second route, the secondary animation of page one is
// kAlwaysDismissedAnimation again.
navigator.currentState.removeRoute(secondRoute);
await tester.pump();
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
});
testWidgets('secondary animation is kDismissed after train hopping finishes and pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation;
animationPageOne = animation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation animationPageTwo;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
animationPageTwo = animation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Replace with a different route while push is ongoing to trigger
// TrainHopping.
ProxyAnimation animationPageThree;
navigator.currentState.pushReplacement(
TestPageRouteBuilder(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
animationPageThree = animation;
return const Text('Page Three');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent;
expect(trainHopper.currentTrain, animationPageTwo.parent);
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, isNot(isA<TrainHoppingAnimation>()));
expect(secondaryAnimationPageOne.parent, animationPageThree.parent);
expect(trainHopper.currentTrain, isNull); // Has been disposed.
await tester.pumpAndSettle();
expect(secondaryAnimationPageOne.parent, animationPageThree.parent);
// Pop page three.
navigator.currentState.pop();
await tester.pump();
await tester.pumpAndSettle();
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
});
testWidgets('secondary animation is kDismissed when train hopping is interrupted', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation;
animationPageOne = animation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation animationPageTwo;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
animationPageTwo = animation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Replace with a different route while push is ongoing to trigger
// TrainHopping.
navigator.currentState.pushReplacement(
TestPageRouteBuilder(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('Page Three');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent;
expect(trainHopper.currentTrain, animationPageTwo.parent);
// Pop page three while replacement push is ongoing.
navigator.currentState.pop();
await tester.pump();
expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
final TrainHoppingAnimation trainHopper2 = secondaryAnimationPageOne.parent;
expect(trainHopper2.currentTrain, animationPageTwo.parent);
expect(trainHopper.currentTrain, isNull); // Has been disposed.
await tester.pumpAndSettle();
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
expect(trainHopper2.currentTrain, isNull); // Has been disposed.
});
});
} }
class MockPageRoute extends Mock implements PageRoute<dynamic> { } class MockPageRoute extends Mock implements PageRoute<dynamic> { }
...@@ -539,3 +796,12 @@ class MockPageRoute extends Mock implements PageRoute<dynamic> { } ...@@ -539,3 +796,12 @@ class MockPageRoute extends Mock implements PageRoute<dynamic> { }
class MockRoute extends Mock implements Route<dynamic> { } class MockRoute extends Mock implements Route<dynamic> { }
class MockRouteAware extends Mock implements RouteAware { } class MockRouteAware extends Mock implements RouteAware { }
class TestPageRouteBuilder extends PageRouteBuilder<void> {
TestPageRouteBuilder({RoutePageBuilder pageBuilder}) : super(pageBuilder: pageBuilder);
@override
Animation<double> createAnimation() {
return CurvedAnimation(parent: super.createAnimation(), curve: Curves.easeOutExpo);
}
}
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