Flutter - Pull To Refresh Using RefreshIndicator and CupertinoSliverRefreshControl

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:

Flutter - RefreshIndicator

 

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:

Flutter - RefreshIndicator - Custom

 

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 to defaultScrollNotificationPredicate.
  • 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 while onRefresh 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:

Flutter - CupertinoSliverRefreshControl

 

That's how to build a page supporting pull-to-refresh in Flutter which works in Android and iOS.