Newby Coder header banner

Flutter Swipe Refresh

Pull to refresh Android/iOS application in Flutter

A refresh on swipe or pull to refresh application activity displays a progress indicator when a user pulls a view typically to indicate that new content is attempted to be loaded

pull_to_refresh package provides SmartRefresher widget to implement pull-to-refresh functionality

It supports pulling up and pulling down refresh functionalities


Creating new Flutter App

Check Flutter installation to setup Flutter

Use flutter create command to create a Flutter project (here swipe_refresh_app :

flutter create swipe_refresh_app

Dependency

Implementation

Import
import 'package:pull_to_refresh/pull_to_refresh.dart'; 

A SmartRefresher widget is used to contain a ListView or GridView which is to be updated as its child property

Properties enablePullDown and enablePullUp determines whether the view can be pulled up and/or down

Pulling down invokes onRefresh method and pulling up invokes onLoad method of SmartRefresher which are set in its declaration

Whether any content is added, or whether content is added to top or bottom of previous content is based on the modification done to child

A RefreshController is assigned to controller property, which is used to call refreshController.refreshCompleted()or refreshController.loadCompleted() so that progress indicator stops being displayed

  List<String> items = ["1", "2", "3", "4", "5", "6", "7", "8"];
  RefreshController _refreshController =
      RefreshController(initialRefresh: false);

  void _onRefresh() async{
    // monitor network fetch
    await Future.delayed(Duration(milliseconds: 1000));
    // if failed,use refreshFailed()
    _refreshController.refreshCompleted();
  }

  void _onLoading() async{
    // monitor network fetch
    await Future.delayed(Duration(milliseconds: 1000));
    // if failed,use loadFailed(),if no data return,use LoadNodata()
    items.add((items.length+1).toString());
    if(mounted)
    setState(() {

    });
    _refreshController.loadComplete();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SmartRefresher(
        enablePullDown: true,
        enablePullUp: true,
        header: WaterDropHeader(),
        footer: CustomFooter(
          builder: (BuildContext context, LoadStatus mode){
            Widget body ;
            if(mode==LoadStatus.idle){
              body =  Text("pull up load");
            }
            else if(mode==LoadStatus.loading){
              body =  CupertinoActivityIndicator();
            }
            else if(mode == LoadStatus.failed){
              body = Text("Load Failed!Click retry!");
            }
            else if(mode == LoadStatus.canLoading){
                body = Text("release to load more");
            }
            else{
              body = Text("No more Data");
            }
            return Container(
              height: 55.0,
              child: Center(child:body),
            );
          },
        ),
        controller: _refreshController,
        onRefresh: _onRefresh,
        onLoading: _onLoading,
        child: ListView.builder(
          itemBuilder: (c, i) => Card(child: Center(child: Text(items[i]))),
          itemExtent: 100.0,
          itemCount: items.length,
        ),
      ),
    );
}

The global configuration RefreshConfiguration, which configures all Smart Refresher representations under a subtree, is generally stored at the root of MaterialApp and is similar in usage to ScrollConfiguration

In addition, if a SmartRefresher behaves differently, then RefreshConfiguration.copyAncestor() can be used to copy attributes from ancestor RefreshConfiguration

// Smart Refresher under the global configuration subtree, here are a few particularly important attributes
 RefreshConfiguration(
     headerBuilder: () => WaterDropHeader(),        // Configure default header indicator
     footerBuilder:  () => ClassicFooter(),        // Configure default bottom indicator
     headerTriggerDistance: 80.0,        // header trigger refresh trigger distance
     springDescription:SpringDescription(stiffness: 170, damping: 16, mass: 1.9),         // custom spring back animate
     maxOverScrollExtent :100, // maximum dragging range of the head
     maxUnderScrollExtent:0, // Maximum dragging range at the bottom
     enableScrollWhenRefreshCompleted: true, //This property is incompatible with PageView and TabBarView| should be set to true to slide left and right on a TabBarView
     enableLoadingWhenFailed : true, //In the case of load failure, users can still trigger more loads by gesture pull-up
     hideFooterWhenNotFull: false, // Disable pull-up to load more functionality when Viewport is less than one screen
     enableBallisticLoad: true, // trigger load more by BallisticScrollActivity
    child: MaterialApp(
        ...
    )
);

App Code

An example widget (LoadOrRefresh) which can be pulled up or down to add content (TextCard)

searchable_dropdown_app/lib/load_or_refresh.dart
import 'basic.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart' hide RefreshIndicator;


class LoadOrRefresh extends StatefulWidget {
  LoadOrRefresh({Key key}) : super(key: key);

  @override
  LoadOrRefreshState createState() => LoadOrRefreshState();
}

class LoadOrRefreshState extends State<LoadOrRefresh> {
  RefreshController _refreshController;
  List<Widget> data = [];
  int counter = 0;

  void _getInitialData() {
    for (int i = 0; i < 4; i++, counter++) {
      data.add(Item.getTextCard('Initial Item $i'));
    }
  }

  @override
  void initState() {
    _getInitialData();
    _refreshController = RefreshController();
    super.initState();
  }


  @override
  Widget build(BuildContext context) {
    return NestedScrollView(
      headerSliverBuilder: (c, s) => [
        SliverAppBar(
          backgroundColor: Colors.greenAccent,
          expandedHeight: 200.0,
          pinned: false,
          flexibleSpace: FlexibleSpaceBar(
            centerTitle: true,
            background: Image.network(
              "https://images.unsplash.com/photo-1541701494587-cb58502866ab?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=0c21b1ac3066ae4d354a3b2e0064c8be&auto=format&fit=crop&w=500&q=60",
              fit: BoxFit.cover,
            ),
          ),
        ),
      ],
      body: Container(
        child: SmartRefresher(
          controller: _refreshController,
          enablePullDown: true,
          header: WaterDropHeader(),
          enablePullUp: true,
          onRefresh: () {
            Future.delayed(const Duration(milliseconds: 2009)).then((val) {
              counter ++;
              data.insert(0, Item.getColouredTextCard('Item $counter, added on refresh', Colors.indigo));
              if (mounted) {
                setState(() {
                  _refreshController.refreshCompleted();
                });
              }
            });
          },
          onLoading: () {
            Future.delayed(const Duration(milliseconds: 2009)).then((val) {
              if (mounted) {
                setState(() {
                  counter ++;
                  data.add(Item.getColouredTextCard('Item $counter, added on loading', Colors.green));
                  _refreshController.loadComplete();
                });
              }
            });
          },
          child: ListView.builder(
            itemExtent: 100.0,
            itemCount: data.length,
            itemBuilder: (context, index) => data[index],
          )
        ),
      )
    );
  }
}

Two example widgets one of which (RefreshListEg) adds content when pulled down and LoadGridEg renders a GridView which adds content when pulled up

searchable_dropdown_app/lib/load_or_refresh.dart
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';


class RefreshListEg extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _RefreshListEgState();
  }
}

class _RefreshListEgState extends State<RefreshListEg> {
  RefreshController _refreshController = RefreshController(initialRefresh: false);
  int counter = 0;
  List<String> data = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];

  Widget buildCtn() {
    return ListView.separated(
      padding: EdgeInsets.only(left: 5, right: 5),
      itemBuilder: (c, i) =>Item.getTextCard('${data[data.length -1 -i]}'),
      separatorBuilder: (context, index) {
        return Container(
          height: 0.5,
          color: Colors.greenAccent,
        );
      },
      itemCount: data.length,
    );
  }

  @override
  Widget build(BuildContext context) {
    return SmartRefresher(
      controller: _refreshController,
      enablePullUp: true,
      child: buildCtn(),
      footer: ClassicFooter(
        loadStyle: LoadStyle.ShowWhenLoading,
        completeDuration: Duration(milliseconds: 500),
      ),
      header: WaterDropHeader(),
      onRefresh: () async {
        //sleep to simulate delay due to network call/work done
        await Future.delayed(Duration(milliseconds: 1000));

        for (int i = 0; i < 5; i++, counter++) {
          data.add("Item $counter");
        }

        if (mounted) setState(() {});
        _refreshController.refreshCompleted();
      },
      onLoading: () async {
        // monitor fetch data from network
        await Future.delayed(Duration(milliseconds: 1000));
        if (mounted) setState(() {});
        _refreshController.loadFailed();
      },
    );
  }
}

//only GridView
class LoadGridEg extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _LoadGridEgState();
  }
}

class _LoadGridEgState extends State<LoadGridEg> {
  int counter = 0;
  RefreshController _refreshController =
      RefreshController(initialRefresh: false);
  List<dynamic> data = [];

  @override
  void initState(){
    for(int i=0;i<4;i++){
      data.add(Item.getTextCard("${i}"));
    }
    super.initState();
  }

  Widget buildCtn() {
    return GridView.builder(
      physics: ClampingScrollPhysics(),
      gridDelegate:
          SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
      itemBuilder: (c, i) => data[i],
      itemCount: data.length,
    );
  }

  @override
  Widget build(BuildContext context) {
    return SmartRefresher(
      controller: _refreshController,
      enablePullUp: true,
      child: buildCtn(),
      header: WaterDropHeader(),
      onRefresh: () async {
        //sleep to simulate delay due to network call/work done
        await Future.delayed(Duration(milliseconds: 1000));

        if (mounted) setState(() {});
        _refreshController.refreshCompleted();
      },
      onLoading: () async {
        //sleep to simulate delay due to network call/work done
        await Future.delayed(Duration(milliseconds: 1000));
        for (int i = 0; i < 10; i++, counter++) {
          data.add(Item.getImageCard('https://picsum.photos/200/300?random=${counter}'));

        }
        if (mounted) setState(() {});
        _refreshController.loadComplete();
      },
    );
  }
}

class Item {
  static Widget getTextCard(text) {
    return Container(
      child: Card(
        margin: EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 5.0),
        child: Center(
          child: Text(text),
        ),
      ),
      height: 100.0,
    );
  }
  static Widget getColouredTextCard(text, color) {
    return Container(
      child: Card(
        color: color,
        margin: EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 5.0),
        child: Center(
          child: Text(text),
        ),
      ),
      height: 100.0,
    );
  }
  static Widget getImageCard(source) {
    return Container(
      child: Card(
        child: Image.network(source,
          fit: BoxFit.cover
        ),
      ),
      height: 100.0,
    );
  }
}

main.dart contains RefreshConfiguration for app and a widget with tabs to navigate to widgets mentioned above

searchable_dropdown_app/lib/main.dart
import 'basic.dart';
import 'load_or_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';


void main() => runApp(SwipeToRefreshApp());

class SwipeToRefreshApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RefreshConfiguration(
      footerTriggerDistance: 15,
      dragSpeedRatio: 0.91,
      headerBuilder: () => MaterialClassicHeader(),
      footerBuilder: () => ClassicFooter(),
      enableLoadingWhenNoData: false,
      shouldFooterFollowWhenNotFull: (state) {
        return false;
      },
      autoLoad: true,
      child: MaterialApp(
        title: 'Pull-to-refresh Example App',
        theme: ThemeData(
            primarySwatch: Colors.blue,
            primaryColor: Colors.greenAccent),
        home: BasicExample()
      ),
    );
  }
}

class BasicExample extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _BasicExampleState();
  }
}

class _BasicExampleState extends State<BasicExample> with SingleTickerProviderStateMixin {
  TabController _tabController;
  String appBarBottomText;

  @override
  void initState() {
    _tabController = TabController(length: 3, vsync: this);
    _tabController.addListener(setAppBarBottomText);
    _tabController.notifyListeners();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshConfiguration.copyAncestor(
      enableLoadingWhenFailed: true,
      context: context,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Swipe Refresh Example App"),
          bottom: AppBar(
            backgroundColor: Colors.blueAccent,
            title : Text(" ${appBarBottomText}",
              style: TextStyle(fontSize:14, fontWeight:FontWeight.normal)
            ),
          ),
        ),
        body: TabBarView(
          physics: NeverScrollableScrollPhysics(),
          controller: _tabController,
          children: <Widget>[
            Scrollbar(
              child: RefreshListEg(),
            ),
            Scrollbar(
              child: LoadGridEg(),
            ),
            Scrollbar(
              child: LoadOrRefresh(),
            )
          ],
        ),
        bottomNavigationBar: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              color: Colors.greenAccent,
              alignment: Alignment.center,
              child: TabBar(
                isScrollable: true,
                controller: _tabController,
                tabs: <Widget>[
                  Tab(
                    text: "Swipe down",
                  ),
                  Tab(
                    text: "Swipe up",
                  ),
                  Tab(
                    text: "Swipe up/down",
                  )
                ],
              ),
            ),
          ],
        ),
      ),
      headerBuilder: () => WaterDropMaterialHeader(
        backgroundColor: Theme.of(context).primaryColor,
      ),
    );
  }

  void setAppBarBottomText() {
    switch(_tabController.index) {
      case 1:
        appBarBottomText = "Loading (Pull up) enabled, Refresh configured to not add items";
        break;
      case 0:
        appBarBottomText = "Refresh (Pull down) enabled, Loading configured to fail";
        break;
      default:
        appBarBottomText = "Pull up or down to load ";
    }
    setState( () {} );
  }
}

Run instructions

Ensure a supported device is connected or emulator/simulator is started

Go to project directory

Use flutter run command to run

flutter run

It builds and runs app on an available android/ios device


Screenshot/image

Android

cl-flutter-swipe-refresh

iOS

cm_flutter_swipe_refresh_as1cm_flutter_swipe_refresh_as2cm_flutter_swipe_refresh_as3
cm_flutter_swipe_refresh_as4cm_flutter_swipe_refresh_as6cm_flutter_swipe_refresh_as7