-
Notifications
You must be signed in to change notification settings - Fork 26.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix TabBarView desynchronized after animation interruption #132748
Fix TabBarView desynchronized after animation interruption #132748
Conversation
3591bff
to
022037a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this approach makes sense, just a question below for clarity. Thanks for your patience while I did a bit of digging on this. It reminded me of #14452
@@ -648,7 +648,9 @@ class DrivenScrollActivity extends ScrollActivity { | |||
} | |||
|
|||
void _end() { | |||
delegate.goBallistic(velocity); | |||
if (!_completer.isCompleted) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a check to see if the controller has been disposed like the comment said? Or was that comment incorrect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my understanding, the comment I removed states that _end
should not be called at all but I agree that it could be read as _end
will have no effect if the _controller
is disposed.
Looking at the Git history this code had very few changes since its introduction (#9575).
But looking more closely it looks like at this time, DrivenScrollActivity._end
was implemented this way:
void _end() {
delegate?.goBallistic(0.0);
}
And the DrivenScrollActivity.dispose
method was the same as now:
@override
void dispose() {
_completer.complete();
_controller.dispose();
super.dispose();
}
But the ActivityScroll.dispose
method was:
void dispose() {
_delegate = null;
}
Which means _end
had no effect once dispose
was called.
So it looks like a bug was introduced during the null safety migration, see #64672.
It changed ActivityScroll.dispose
to:
void dispose() { }
So to answer your question:
Should there be a check to see if the controller has been disposed like the comment said? Or was that comment incorrect?
Yes there should be a check, but AFAIK, there is no standard way to check if the animation controller was disposed so I chose to rely on the completer state.
I plan to update the PR to replace the check on completer with a check on an ‘_isDisposed’ instance variable, that will make the code more self explanatory. I will also add a narrowed test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I investigated this further:
- The comment (
.whenComplete(_end); // won't trigger if we dispose _controller first
) is right, it is expected that the whenComplete parameter is not called when a running animation controller is disposed. - But in our case, the animation controller completes just before being disposed (this has to be on the exact same frame). So at the time the
_end
function is called the animation is disposed. - It seems the issue surfaced because of Futures resolution order :
_controller.whenComplete
is executed before the new activity start but theFuture
it returned is executed after the start of the new activity. I tried hard to write a more narrowed test (one not relying onTabBarController
) but I was not able to figure out one exposing the same Future ordering.
@Piinks I updated the PR with an _isDisposed
flag and some comments.
b204be2
to
b7a7a21
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow! Excellent digging into the history behind this. That you for being so thorough. I think this is nearly ready, just a small tweak below. :)
@@ -628,11 +628,12 @@ class DrivenScrollActivity extends ScrollActivity { | |||
) | |||
..addListener(_tick) | |||
..animateTo(to, duration: duration, curve: curve) | |||
.whenComplete(_end); // won't trigger if we dispose _controller first | |||
.whenComplete(_end); // won't trigger if we dispose _controller before it completes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we have the same condition in BallisticScrollActivity.
I think having the _isDisposed flag makes sense, it's not dissimilar from the mounted
check that is available elsewhere.
Can you move the isDisposed flag to the super class - ScrollActivity - so we can use it the same way in _end for BallisticScrollActivity and DrivenScrollActivity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
I pushed an update where _isDisposed
is moved to ScrollActivity
.
b7a7a21
to
0abd171
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto label is removed for flutter/flutter/132748, due to - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label. |
Hmm I think a rebase would fix the issue here, but it is not enabled for me to be able to update it from here. |
0abd171
to
73d84d2
Compare
73d84d2
to
ff5ca34
Compare
auto label is removed for flutter/flutter/132748, due to - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label. |
Checking on Google testing |
The failure here is an instance of https://github.com/flutter/flutter/wiki/Understanding-Google-Testing#my-cl-reached-max-number-of-retry-why-and-how-do-i-resolve-this I am going to set the check to green manually. |
auto label is removed for flutter/flutter/132748, due to - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label. |
Description
This PR fixes a
DrivenScrollActivity
issue which surfaced in TabBar/TabBarView desynchronization (see #132293).A comment in
DrivenScrollActivity
constructor states that_end
will not be called if the animation controller is disposed.While investigating this tab bar view issue, I noticed that the
_end
method of a firstDrivenScrollActivity
was called after this activity was disposed (and in the tab bar view case, this leads togoBallistic
andgoIdle
being called and inteferring with a newDrivenScrollActivity
that just started).Filing this PR as a WIP to gather feedback because It seems strange that this issue never surfaced before so maybe there is something wrong on how
TabBarView
callsPageController.animateTo()
, but after hours of investigation It seems thatDrivenScrollActivity._end
being called when it is not expected is a good candidate (and the small change in this PR fixes the TabBarView issue). It is also possible that this issue requires very specific conditions to surface.In the TabBarView case this issue surfaced after #122021 that made it possible to interrupt a pending tab bar view animation and start a new one (before that PR, a tab bar view animation had to end before a new one is started).
Related Issue
Fixes #132293.
Tests
Adds 1 tests.
I will add a second one in scroll_activity_test.dart if this fix is valid.