Skip to content

Show RefreshIndicator on top when scroll's axis direction is up (matching native behaviour)#93779

Merged
fluttergithubbot merged 3 commits intoflutter:masterfrom
NevercodeHQ:refresh_indicator_reverse
Feb 18, 2022
Merged

Show RefreshIndicator on top when scroll's axis direction is up (matching native behaviour)#93779
fluttergithubbot merged 3 commits intoflutter:masterfrom
NevercodeHQ:refresh_indicator_reverse

Conversation

@TahaTesser
Copy link
Contributor

@TahaTesser TahaTesser commented Nov 17, 2021

Fixes #26758

Created a repo for testing RefreshIndicator with SwipeRefreshLayout
https://github.com/TahaTesser/refresh_indicator_reverse_example
(Based on https://github.com/nt4f04uNd/tabs_overlay, thanks @nt4f04uNd)

Showcase

This is commonly used in a chat app where new messages appear on top in a reversed list and when user want to load previous messages they scroll from top and indicator is shown for loading older messages (as new messages always at the bottom and these messages are usually loaded first).

Also looks like SwipeRefreshLayout doesn't support pull from bottom https://stackoverflow.com/questions/24867204/swiperefreshlayout-pull-from-bottom

This PR shows RefreshIndicator on top when scroll's axis direction is up (matching native behaviour)

Current behaviour when RefreshIndicatoris wrapped around reverse ListView.

Pull from bottom Pull from top
Flutter triggers RefreshIndicator while Android doesn't. SwipeRefreshLayouttriggered by Android.

With this PR, when RefreshIndicatoris wrapped around reverse ListView

Pull from bottom Pull from top
Flutter doesn't triggers RefreshIndicator matching Android behaviour Both RefreshIndicator & SwipeRefreshLayout are triggered.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Nov 17, 2021
@google-cla google-cla bot added the cla: yes label Nov 17, 2021
@TahaTesser TahaTesser changed the title Add reverse property to `RefreshIndicator Add reverse property to RefreshIndicator Nov 17, 2021
@pattaravitappsynth
Copy link

I want this one!!

Copy link
Contributor

@darrenaustin darrenaustin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. Looks good. Just a small typo and a question below.

It is a shame we can't have some way for this to just track the reverse setting on the scrollable descendent so that this just works out of the box, but this at least gives apps the ability to manually set both the scrollable and the indicator to handle the reverse case.

@Piinks Piinks self-requested a review January 10, 2022 21:30
@Piinks Piinks added the f: scrolling Viewports, list views, slivers, etc. label Jan 10, 2022
Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a shame we can't have some way for this to just track the reverse setting on the scrollable descendent so that this just works out of the box

You should be able to do this by checking the AxisDirection of the notification. That is how we do this in widgets like the Scrollbar.

It looks like the RefreshIndicator already does this though. When the ListView is reversed, the RefreshIndicator moves to the bottom, so it is always there at the leading edge of the scroll view. I think what this is trying to do is put it at the trailing edge instead.

@darrenaustin
Copy link
Contributor

It looks like the RefreshIndicator already does this though. When the ListView is reversed, the RefreshIndicator moves to the bottom, so it is always there at the leading edge of the scroll view. I think what this is trying to do is put it at the trailing edge instead.

Yeah, it does track the scroll direction properly. The request from #26758 was to have it appear at the top of the list if the ListView is using the reverse. I was just wondering if there was a way that the indicator could pick up on this without needed a new reverse parameter that would manually need to be set by the app.

Which now that I think about it, the request seems a bit odd. The whole UI gesture of 'pull down to refresh' is built on the idea that new items usually appear at the top of the list, so you scroll to the top of the list and 'pull' for more. In the case of a chat app using the reverse setting, new items will appear at the bottom, so pulling down from the top is a little odd.

@TahaTesser TahaTesser force-pushed the refresh_indicator_reverse branch from 341ea4d to 86cea90 Compare January 24, 2022 13:30
@TahaTesser TahaTesser force-pushed the refresh_indicator_reverse branch from 86cea90 to c421f20 Compare January 25, 2022 10:52
@TahaTesser TahaTesser changed the title Add reverse property to RefreshIndicator Show RefreshIndicator on top when scroll's axis direction is up (matching native behavioiur) Jan 25, 2022
@TahaTesser
Copy link
Contributor Author

@darrenaustin @Piinks
I agree that adding parameter is not the way to go. Please checkout my updated description and new code.
Let me know if there are other changes needed to improvide this behaviour, thanks

@TahaTesser TahaTesser changed the title Show RefreshIndicator on top when scroll's axis direction is up (matching native behavioiur) Show RefreshIndicator on top when scroll's axis direction is up (matching native behaviour) Jan 25, 2022
@Piinks Piinks requested a review from nt4f04uNd February 2, 2022 22:27
@Piinks Piinks added the a: fidelity Matching the OEM platforms better label Feb 2, 2022
Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great - thank you for pursuing a full fidelity update here.

I have added @nt4f04uNd because they are working on a big fidelity update as well, review not necessary, just added as an FYI :)

);

await tester.fling(find.text('X'), const Offset(0.0, -300.0), 1000.0);
await tester.fling(find.text('X'), const Offset(0.0, 600.0), 1000.0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm running this PR through internal testing now, I am concerned it may be breaking, but hopeful it won't be.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@darrenaustin actually approved this (so I won't make changes to turn it into parameter based direction)
Can you please take a look if this breaks any internal tests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out internal presubmit tester is having some issues, I think this is ok to land though.

Copy link
Member

@nt4f04uNd nt4f04uNd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is probably a case where we should provide a better behavior for users, rather than match the native behavior. It seems right to me that refresh indicator shows by default at the leading extent.

Although, #26758 seems a valid feature request and is saying they want to change this behavior, so perhaps the reverse (or something like appearAtEnd) parameter is a better way to go? Adding docs about all of this also would be good.

What do you guys think?

@nt4f04uNd
Copy link
Member

nt4f04uNd commented Feb 2, 2022

I also cloned your PR and tried running it with BouncingScrollPhysics, and refresh indicator didn't appear anywhere

@TahaTesser
Copy link
Contributor Author

@nt4f04uNd
If fidelity is not important, I would love to add something like appearAtEnd

@TahaTesser
Copy link
Contributor Author

TahaTesser commented Feb 4, 2022

@nt4f04uNd
Thanks for the test, I am surprised why it is broken with BouncingScrollPhysics.
I will take look into adding parameters instead as well

Copy link
Contributor

@darrenaustin darrenaustin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM.

@TahaTesser
Copy link
Contributor Author

TahaTesser commented Feb 16, 2022

@nt4f04uNd @Piinks
I am not an export in scroll physics but it looks like RefreshIndicator should use AlwaysScrollableScrollPhysics, not BouncingScrollPhysics (even tho it does show an indicator without this PR for some reason)

Minimal example, this never triggers overscroll.

      body: Center(
        child: NotificationListener<OverscrollNotification>(
          onNotification: (OverscrollNotification overscroll) {
            print('overscroll: $overscroll');
            return false;
          },
          child: ListView.builder(
            reverse: true,
            physics: const BouncingScrollPhysics(),
            itemCount: 25,
            itemBuilder: (BuildContext context, int index) {
              return  ListTile(
                title: Text('List item -  $index'),
              );
            },
          ),
        ),
      ),

Also, the Troubleshooting section in the documentation also suggests this and stackoverflow

@TahaTesser TahaTesser requested a review from Piinks February 16, 2022 13:59
@Piinks
Copy link
Contributor

Piinks commented Feb 16, 2022

I think @TahaTesser's comment about BouncingScrollPhysics not being compatible by default makes sense. The docs for BouncingScrollPhysics even mention there are caveats to this type of ScrollPhysics triggering overscroll:

BouncingScrollPhysics by itself will not create an overscroll effect if the contents of the scroll view do not extend beyond the size of the viewport. To create the overscroll and bounce effect regardless of the length of your scroll view, combine with AlwaysScrollableScrollPhysics.

And the docs for the refresh indicator specify you need to trigger an overscroll to trigger the refresh. We may be able to make this clearer though in the docs.

Often on iOS you'll find that AlwaysScrollableScrollPhysics being used with BouncingScrollPhysics. :)

Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this achieves the desired result after checking it out and testing.

If I use the following sample (from the api docs, master branch: https://master-api.flutter.dev/flutter/material/RefreshIndicator-class.html), on iOS it works fine, until I set reverse true on the ListView. Then I can't make the refresh indicator appear at all. It doesn't seem right that it works for reverse: false, but not reverse: true now.
On Android, it appears to work fine both ways.

This seems like it might be broken, can you take a look @TahaTesser?

Here's the code sample:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'RefreshIndicator Sample';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: RefreshIndicatorExample(title: _title),
    );
  }
}

class RefreshIndicatorExample extends StatefulWidget {
  const RefreshIndicatorExample({Key? key, required this.title})
      : super(key: key);

  final String title;

  @override
  State<RefreshIndicatorExample> createState() =>
      _RefreshIndicatorExampleState();
}

class _RefreshIndicatorExampleState extends State<RefreshIndicatorExample> {
  final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
      GlobalKey<RefreshIndicatorState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: RefreshIndicator(
        key: _refreshIndicatorKey,
        color: Colors.white,
        backgroundColor: Colors.blue,
        strokeWidth: 4.0,
        onRefresh: () async {
          // Replace this delay with the code to be executed during refresh
          // and return a Future when code finishs execution.
          return Future<void>.delayed(const Duration(seconds: 3));
        },
        // Pull from top to show refresh indicator.
        child: ListView.builder(
          reverse: true,
          itemCount: 25,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              title: Text('Item $index'),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          // Show refresh indicator programmatically on button tap.
          _refreshIndicatorKey.currentState?.show();
        },
        icon: const Icon(Icons.refresh),
        label: const Text('Show Indicator'),
      ),
    );
  }
}

@TahaTesser TahaTesser force-pushed the refresh_indicator_reverse branch from c421f20 to fed4f5a Compare February 17, 2022 11:09
@skia-gold
Copy link

Gold has detected about 38 new digest(s) on patchset 3.
View them at https://flutter-gold.skia.org/cl/github/93779

@TahaTesser
Copy link
Contributor Author

TahaTesser commented Feb 17, 2022

@Piinks
Thank you so much for taking the time to test this

Looks like I missed adjusting _dragOffset, this is now fixed and I also added a test for this using BouncingScrollPhysics.

Preview

Screen.Recording.2022-02-17.at.11.41.44.mov

I've renamed some existing tests to match new behavior bottom doesn't seem right in the title as this PR would bring refresh indicator on top matching native behavior.

@TahaTesser TahaTesser requested a review from Piinks February 17, 2022 13:05
Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you for the quick follow up!

@TahaTesser TahaTesser force-pushed the refresh_indicator_reverse branch from fed4f5a to facec0f Compare February 18, 2022 15:24
@fluttergithubbot fluttergithubbot merged commit 14a406f into flutter:master Feb 18, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 18, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 18, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 18, 2022
@TahaTesser TahaTesser deleted the refresh_indicator_reverse branch February 18, 2022 19:08
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 18, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 19, 2022
clocksmith pushed a commit to clocksmith/flutter that referenced this pull request Mar 8, 2022
@garg204
Copy link

garg204 commented Jun 1, 2022

Hey @TahaTesser , I tried this code with reverse set to true but it is not working for me.

Can you please suggest what change I need to make

environment:
sdk: '>=2.12.0 <3.0.0'
flutter: '>=1.17.0'

RefreshIndicator(
            onRefresh: () async {
              //Do whatever you want on refrsh.Usually update the date of the listview
            },
            child: NotificationListener<OverscrollNotification>(
              onNotification: (OverscrollNotification overscroll) {
                print('overscroll: $overscroll');
                return true;
              },
              child: ListView.builder(
                physics: const BouncingScrollPhysics(),
                itemCount: messages.length,
                itemBuilder: (context, index) =>
                    widgetCreator.getWidget(context, messages[index]),
                reverse: true,
              ),
            ),
            color: context.colorTokens.iconDynamicDefault,
          )

@AndreiMisiukevich
Copy link

Hello @TahaTesser

Do I understand correctly that RefreshIndicator is not supposed to work with reverse list view?
Messenger app is a pretty common use-case for upside-down pull-to-refresh behavior.
Now it seems impossible to achieve (except tricks with RotatedBox which won't be 100% working it changes axisdirection of list view).

Thanks

@MattyBoy4444
Copy link

Hello @TahaTesser

Do I understand correctly that RefreshIndicator is not supposed to work with reverse list view? Messenger app is a pretty common use-case for upside-down pull-to-refresh behavior. Now it seems impossible to achieve (except tricks with RotatedBox which won't be 100% working it changes axisdirection of list view).

Thanks

I agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: fidelity Matching the OEM platforms better f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RefreshIndicator pull-down-to-refresh (from top) not working with ListView.builder reverse:true

10 participants