Reputation: 831
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
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
Reputation: 488
Most of the other answers will work but they all have some downsides:
SingleChildScrollView
without adjusting the height will cause the scrolling glow effect to be not at the bottom of the page.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
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
Reputation: 1639
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.
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
Reputation: 5585
If you are using CustomScrollView
return RefreshIndicator(
onRefresh: () {},
child: Stack( // <--- use Stack
children: [
CustomScrollView(
physics: AlwaysScrollableScrollPhysics(), // <--- set physics
slivers: [
...
],
),
],
),
);
Upvotes: 0
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.
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:
Upvotes: 11
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.
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:
Upvotes: 44
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:
Upvotes: 2
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
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
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
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
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
Reputation: 1445
You must do the following:
SingleChildScrollView
with the property physics
set to AlwaysScrollableScrollPhysics()
.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
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