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

Fix memory leak in TransitionRoute (#42777)

parent 1faf6a9a
......@@ -228,31 +228,49 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
final Animation<double> current = _secondaryAnimation.parent;
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;
newAnimation = TrainHoppingAnimation(
current.currentTrain,
nextRoute._animation,
currentTrain,
nextTrain,
onSwitchedTrain: () {
assert(_secondaryAnimation.parent == newAnimation);
assert(newAnimation.currentTrain == nextRoute._animation);
_secondaryAnimation.parent = newAnimation.currentTrain;
_setSecondaryAnimation(newAnimation.currentTrain, nextRoute.completed);
newAnimation.dispose();
},
);
_secondaryAnimation.parent = newAnimation;
_setSecondaryAnimation(newAnimation, nextRoute.completed);
}
if (current is TrainHoppingAnimation) {
current.dispose();
} else {
_secondaryAnimation.parent = TrainHoppingAnimation(current, nextRoute._animation);
}
} else {
_secondaryAnimation.parent = nextRoute._animation;
_setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
}
} else {
_secondaryAnimation.parent = kAlwaysDismissedAnimation;
_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;
if (animation is TrainHoppingAnimation) {
animation.dispose();
}
}
});
}
/// Returns true if this route supports a transition animation that runs
/// when [nextRoute] is pushed on top of it or when [nextRoute] is popped
/// off of it.
......
......@@ -504,6 +504,7 @@ void main() {
verifyNoMoreInteractions(pageRouteAware);
});
});
testWidgets('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
......@@ -532,6 +533,262 @@ void main() {
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> { }
......@@ -539,3 +796,12 @@ class MockPageRoute extends Mock implements PageRoute<dynamic> { }
class MockRoute extends Mock implements Route<dynamic> { }
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