71

I know with a ListView or SingleChildScrollView RefreshIndicator just works natively but I want to use a RefreshIndicator on a page that doesn't scroll. Is this even possible and if so, how?

15 Answers 15

141

You must do the following:

  • Use a SingleChildScrollView with the property physics set to AlwaysScrollableScrollPhysics().
  • Make sure it's child has a height of the size of the screen, which you can get with MediaQuery.of(context).size.height.

The complete example:

classs MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: () {},
      child: SingleChildScrollView(
        physics: AlwaysScrollableScrollPhysics(),
        child: Container(
          child: Center(
            child: Text('Hello World'),
          ),
          height: MediaQuery.of(context).size.height,
        ),
      ),
    );
  }
}
5
  • Somewhat related, I had a listview.builder somewhere in the widget tree and I just needed to use the physics AlwaysScrollableScrollPhysics() and the media query height in the container above it in order to achieve the same. Commented Jan 17, 2021 at 18:20
  • What if I want to a scroll view without using ListView ?PLZ tell! I have a future builder as child of refresh indicator which is scrollable Commented Mar 25, 2021 at 19:01
  • This will not work in almost all cases, because as soon as you have a toolbar at the top or at the bottom, the MediaQuery.of(context).size.height expression would give you the height of the whole screen, not the height between the toolbars, and your content will always overflow to the bottom. The correct asnwer is this one, it does not require to mess with the height: stackoverflow.com/a/62157637/662084
    – afrish
    Commented Apr 13, 2023 at 15:36
  • Dear @GautamGoyal please check this: stackoverflow.com/a/79085854/22675249
    – Fady Fawzy
    Commented Oct 14 at 10:54
  • Dear @AxesGrinds please check this: stackoverflow.com/a/79085854/22675249
    – Fady Fawzy
    Commented Oct 14 at 10:55
49

Most of the other answers will work but they all have some downsides:

  • Using a SingleChildScrollView without adjusting the height will cause the scrolling glow effect to be not at the bottom of the page.
  • Setting the height to the maximum height of the current view (by using MediaQuery or a LayoutBuilder) will fix that, but there will be a render overflow when your content's height is bigger than the height of the available space.

Eventually, the following solution works perfectly without any problems:

LayoutBuilder(
  builder: (context, constraints) => RefreshIndicator(
    onRefresh: () async {},
    child: SingleChildScrollView(
      physics: AlwaysScrollableScrollPhysics(),
      child: ConstrainedBox(
        constraints: BoxConstraints(
          minWidth: constraints.maxWidth,
          minHeight: constraints.maxHeight,
        ),
        child: Text("My Widget"),
      ),
    ),
  ),
)
2
  • 1
    This solution is the best so far, as you said when setting it to the screen height you will simply get render overflow, but with your answer the issue is totally handled Commented Nov 23, 2022 at 12:40
  • Almost correct but spaces after the My Widget text doesn't trigger refresh, use minWidth: constraints.maxWIdth to fix this issue. Commented Jul 16, 2023 at 6:29
42

The SingleChildScrollView solution in other answers would work for most cases.

I had a use case* where it didn't work because the content inside the SingleChildScrollView wanted to expand to fill the remaining height. So the SingleChildScrollView and Expanded widgets contradicted each other.

What saved the day, in the end, was using a CustomScrollView with a SliverFillRemaining widget.

No custom widget size calculation was needed.

pull-to-refresh-with-custom-scroll-view

Code:

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: RefreshIndicator(
          onRefresh: () async {
            await Future.delayed(const Duration(seconds: 1));
          },
          child: CustomScrollView(
            slivers: <Widget>[
              SliverFillRemaining(
                child: Container(
                  color: Colors.blue,
                  child: Center(
                    child: Text("No results found."),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

You can test this on DartPad as well.

*My use case:

pull-to-refresh-complex-case

4
  • 1
    this is way better than setting the height Commented Dec 8, 2023 at 22:31
  • this should be the final answer Commented Jun 5 at 19:55
  • 1
    It looks like the recent Flutter upgrade broke both these approaches...
    – Gábor
    Commented Jul 25 at 19:13
  • thank you sir, this is works on my non-scrollable layout
    – justnoone
    Commented Oct 31 at 2:35
22

So in my case I wanted to display a placeholder in an empty list:

          RefreshIndicator(
            child: Stack(
              children: <Widget>[
                Center(
                  child: Text("The list is empty"),
                ),
                ListView()
              ],
            ),
            onRefresh: () {},
          )
2
  • 2
    this is what i use as well. but for a widgets above the listview that requires users to tap on buttons, it does not work.
    – chitgoks
    Commented Apr 13, 2020 at 1:08
  • @chitgoks sorry didn't get you. Which "buttons" are you referring to? Commented Aug 13, 2021 at 8:04
11

I aggree with others about SingleChildScrollView solution. However it will work differently on android and on ios.

On android all is well but on ios you will get a pretty ugly overscroll behavior.

enter image description here

You will not be able to fix this behavior by using ClampingScrollPhysics instead of AlwaysScrollableScrollPhysics because it will break RefreshIndicator.

A really good options is to override default ScrollConfiguration so it looks the same on both platforms

Here is a whole code for a custom widget:

import 'package:flutter/material.dart';

// NOTE: this is really important, it will make overscroll look the same on both platforms
class _ClampingScrollBehavior extends ScrollBehavior {
  @override
  ScrollPhysics getScrollPhysics(BuildContext context) => ClampingScrollPhysics();
}

class NonScrollableRefreshIndicator extends StatelessWidget {
  final Widget child;
  final Future<void> Function() onRefresh;

  const NonScrollableRefreshIndicator({
    required this.child,
    required this.onRefresh,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: ((_, constraints) {
        return RefreshIndicator(
          onRefresh: onRefresh,
          child: ScrollConfiguration(
            // Customize scroll behavior for both platforms
            behavior: _ClampingScrollBehavior(),
            child: SingleChildScrollView(
              physics: AlwaysScrollableScrollPhysics(),
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minHeight: constraints.maxHeight,
                  maxHeight: constraints.maxHeight
                ),
                child: child,
              ),
            ),
          ),
        );
      }),
    );
  }
}

And it will behave like this on both platforms: enter image description here

7

More flexible solution is put your scrollable empty/error state widget into LayoutBuilder

LayoutBuilder(
  builder: (context, constraints) => SingleChildScrollView(
    physics: AlwaysScrollableScrollPhysics(),
    child: SizedBox(
      height: constraints.maxHeight,
      child: ErrorState(
          subtitle: (snapshot.data as ErrorStateful).errorMessage),
    ),
  ),
);
1
  • Excelent solution!!! The best for me, thx
    – Shadros
    Commented Jul 18, 2023 at 14:23
3

Is it possible without page that doesn't scroll?

-No.


Please read the documentation of the parameter child in RefreshIndicator:

The widget below this widget in the tree.

The refresh indicator will be stacked on top of this child. The indicator will appear when child's Scrollable descendant is over-scrolled.

Typically a [ListView] or [CustomScrollView].


Is there a work-around?

-Yes

You can put only one widget in the Listview's children list:

new RefreshIndicator(
  child: new ListView(
    children: <Widget>[
      new Text('This is child with refresh indicator'),
    ],
  ),
  onRefresh: () {},
)
1
  • 1
    that's not really a workaround, that's exactly what the OP wants to avoid. A workaround would be to add a drag gesture that brings a refresh indicator from the top, if the release is about some amount of pixels below the start of the drag, then, trigger a the refresh method you want.'
    – nosmirck
    Commented Sep 7, 2018 at 17:20
2

In ListView.builder use physics: AlwaysScrollableScrollPhysics()

Example:

RefreshIndicator(
   onRefresh: () => _onRefresh(),
   child: Center(
            child: ListView.builder(
                     physics: AlwaysScrollableScrollPhysics(),
                      controller: _scrollController,
                      itemCount: 4,
                      addAutomaticKeepAlives: true,
                      itemBuilder: (BuildContext context, int position) {
                            return Text("Hello $position");
                          }
              ),
      ),
)
2

You can use CustomScrollView with only one child SliverFillRemaining as my case for showing no data widget:

CustomScrollView(
    slivers: [
    SliverFillRemaining(
        child: Center(
        child: Text(
            AppLocalizations.of(context).noData,
        ),
        ),
    )
    ],
)
2

This is full code of what you need (Includes solved the problem of height when use appBar):

import 'package:flutter/material.dart';

class RefreshFullScreen extends StatefulWidget {
  const RefreshFullScreen({Key? key}) : super(key: key);

  @override
  _RefreshFullScreenState createState() => _RefreshFullScreenState();
}

class _RefreshFullScreenState extends State<RefreshFullScreen> {
  @override
  Widget build(BuildContext context) {

    double height = MediaQuery.of(context).size.height; // Full screen width and height
    EdgeInsets padding = MediaQuery.of(context).padding; // Height (without SafeArea)
    double netHeight = height - padding.top - kToolbarHeight; // Height (without status and toolbar)

    return Scaffold(
      appBar: AppBar(),
      body: RefreshIndicator(
        onRefresh: () {
          return Future.delayed(
            Duration(seconds: 1),
                () {
            },
          );
        }, child: SingleChildScrollView(
            physics: AlwaysScrollableScrollPhysics(),
            child: Container(
              color: Colors.red,
              height: netHeight,
            )
      ),
      ),
    );
  }
}

And this is the result:

enter image description here

2

Global Key Solution

As the question asks explicitly for a solution to "use a RefreshIndicator on a page that doesn't scroll."

There is one way, but it might be considered hacky - it is possible define a GlobalKey to the RefreshIndicator to get a reference to the RefreshIndicatorState, which have a public show method hence there is no need for any scrollable widgets.

The downside is that since there is no scrolling, the indicator does not gradually come into the screen, rather it instantly appears and animates out.

final GlobalKey<RefreshIndicatorState> refreshIndicatorKey = GlobalKey();

For use cases that need to programmatically trigger the RefreshIndicator this approach might be interesting.

Example:

RefreshIndicator(
  key: refreshIndicatorKey,
  onRefresh: () async {
    await Future.delayed(const Duration(seconds: 1));
  },
  child: Center(
    child: ElevatedButton(
      onPressed: () {
        refreshIndicatorKey.currentState?.show(atTop: true);
      },
      child: const Text("Refresh"),
    ),
  ),
);
1

Complementing Gustavo answer you can use constraints to define the minimum size of SingleChildScrollView in this way

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: () {},
      child: ConstrainedBox(
        constraints: BoxConstraints(
          minHeight: MediaQuery.of(context).size.height,
        ),
        child: SingleChildScrollView(
          physics: AlwaysScrollableScrollPhysics(),
          child: Center(
            child: Text('Hello World'),
          ),
        ),
      ),
    );
  }
}
1

Going with @Gustavo's answer, in a situation whereby you want a refresh indicator also when the child content overflows, and you want a scroll at the same time, just wrap the child with another refreshing indicator with a scrolling physics, like this:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: () {},
      child: SingleChildScrollView(
        physics: AlwaysScrollableScrollPhysics(),
        child: Container(
          child: RefreshIndicator(// wrap the child with a new RefreshIndicator here
              onRefresh: () {},
              child: SingleChildScrollView(// A new ScrollView Widget here
                  physics: const BouncingScrollPhysics(),//A new scroll physics here
                  child: Center(
                    child: Text('Hello World'), // You can make this a scroll overflown child
                  ))),
          height: MediaQuery.of(context).size.height,
        ),
      ),
    );
  }
}

0

If you are using CustomScrollView

 return RefreshIndicator(
       onRefresh: () {},
      child: Stack( // <--- use Stack
        children: [
          CustomScrollView(
            physics: AlwaysScrollableScrollPhysics(), // <--- set physics
            slivers: [
             ...
            ],
          ),
        ],
      ),
    );
0

Use AlwaysScrollableScrollPhysics() physics in your ListView(builder or seperated) or SingleChildScrollView to solve the problem. But if you are using this physics inside a ListView(builder or seperated), then atleast one element has to be present. If you want to use ListView and use this pull down to refresh method, then try to wrap the ListView inside a SingleChildScrollView, And use ClampingScrollPhysics in ListView and AlwaysScrollPhysics in SingleChildScrollView. Don't forgot to add shrinkWrap: true in ListView. Then wrap the SingleChildScrollView in a SizedBox and give height as height: MediaQuery.of(context).size.height. If you need any help please comment.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.