Trent Piercy
Trent Piercy

Reputation: 831

Refresh indicator without scrollview

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?

Upvotes: 73

Views: 54447

Answers (15)

Jett Rawther
Jett Rawther

Reputation: 11

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.

Upvotes: 0

Lortseam
Lortseam

Reputation: 488

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"),
      ),
    ),
  ),
)

Upvotes: 49

Erisan Olasheni
Erisan Olasheni

Reputation: 2905

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,
        ),
      ),
    );
  }
}

Upvotes: 1

Tor-Martin Holen
Tor-Martin Holen

Reputation: 1639

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"),
    ),
  ),
);

Upvotes: 2

Erfan Eghterafi
Erfan Eghterafi

Reputation: 5585

If you are using CustomScrollView

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

Upvotes: 0

makstheimba
makstheimba

Reputation: 166

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

Upvotes: 11

Rohan Taneja
Rohan Taneja

Reputation: 10697

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

Upvotes: 44

AnasSafi
AnasSafi

Reputation: 6264

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

Upvotes: 2

Thai Huynh
Thai Huynh

Reputation: 23

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,
        ),
        ),
    )
    ],
)

Upvotes: 2

Lucas Batista
Lucas Batista

Reputation: 177

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'),
          ),
        ),
      ),
    );
  }
}

Upvotes: 1

Viroth
Viroth

Reputation: 712

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");
                          }
              ),
      ),
)

Upvotes: 2

Nickolay Savchenko
Nickolay Savchenko

Reputation: 1574

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),
    ),
  ),
);

Upvotes: 7

Lucien Theron
Lucien Theron

Reputation: 413

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: () {},
          )

Upvotes: 22

Gustavo
Gustavo

Reputation: 1445

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,
        ),
      ),
    );
  }
}

Upvotes: 142

Arnold Parge
Arnold Parge

Reputation: 6867

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: () {},
)

Upvotes: 3

Related Questions