flutter_easy_refresh: pull-down refresh and pull-up load for any Flutter scrollable
A flutter widget that provides pull-down refresh and pull-up load.
At a glance
- What is it?
- EasyRefresh wraps almost any Flutter scrollable with refresh and load-more behaviour, ships several indicator styles, and exposes a controller for programmatic triggering. Here is how it installs, how the scope and locator mechanics work, and where it stops being the right choice.
- Who is it for?
- Adopt flutter_easy_refresh if you need refresh and load-more on scrollables that RefreshIndicator or a single-widget pull_to_refresh cannot cover, or if you want to trigger refresh and load from code. Do not adopt it if you only need a Material refresh on a plain ListView, or if you cannot accept a package that depends on Flutter's scroll physics contract.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 10 days ago.
- What is it written in?
- Mainly Dart, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What flutter_easy_refresh solves, and who it is for
Flutter's built-in RefreshIndicator covers the common case: one vertical list, pull down, spinner, done. It does not cover load-more, it does not cover horizontal or custom scrollables, and it gives you no way to start a refresh from code. EasyRefresh targets that gap. The README states it supports almost all Flutter Scrollable widgets, and that its function is similar to Android's SmartRefreshLayout. That comparison is the clearest statement of intent: this is a general refresh framework for Flutter, not a single widget.
The audience is Flutter app developers building list-heavy screens. Feed pages, search results, chat histories, any screen where the user pulls for new data and scrolls to the bottom for older data. If your screen is a plain ListView with a Material spinner and nothing else, the built-in indicator is smaller and has no third-party dependency. EasyRefresh earns its place when you need a custom indicator animation, a specific indicator position, or a controller that drives refresh and load from application logic rather than from a gesture.
The scope mechanism: one physics shared across the child tree
The default constructor is the part worth understanding before you write any code. The README notes that in the child scope, all scrolling components share one physics, and that if there is scroll nesting you should use EasyRefresh.builder or set the scope with ScrollConfiguration. That is the core design: EasyRefresh does not wrap your list in a gesture detector and hope. It supplies a ScrollPhysics to the descendants, and the scrollables report their overscroll through it.
This explains why the builder constructor exists. In EasyRefresh.builder, childBuilder receives a physics object and you are expected to pass it to your scrollable explicitly. The nested-scroll sample shows the same physics handed to both NestedScrollView and the inner ListView. If you forget one of them, that scrollable simply will not participate, and the symptom is a refresh that never fires rather than an exception.
Indicator position is a separate axis. By default the header and footer sit at the edges of the scroll view. Setting position to IndicatorPosition.locator, then placing HeaderLocator.sliver and FooterLocator.sliver inside a CustomScrollView's slivers list, moves the indicator into the sliver stream itself. The README describes this as supporting indicator position setting, and adds that combined with listeners the indicator can be placed in any position. The NestedScrollView sample passes clearExtent: false to HeaderLocator.sliver, which suggests the locator reserves layout extent by default and that flag opts out. The README does not spell out what clearExtent does beyond that example.
Installing easy_refresh and wiring a first refresh and load
The package is published on pub.dev as easy_refresh, and the README links the API reference there. The README shows a pub.dev badge but no version constraint, so pin the version yourself rather than copying a number from an article. The README's companion-package section gives the import lines you need once the dependency is in place.
import 'package:easy_paging/easy_paging.dart';
import 'package:easy_refresh/easy_refresh.dart';A minimal screen uses the default constructor. Both callbacks are async, and both may return an IndicatorResult to tell the indicator how to end.
EasyRefresh(
onRefresh: () async {
....
},
onLoad: () async {
....
return IndicatorResult.noMore;
},
child: ListView(),
);After this runs, pulling down on the list triggers onRefresh and pulling up past the end triggers onLoad. Returning IndicatorResult.noMore from onLoad is how the footer learns there is nothing left to fetch; the README uses exactly this in both its default-constructor and controller samples.
If you want to drive refresh from a button or on first frame, use the controller. The README's example constructs it with both controlFinishRefresh and controlFinishLoad set to true, which means the callbacks must end the state themselves.
EasyRefreshController _controller = EasyRefreshController(
controlFinishRefresh: true,
controlFinishLoad: true,
);
EasyRefresh(
controller: _controller,
onRefresh: () async {
....
_controller.finishRefresh();
_controller.resetFooter();
},
onLoad: () async {
....
_controller.finishLoad(IndicatorResult.noMore);
},
);
_controller.callRefresh();
_controller.callLoad();Note the asymmetry in that sample: onRefresh calls finishRefresh then resetFooter, while onLoad calls finishLoad with a result. resetFooter appears in the README only in this context, and the README does not document what happens if you omit it. Treat that pairing as required until you confirm otherwise against the example app.
Custom headers, footers and the style packages
EasyRefresh ships header and footer widgets you can pass per instance, or set globally. The README shows MaterialHeader and MaterialFooter on a single EasyRefresh, and then assigns EasyRefresh.defaultHeaderBuilder and EasyRefresh.defaultFooterBuilder to ClassicHeader and ClassicFooter for a global default. That global hook is the practical way to keep one visual language across an app without repeating constructor arguments on every screen.
Beyond the built-ins, the README lists six companion style packages: easy_refresh_bubbles, easy_refresh_bow, easy_refresh_halloween, easy_refresh_skating, easy_refresh_space and easy_refresh_squats. Each is a separate pub.dev package. That split is a deliberate trade-off: installing the core package does not pull in six animation sets you will never use, but it also means a custom indicator is a second dependency and a second version to track against the core package's header and footer interfaces.
There is also a separate easy_paging package for pagination helpers, imported alongside easy_refresh and demonstrated in example/lib/page/sample/paging_page.dart. The README points to that file as the sample implementation, so it is the place to look when the controller API alone is not enough structure for your data layer.
Where EasyRefresh is the wrong tool
The physics-sharing model is the source of its main failure mode. Because EasyRefresh relies on scrollables accepting the physics it provides, any scrollable that ignores or overrides that physics is outside its reach. The README's own guidance to use the builder constructor or ScrollConfiguration when nesting scroll views is an admission that the default constructor does not handle every tree. If your screen mixes a PageView, a TabBarView and an inner list, expect to debug which physics object reaches which scrollable, and expect the failure to be silent rather than loud.
The second limitation is scope. This is a refresh and load-more framework. It does not fetch, cache, retry or deduplicate anything. Every callback is yours to implement, and the indicator only knows what your return value tells it. If you were hoping for a data layer, easy_paging is the closest thing the README mentions, and it is described as pagination helpers, not as a networking or caching layer.
Third, the README does not document rollback or error semantics for the controller. If finishRefresh is never called because an exception escapes your callback, the README gives no guidance on what the indicator does. Wrap your callback bodies defensively until you have verified the behaviour yourself.
How it compares with RefreshIndicator and pull_to_refresh
Flutter's own RefreshIndicator is the baseline. It is one widget, it assumes a Material spinner, it has no load-more, and it cannot be triggered programmatically. Its advantage is that it is part of the framework: no dependency, no version drift, no physics contract to satisfy beyond what the framework already enforces. If your screen is a single vertical ListView with a standard spinner, RefreshIndicator is the smaller answer and the README of EasyRefresh does not claim otherwise.
The pull_to_refresh family takes a different approach: it is built around a notifier and a wrapper widget per scrollable, with the refresh state held in an object you attach to the list. EasyRefresh instead pushes a physics object down the tree and keeps state in the controller and the indicator widgets. The practical difference shows up in nesting. With a per-widget wrapper you attach each scrollable individually and the wiring is explicit; with the physics-scope model you attach once and every descendant that accepts the physics participates, which is less code in the common case and more confusing when a descendant does not cooperate. Neither approach dominates; the choice depends on whether your tree is flat or nested.
Maintenance, licence and upgrade cost
The repository is not archived. The last push was on 2026-09-20, three days before this writing, so the project is being touched regularly. The release history is more uneven: v3.5.0 was published on 2026-03-23, and the release before it, 3.4.0, dates from 2024-05-14. That is roughly a twenty-two month gap between 3.4.0 and 3.5.0, with 3.3.5+1 in April 2024. A reader planning a dependency should weigh that: commits land, releases do not always follow quickly.
The licence is MIT, stated in the README badge and present as a LICENSE file at the repository root. MIT is permissive and imposes no copyleft obligation on your application, but this is a description of the licence text, not legal advice; read the LICENSE file and your own organisation's policy before shipping.
Upgrade cost concentrates in two places. The controller API, because controlFinishRefresh and controlFinishLoad change who owns the indicator state, and the header and footer interfaces, because the style packages in the README are versioned separately and must track the core package. The major version is 3, and the default branch is named v3, so the v2 to v3 transition is the one most likely to appear in a migration guide; the README here is written for v3 and does not cover v2.
Editorial conclusion
Adopt flutter_easy_refresh if you need refresh and load-more on scrollables that RefreshIndicator or a single-widget pull_to_refresh cannot cover, or if you want to trigger refresh and load from code. Do not adopt it if you only need a Material refresh on a plain ListView, or if you cannot accept a package that depends on Flutter's scroll physics contract. Before committing, verify the physics wiring on your nesting pattern against the example app, and confirm the controller's finishRefresh, resetFooter and finishLoad behaviour matches how your state layer reports success and noMore.
Frequently asked questions
How do I refresh the screen in Flutter with flutter_easy_refresh?
Wrap the scrollable in an EasyRefresh widget and supply an async onRefresh callback. Pulling down on the list then triggers that callback, and you can end the state by returning an IndicatorResult or by calling finishRefresh on an EasyRefreshController.
Does flutter_easy_refresh work with any Flutter scrollable widget?
The README states it supports almost all Flutter Scrollable widgets. In the default constructor all scrolling components in the child scope share one physics, and the README advises using EasyRefresh.builder or ScrollConfiguration when scroll views are nested.
Can flutter_easy_refresh trigger a refresh or a load without a user gesture?
Yes. The README shows an EasyRefreshController with controlFinishRefresh and controlFinishLoad set to true, and the controller exposes callRefresh and callLoad for triggering each action from code.
How does flutter_easy_refresh know there is no more data to load?
The onLoad callback returns an IndicatorResult. The README's controller sample returns IndicatorResult.noMore from onLoad and passes the same value to finishLoad, which is how the footer learns the list is exhausted.
What licence does flutter_easy_refresh use?
MIT. The README carries an MIT badge and the repository root contains a LICENSE file. That is a description of the licence text, not legal advice.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/xuelongqy-flutter-easy-refresh)