# flutter-custom-refresh-indicator: build your own pull-to-refresh widget in Flutter

> This package gives you two ways to replace Flutter's built-in RefreshIndicator: a wrapper for the material indicator whose content you swap, and a fully custom widget where you draw the whole thing. It suits a designer who wants a distinctive gesture, and its last stable release was in mid-2024.

**gonuit/flutter-custom-refresh-indicator** — Widget that makes it easy to implement a custom pull to refresh gesture.

- Repository: https://github.com/gonuit/flutter-custom-refresh-indicator
- Stars: 607 · Forks: 82
- Language: Dart
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/gonuit-flutter-custom-refresh-indicator

## Two widgets, and the choice between them is the design decision

The package description is plain: a widget that makes it easy to implement a custom pull to refresh gesture. What the readme adds is the headline that you can create your own custom refresh indicator widget in the blink of an eye, plus a link to an online demo and a promise of completely custom indicator widgets.

There are two entry points, and the difference between them is the whole API.

CustomMaterialIndicator is the smaller commitment. It builds a material container and, in addition to the built in RefreshIndicator it supports, it lets you replace the content of the indicator. The example in the readme shows the shape:

```dart
CustomMaterialIndicator(
  onRefresh: onRefresh, // Your refresh logic
  backgroundColor: Colors.white,
  indicatorBuilder: (context, controller) {
    return Padding(
      padding: const EdgeInsets.all(6.0),
      child: CircularProgressIndicator(
        color: Colors.redAccent,
        value: controller.state.isLoading ? null : math.min(controller.value, 1.0),
      ),
    );
  },
  child: child,
)
```

CustomRefreshIndicator is the larger one. You wrap your scrollable list and design the whole indicator:

```dart
CustomRefreshIndicator(
  onRefresh: onRefresh, // Your refresh logic
  builder: (context, child, controller) {
    // Place your custom indicator here.
    // Need inspiration? Look at the example app!
    return MyIndicator(
      child: child,
      controller: controller,
    );
  },
  child: ListView.builder(
    itemBuilder: (_, index) => Text('Item $index'),
  ),
)
```

The second is the interesting one, because the builder receives the child and the controller, so the indicator can wrap, transform or overlay the scrolling content rather than merely sit above it. That is what makes effects like a scaling envelope possible at all.

## The controller is the contract between your gesture and your pixels

Both builders hand you a controller, and the readme's parameter list shows what it is: the controller manages the state and interaction of the refresh indicator, and it is an optional parameter of the widget itself.

The example shows the state being consulted inside the builder, with controller.state.isLoading deciding between a determinate and an indeterminate progress indicator, and controller.value giving the drag fraction, clamped with math.min against 1.0. That is the pattern in one line: the controller exposes both the phase of the gesture and how far through it you are, and your widget decides what those numbers look like.

Because the controller is a parameter, the interaction can also be driven from outside the widget, and one of the example gallery entries is described as programmatically controlled, with a source file and a demo link. That is a materially different capability from a stock RefreshIndicator, where the only handle you have is the onRefresh callback and a fixed visual.

The documentation section lists the pieces you would need to read before building: the CustomRefreshIndicator parameters, the indicator states, handling state changes, the trigger modes, the IndicatorController properties, and an IndicatorController showcase. The parameters are grouped, and the first group is the basic one: the child, which is the content of the scroll view that will be pulled down to trigger the refresh, the builder function returning the widget used as the refresh indicator, the onRefresh callback, which should return a Future, and the optional controller.

The timing group is where the polish lives, with a RefreshIndicatorDurations object holding a cancel duration for hiding the indicator after canceling, a settle duration for the indicator to settle after release, and a finalize duration that the visible excerpt cuts off. These are the values that decide whether a refresh feels instant or sluggish, and they are the numbers a designer will want to tune first.

## Triggering from either edge, and horizontal lists

Two capabilities stand out from the framework default, and both come from the same underlying design.

The first is the trigger argument. The feature list says you can trigger the refresh indicator from the leading, trailing, or both scroll edges, and the CustomMaterialIndicator description repeats that it supports horizontal lists and triggering from both edges, pointing at the trigger argument. A standard RefreshIndicator only responds to a pull down at the top of a vertical list. With this package you can decide that a swipe from the right refreshes, or that either edge does, which is what a chat-style interface or a horizontally paged carousel needs.

The second is the horizontal list support, which is described in the features as triggering the refresh indicator on horizontal lists, with an online example linked for that case.

These are not separate features so much as one change in the gesture model. If the refresh is bound to an edge rather than to the top of a vertical scroll, then a horizontal axis is just another axis, and the same code path handles it. That is a small, clean idea, and it is the reason the package is more than a styling exercise.

The other things in the feature list are more conventional. Quickly change the content of the material refresh indicator is the CustomMaterialIndicator path, and creating a completely custom refresh indicator widget is the CustomRefreshIndicator path.

## The gallery shows what is possible, and the README shows how it is done

The readme leans on examples rather than prose, and the table of examples is the best documentation in the repository.

The gallery covers a plane indicator, an ice cream indicator and a warp indicator, then a complete state indicator, a pull to fetch more indicator and an envelope indicator, then a programmatically controlled indicator and one based on drag details. Each entry links both a source file under example/lib/indicators/ and a demo page, and the readme notes that all of these are available in the example application.

That structure is worth pausing on. A library for custom indicators is only as good as the examples people can read, and here every effect ships with its source in the repository rather than only as a video. The readme's own invitation to contributors is in that same table, where a slot reads Have you created a fancy refresh indicator? This place is for you, next to an open pull request link.

The two headings under the gallery are also telling. A complete state indicator means the widget can render a finished, non-spinning state such as a check mark, and a pull to fetch more indicator means the same widget can be reused for the infinite scroll pattern rather than only for a manual refresh. Those are two different products that share one implementation, and seeing them side by side in the table is more informative than a list of parameter names.

For a designer evaluating the package, the practical advice is to open the demo links, find the effect closest to what you want, and read that example's source file.

## Project state, and the gap between the last release and the last push

The maintenance facts deserve a plain reading.

The repository is not archived, and the last push was on 2026-08-28. The recent releases are v4.1.0-dev.1 from 2026-08-25, then v4.0.1 and v4.0.0 both from 2024-07-18. So the last stable release is more than two years old, and the work since then has been building towards 4.1.0, which is published as a development build rather than a stable tag.

The badge on the readme points at a tests workflow, and the repository has the expected shape for a Flutter package: lib/ for the widget code, example/ with per-platform folders for android, ios, linux, macos, web and windows, test/ for the test suite, a pubspec.yaml, an analysis_options.yaml for the linter configuration, a CHANGELOG.md and a readme/ directory holding the images the readme references.

The licence is MIT, and the package is a normal pub.dev package, so a dependency is declared in the usual way in a pubspec.yaml. The readme does not spell out the pubspec dependency snippet, so the practical step for an adopter is to take the package name and version from the pub.dev listing and add it to their own pubspec, and the version to note is 4.0.1, the last stable one, unless the 4.1.0 dev build has since been published as stable.

The 4.1.0 development tag is the one thing to keep in mind when pinning.

## When this package is the wrong answer

There are three cases where the package is more than you need, and one where it is not enough.

The first is the simplest case. If you want a different spinner in the standard pull to refresh, CustomMaterialIndicator with a different indicatorBuilder is a few dozen characters of change, and pulling in a package for it is optional. The package is really for the second case, where the indicator is a designed moment rather than a spinner.

The second is when the platform's own affordance is enough. On Android the pull to refresh is a platform convention with system animation, and on iOS the equivalent. Replacing it with a custom animation is a deliberate break from the platform look, and a product decision rather than a technical one.

The third is when the app has no scrollable list to refresh. The whole widget is built around a child that is pulled, and the readme does not describe a programmatic trigger on a widget that has no scroll view, although the presence of the controller parameter and the programmatically controlled example suggest some headroom there.

And the case where the package is not enough is the honest one: this is a gesture and a visual, and it does not do the refreshing. The onRefresh callback is yours, and the readme is explicit about it returning a Future, so the data fetching, the pagination, the error handling and the deduplication of results are all still your code. The package removes the need to reimplement the gesture physics and the state machine, which is a real saving, and it removes nothing else.

## Conclusion

The package fits a Flutter team that wants a branded pull-to-refresh gesture rather than the stock spinner, and it does not fit a project that just needs to swap the icon inside the standard indicator, where a few lines of CustomMaterialIndicator will do. Before depending on it, read the indicator states and controller documentation rather than only the examples, and check whether the 4.1.0 dev build you are considering is a stable enough base for a shipped app.

## FAQ

### What is flutter-custom-refresh-indicator?

It is a MIT licensed Flutter package for building a custom pull to refresh gesture, published by gonuit. It offers two widgets, CustomMaterialIndicator for swapping the content of the material indicator, and CustomRefreshIndicator for drawing the whole indicator yourself through a builder function.

### How do I use the IndicatorController in a custom indicator?

The builder function receives a controller that manages the state and interaction of the indicator, and the readme's example uses controller.state.isLoading to switch between determinate and indeterminate progress, and controller.value clamped with math.min against 1.0 for the drag fraction. The controller can also be passed to the widget itself for programmatic control.

### Can flutter-custom-refresh-indicator work with horizontal lists?

Yes. The feature list calls out triggering the refresh indicator on horizontal lists, with an online example for that case, and the trigger argument can be the leading, the trailing or both scroll edges, rather than only a pull down at the top of a vertical list.

### What is the latest stable version of flutter-custom-refresh-indicator?

The recent releases are v4.1.0-dev.1 from 2026-08-25 and v4.0.1 and v4.0.0 from 2024-07-18, so the last stable release is 4.0.1 and the repository's last push was on 2026-08-28. The repository is not archived.

### Does flutter-custom-refresh-indicator handle the data fetching too?

No. The package provides the gesture and the indicator widget, and the onRefresh callback is yours and should return a Future, so the data loading, pagination and error handling remain in your code.

## Sources

- [gonuit/flutter-custom-refresh-indicator on GitHub](https://github.com/gonuit/flutter-custom-refresh-indicator)
- [Issues](https://github.com/gonuit/flutter-custom-refresh-indicator/issues)
- [License: MIT](https://github.com/gonuit/flutter-custom-refresh-indicator/blob/master/LICENSE)
- [README](https://github.com/gonuit/flutter-custom-refresh-indicator/blob/master/README.md)
- [Releases](https://github.com/gonuit/flutter-custom-refresh-indicator/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gonuit-flutter-custom-refresh-indicator
