-
Notifications
You must be signed in to change notification settings - Fork 28.7k
fix(CupertinoListTile): Click to wait. #161799
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
Conversation
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group. |
This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
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.
Thank you for putting this together. I had some questions about the implementation and test.
@@ -161,12 +161,20 @@ void main() { | |||
await tester.tap(find.byType(CupertinoListTile)); | |||
await tester.pump(); | |||
|
|||
final TestGesture gesture = await tester.startGesture( |
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.
The combination of both this and the tap just above seems weird. Can you remove one, and still have the tests pass? Do you need this new gesture at all?
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.
In fact, I am indeed a bit unsure about testing this change. Could you provide some advice on how I should test this modification?
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 the best thing to do is to add a new test entirely. Make a test with list tile that has an async callback, one that's long enough so that in the test you can pump it for an amount of time that's long enough for the background color to go back to the original color, but the callback isn't done yet. Theoretically that would fail before your change.
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.
Yes, the best approach would be to create a new test. However, since I modified the click behavior to refresh without waiting, the 'changes backgroundColor when tapped if onTap is provided' test would fail. Additionally, since the existing test can cover the scope of my changes, I didn’t create a new test but instead adjusted the existing one.
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.
Modifying this test to pass, and adding a new one would probably be the best approach then.
@@ -376,13 +376,12 @@ class _CupertinoListTileState extends State<CupertinoListTile> { | |||
() => setState(() { | |||
_tapped = false; | |||
}), | |||
onTapUp: |
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.
Since onTapUp is called just before onTap, would moving the setState to before the widget.onTap inside onTap work the same?
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.
It does achieve the same effect, but I think placing it in onTapUp seems more consistent with the action’s intent—refreshing the style when the finger is lifted.
@MitchellGoodwin |
Can you post a screen recording of what this looks like when navigating to a new page? Part of the native design is that the tile keeps its darkened background while the page is transitioning out. I'm concerned that this implementation will remove that. Screen.Recording.2025-01-27.at.11.35.07.AM.mov |
iOS Settings ListiOS.mp4Before Fixdefault.mp4After Fixdefault.mp4 |
Because when clicking, the style refresh only occurs after the onTap function finishes execution. This raises an issue: if onTap involves a delayed function, it can result in the background remaining gray even after the user has lifted their finger. This could confuse users, making them think the app is lagging or unresponsive. If the goal is to achieve a click effect similar to the iOS settings list, it might not be that simple to implement. It seems that even swipe actions are tied to this behavior. For now, I believe removing the wait is the most appropriate solution. |
Thank you for the videos. What I was worried about is not taking place. I think the proposed solution is fine, and further fine tuning is beyond the scope of this PR. All that's left is the test. |
I sincerely apologize. It seems I mistakenly recorded the wrong video. The correct version after the fix should be: default.mp4 |
Sorry, what exactly do you mean by the content outside the scope of this PR? |
So it looks like the tap effect disappears mid transition, and is also not visible on page return. I don't think we want to regress on that behavior, as I think the visual effect is important to the design. It reduces visual "noise" on the page animating out, and on return draws the eye to where the user was previous interacting with the page.
Apologies, I meant we don't have to get the tap effect perfect in this PR. Fixing the issue you listed is enough. That was in response to:
However if this fix causes other behavior to regress, then that is problematic. |
@MitchellGoodwin This request indeed changes some of the previous design effects. However, in my opinion, I believe this is actually a bug. When onTap is a time-consuming operation, ListTile may exhibit a sort of style lag, and developers cannot control this behavior. This can be quite problematic. Given that this would modify some of the previous design effects, do you think there are other ways to fix this issue? For example, adding a control parameter that allows developers to decide whether they need to wait for the time-consuming operation. |
In general, if a developer wants onTap to trigger an asynchronous function, but does not want to wait for the Future to resolve, than they can just pass a synchronous function to onTap that calls the Future. So I don't think an extra control parameter is necessary. We want to cover cases where the developer is expecting a Future to resolve, but doesn't. The original issue was with launchUrl, which I'm unfamiliar with. Is the Future resolving when the widget is unmounted? |
The initial issue was indeed caused by launchUrl, which occurred due to an incorrect callback. flutter/packages/flutter/lib/src/cupertino/list_tile.dart Lines 370 to 389 in d4772e5
But the root cause is that the click on CupertinoListTile needs to wait for the onTap to complete before refreshing the style. However, due to potential time-consuming operations or interruptions like exceptions, it can result in the background remaining in the backgroundColorActivated state. CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Settings'),
),
child: ListView.separated(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return CupertinoListTile(
title: Text('item $index'),
onTap: () async {
await Future.delayed(
const Duration(milliseconds: 2000), () {});
},
);
},
separatorBuilder: (BuildContext context, int index) => Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Divider(
height: 1,
thickness: 1,
color: Colors.grey[300],
),
),
),
) Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-02-06.at.18.34.27.mp4Video code .CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Settings'),
),
child: ListView.separated(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return CupertinoListTile(
title: Text('item $index'),
onTap: () async {
await Future.delayed(
const Duration(milliseconds: 2000), () {});
},
);
},
separatorBuilder: (BuildContext context, int index) => Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Divider(
height: 1,
thickness: 1,
color: Colors.grey[300],
),
),
),
) The above is what I think is unreasonable. I’ve already finished clicking, and my finger has already left the screen. But the background is still backgroundColorActivated. This will inevitably happen in cases where there is a delay or abnormality in the click event. Of course, if the developer doesn’t use await in onTap, then this issue won’t occur. Based on the above, although this PR can fix the issue, it also removes the previous design effects. Therefore, I’m not sure if it’s appropriate to continue with this PR at this stage. |
I don't think the current fix should be merged in, no. The tile should stay darkened while its onTap is unresolved. If a delay is expected, the onTap can be setup to handle that. If the delay is unexpected than that is either a bug that should be handled in the app code, or is an area we need to investigate. I don't think worrying about delays manually added is worth fixing for, but the launchUrl situation should be looked into. If you have time to look into that and are willing, this PR can be left open and that can happen here. Otherwise we can close this PR for now. |
Hi, I actually think these are two separate issues. But since we’re discussing it here, I did some investigation yesterday. Specifically, I believe the issue occurs when using iOS’s SFSafariViewController. If the close button in the navigation bar is clicked before the page finishes loading, the completion callback isn’t called. This prevents the BasicMessageChannel from completing properly. It also causes the code after await launchUrl() to not execute, as it is still waiting for the callback. |
I submitted a PR, but testing seems to be challenging. I’m not sure how to simulate a user quickly closing it. |
Thank you for putting that PR together. Looks like Stuart responded on it, and he'll have much better feedback than I in that area. I'm going to close this PR for now. I don't think a general change currently can be made on the framework side. It seems difficult for the list tile to make an assumption on how to handle a Future not correctly resolving, and should be done manually by the developer if they are using a callback that has that possibility. |
ok thank you for your guidance. |
fix: #161790
Pre-launch Checklist
///
).