跪拜 Guibai
← All articles
Frontend · Flutter

A Five-Layer Mixin Stack Eliminates Boilerplate Pull-to-Refresh in Flutter

By SoaringHeart ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Pull-to-refresh is deceptively tedious: page counters, loading guards, footer resets, and empty states multiply across every list screen. A composable mixin stack that bakes in these edge cases means fewer copy-paste bugs and a single place to fix pagination logic when an API changes.

Summary

Pull-to-refresh and pagination logic repeats across nearly every screen in a Flutter app. This pattern collapses that repetition into five layers of Dart mixins and widgets, starting from a bare interface (NRefreshable) and building up to ready-to-use Sliver-based components. List pages get NCustomScrollView with built-in page tracking, load-more, and empty-state handling; detail pages get NCustomScrollViewForModel with skeleton-screen support. External controllers expose state manipulation without touching the widget tree.

The design separates concerns vertically: interface contracts define what data a page holds, mixin implementations handle the EasyRefresh controller and request lifecycle, State mixins wire in setState and disposal, and final widget classes provide the CustomScrollView shell. A developer only supplies an onRequest callback and an itemBuilder.

SoaringHeart refined the approach across multiple production apps and two major rewrites. The result is a set of components that auto-trigger the first refresh, guard against concurrent requests, and reset footer state on refresh — details that otherwise accumulate bugs when copied between screens.

Takeaways
— Five abstraction layers separate the refresh contract, data model, request logic, State lifecycle, and widget shell.
— NCustomScrollView handles list pagination with automatic first-page fetch, concurrent-request guards, and no-more-data footer state.
— NCustomScrollViewForModel wraps single-model detail pages with pull-to-refresh and an optional skeleton screen for the initial load.
— External controller classes (NListRefreshController, NRefreshController) let parent widgets trigger refresh or mutate page state without coupling to the widget internals.
— Sliver-based components integrate with NestedScrollView for sticky headers, while non-Sliver variants exist for simpler layouts.
— The mixin design uses `implements` to enforce the contract and `on` to restrict composition, preventing misuse at compile time.
Conclusions

Vertical layering of mixins — interface, logic, State, widget — mirrors the OSI model and works well for Flutter because it keeps each concern testable and replaceable independently.

Baking the first-refresh trigger into initState via addPostFrameCallback avoids the common mistake of firing network requests before the widget is mounted, a subtle source of exceptions.

Exposing a controller with attach/detach is a pragmatic alternative to passing GlobalKeys around; it decouples imperative actions from the widget tree without introducing a full state-management library.

The `firstPageItems` preset mechanism solves a real-world UX pattern where a dialog or bottom sheet needs to show cached data immediately, then refresh — a detail most generic refresh wrappers ignore.

Concepts & terms
EasyRefresh
A Flutter package that provides pull-to-refresh and load-more widgets with customizable headers, footers, and controller-based finish callbacks.
Sliver
A Flutter layout protocol for scrollable areas; Sliver-based widgets (SliverList, SliverToBoxAdapter) compose inside CustomScrollView and enable advanced scrolling effects like sticky headers.
Mixin
A Dart language feature that lets classes reuse code from multiple sources without traditional inheritance; mixins can declare `on` constraints to restrict which classes they can be applied to.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗