A page that contains dynamic data needs a way to refresh the content. In recent years, one of the most popular and easy-to-use ways is by implementing Material swipe to refresh. The user only needs to scroll the page until it overscrolls at which time a loading progress indicator is displayed. This tutorial gives you examples of how to implement pull-to-refresh in Flutter on both Android and iOS, including how to update the data during refresh and customize the look of the refresh indicator.
For this tutorial, we are going to create a simple list, each contains a random word that will be refreshed every time the list is overscrolled. Below is the initial code for this tutorial which still generates a blank page. Later, we are going to implement _buildList
method to build a widget that contains a ListView
supporting pull-to-refresh. There is also a method named _refreshData
whose task is updating the data by generating a new list of random words. A delay of 3 seconds is added so that you can have enough time to see the loading animation.
import 'dart:io';
import 'package:english_words/english_words.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Woolha.com Flutter Tutorial',
home: _PullToRefreshExample(),
);
}
}
class _PullToRefreshExample extends StatefulWidget {
@override
_PullToRefreshExampleState createState() => _PullToRefreshExampleState();
}
class _PullToRefreshExampleState extends State<_PullToRefreshExample> {
final _data = <WordPair>[];
@override
void initState() {
super.initState();
_data.addAll(generateWordPairs().take(20));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Woolha.com Flutter Tutorial'),
),
body: _buildList(),
);
}
Widget _buildList() {
// TODO build the list with pull-to-refresh
return Container();
}
Widget _buildListItem(String word, BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(word),
),
);
}
Future _refreshData() async {
await Future.delayed(Duration(seconds: 3));
_data.clear();
_data.addAll(generateWordPairs().take(20));
setState(() {});
}
}
Using RefreshIndicator
RefreshIndicator
is a widget in Flutter that supports Material's swipe-to-refresh. It works by showing a circular progress indicator when the child's Scrollable
is overscrolled. If the user ends the scroll and the indicator has been dragged far enough, it will call onRefresh
. You can define your own callback function. Usually the callback contains code to update the data.
Below is the constructor of the widget.
const RefreshIndicator({
Key key,
@required this.child,
this.displacement = 40.0,
@required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = 2.0
})
There are two required parameters. The first is child
(Widget
) which is the content that can be refreshed. The other required parameter is onRefresh
, a callback function to be called when the refresh indicator has been dragged far enough.
In the example below, we use the _refreshData
method defined above to be set as onRefresh
callback. As for the child, we create a simple ListView
.
Widget _buildList() {
return RefreshIndicator(
onRefresh: _refreshData,
child: ListView.builder(
padding: EdgeInsets.all(20.0),
itemBuilder: (context, index) {
WordPair wordPair = _data[index];
return _buildListItem(wordPair.asString, context);
},
itemCount: _data.length,
),
);
}
Output:
Customizing RefreshIndicator
You can do some customizations on the refresh indicator. A named parameter displacement
allows you to pass a double
value to set where the refresh indicator will settle measured from the child's top or bottom edge. It can be used to control how much overscroll is needed to perform the refresh. For the icon's style, you can pass Color
values as color
and backgroundColor
to change the icon's foreground and background colors respectively. The strokeWidth
of the icon which defaults to 2.0 can be customized by passing strokeWidth
parameter. Below is the example of a customized RefreshIndicator
.
RefreshIndicator(
onRefresh: _refreshData,
backgroundColor: Colors.teal,
color: Colors.white,
displacement: 200,
strokeWidth: 5,
child: ListView.builder(
padding: EdgeInsets.all(20.0),
itemBuilder: (context, index) {
WordPair wordPair = _data[index];
return _buildListItem(wordPair.asString, context);
},
itemCount: _data.length,
),
)
Output:
Here's the list of named parameters you can pass to the constructor of RefreshIndicator
.
Key key
: The widget's key.Widget child
*: The widget below this widget in the tree. It contains the content that can be refreshed.double displacement
: Where the refresh indicator will settle measured from the child's top or bottom edge.Future<void> onRefresh
*: A callback function to be called when the refresh indicator has been dragged far enough which means a refresh should be performed.Color color
: The foreground color of the refresh indicator.Color backgroundColor
: The background color of the refresh indicator.bool notificationPredicate
: Specifies whether a [ScrollNotification] should be handled by this widget. Defaults todefaultScrollNotificationPredicate
.String semanticsLabel
:Semantics.label
for the widget.String semanticsValue
:Semantics.value
for the widget.double strokeWidth
: The stroke width of the refresh indicator. Defaults to 2.0.
*: required
Using CupertinoSliverRefreshControl
If you want to implement iOS-style pull-to-refresh, there is a widget called CupertinoSliverRefreshControl
. Usually, you need to use CustomScrollView
. The CustomScrollView
itself allows you to pass sliver widgets as slivers
parameter. So, you can pass a CupertinoSliverRefreshControl
as the first sliver and another sliver containing the data that can be refreshed.
Below is the constructor of CupertinoSliverRefreshControl
.
const CupertinoSliverRefreshControl({
Key key,
this.refreshTriggerPullDistance = _defaultRefreshTriggerPullDistance,
this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent,
this.builder = buildRefreshIndicator,
this.onRefresh,
})
There is no required parameter. However, passing onRefresh
callback is usually necessary. The code below shows you the basic usage of CupertinoSliverRefreshControl
.
Widget _buildList() {
return Padding(
padding: const EdgeInsets.all(20.0),
child: CustomScrollView(
slivers: [
CupertinoSliverRefreshControl(
onRefresh: _refreshData,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
WordPair wordPair = _data[index];
return _buildListItem(wordPair.asString, context);
},
childCount: _data.length
),
),
],
),
);
}
Below is the list of named parameters you can pass to the constructor of CupertinoSliverRefreshControl
.
Key key
: The widget's key.double refreshTriggerPullDistance
: The amount of overscroll to trigger a reload.double refreshIndicatorExtent
: The amount of space whileonRefresh
is running.RefreshControlIndicatorBuilder builder
: A builder that's called when this sliver's size changes or the state changes.Future<void> onRefresh
: A callback function to be called when the refresh indicator has been dragged far enough which means a refresh should be performed.
However, CupertinoSliverRefreshControl
doesn't work in Android by default. A possible solution is using different widgets for Android and iOS. If the platform is iOS, use CupertinoSliverRefreshControl
. If the platform is Android, use RefreshIndicator
.
Widget _buildList() {
return Platform.isIOS ? _buildIOSList() : _buildAndroidList();
}
Widget _buildIOSList() {
return Padding(
padding: const EdgeInsets.all(20.0),
child: CustomScrollView(
slivers: [
CupertinoSliverRefreshControl(
onRefresh: _refreshData,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
WordPair wordPair = _data[index];
return _buildListItem(wordPair.asString, context);
},
childCount: _data.length
),
),
],
),
);
}
Widget _buildAndroidList() {
return RefreshIndicator(
onRefresh: _refreshData,
child: ListView.builder(
padding: EdgeInsets.all(20.0),
itemBuilder: (context, index) {
WordPair wordPair = _data[index];
return _buildListItem(wordPair.asString, context);
},
itemCount: _data.length,
),
);
}
But if you also want to use CupertinoSliverRefreshControl
in Android, you need to set the physics of the CustomScrollView
to use BouncingScrollPyshics
.
CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: [
CupertinoSliverRefreshControl(
onRefresh: _refreshData,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
WordPair wordPair = _data[index];
return _buildListItem(wordPair.asString, context);
},
childCount: _data.length
),
),
],
)
Now the CupertinoSliverRefreshControl
also works in Android.
Output:
That's how to build a page supporting pull-to-refresh in Flutter which works in Android and iOS.