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
Check Flutter installation to setup Flutter
Use flutter create
command to create a Flutter project (here swipe_refresh_app :
flutter create swipe_refresh_app
Add pull_to_refresh
package to pubspec.yaml
dependencies:
pull_to_refresh: ^1.5.7
http: "^0.12.1"
flutter:
sdk: flutter
Run following command to add dependency
flutter pub get
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(
...
)
);
An example widget (LoadOrRefresh) which can be pulled up or down to add content (TextCard)
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
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
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( () {} );
}
}
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