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
You must do the following:
- Use a
SingleChildScrollView
with the propertyphysics
set toAlwaysScrollableScrollPhysics()
. - 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,
),
),
);
}
}
-
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– afrishCommented Apr 13, 2023 at 15:36 -
Dear @GautamGoyal please check this: stackoverflow.com/a/79085854/22675249 Commented Oct 14 at 10:54
-
Dear @AxesGrinds please check this: stackoverflow.com/a/79085854/22675249 Commented Oct 14 at 10:55
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 aLayoutBuilder
) 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"),
),
),
),
)
-
1This 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
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.
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:
-
1
-
-
1It looks like the recent Flutter upgrade broke both these approaches...– GáborCommented Jul 25 at 19:13
-
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: () {},
)
-
2this is what i use as well. but for a widgets above the listview that requires users to tap on buttons, it does not work.– chitgoksCommented 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
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.
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,
),
),
),
);
}),
);
}
}
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),
),
),
);
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: () {},
)
-
1that'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.'– nosmirckCommented Sep 7, 2018 at 17:20
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");
}
),
),
)
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,
),
),
)
],
)
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:
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"),
),
),
);
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'),
),
),
),
);
}
}
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,
),
),
);
}
}
If you are using CustomScrollView
return RefreshIndicator(
onRefresh: () {},
child: Stack( // <--- use Stack
children: [
CustomScrollView(
physics: AlwaysScrollableScrollPhysics(), // <--- set physics
slivers: [
...
],
),
],
),
);
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.