Pull-to-refresh breaks on long lists and slow networks
Pull-to-refresh looks like a trivial gesture, but it breaks exactly where the list is long and the network is slow. The user pulls down, the indicator spins, the data doesn't arrive — and instead of a refresh, they get a frozen indicator and a lost scroll position.
Then they either wait in vain or jerk the gesture again, and at that moment duplicate requests are sent into the system.
The first scenario is janky scrolling on a long list. If the refresh gesture conflicts with nested scrolling or horizontal carousels inside cards, the app intercepts the touch at the wrong time: the list "sticks," and the refresh triggers accidentally.
On a long list this is more noticeable — the finger travels a greater distance, and the trigger threshold gets thrown off.
The second is a slow network. While the response is in flight, the indicator must remain controllable: the user should see that the process is ongoing and be able to interrupt it.
If the response hasn't arrived and no timeout is set, the frozen indicator blocks the interface, and the app looks dead. Worse is when a repeated gesture launches a second request on top of the first — the data arrives interleaved, and the list redraws twice.
The third is loss of position. After a refresh, the list is often rebuilt from scratch, and a user who was reading the middle ends up at the top. On a long list this is the most irritating: they lose context and don't understand what actually changed.
The fourth is duplicate requests during rapid repeated gestures. Without blocking parallel calls and deduplicating responses, the app sends several identical requests, loads the network, and gets a race condition during rendering.
All four failures aren't about the gesture itself, but about how it's connected to screen state, timeouts, and the request queue. Let's break down how this is solved at the level of the list architecture.
What a correct implementation of gesture-based refresh delivers
A correct pull-to-refresh is not a "spinner that disappears" but a predictable contract between the gesture, the network, and the list.
The user pulls down and expects to see fresh data within a time that feels instantaneous: the indicator appears immediately, not after the server responds, otherwise the interface seems frozen and the person pulls again.
When the gesture is handled correctly, the share of successful refreshes grows, and duplicate requests — the very ones that go "into the void" out of impatience — are reduced severalfold.
The interface response is built on separating two things: the visual reaction to the gesture and the actual data loading. The indicator should render in the very first frame, and the request should go out once, without duplicates on rapid repeated pulls.
If this is not done, every extra gesture turns into a separate request to the server, and under load it is not just the list that suffers, but the entire backend.
List stability is the second half of the task. After a refresh, the data should not "jump": we preserve the scroll position, do not lose already loaded items, and carefully merge in new records without a full re-render.
Then the person sees that the list has actually refreshed, rather than flickering and returning to its original state.
The measurable effect of such an implementation is visible in three metrics: the time from gesture to the appearance of fresh data, the share of refreshes that completed successfully, and the number of support requests complaining that it "doesn't refresh" or "freezes." All three are directly tied to how honestly the gesture is handled — and these are exactly what we look at when we take an app into work.
Ways to Implement List Refresh: Comparison of Approaches
The choice of list refresh mechanics determines not only the appearance of the screen, but also how the app behaves on a weak network, how much traffic and battery it consumes. Below are four working options that we use in projects, with the honest limitations of each.
| Option | When it fits | Limitations |
|---|---|---|
| Built-in pull-to-refresh gesture | The main scenario for feeds, catalogs, chats: the user decides when to refresh | On screens without scrolling, the gesture won't work; on long lists, it conflicts with horizontal card swipes |
| Custom animation | When refresh is part of the brand: custom indication, illustration, staged loading progress | More expensive to develop and maintain; it's easy to break familiar behavior if the gesture differs from the system one |
| Refresh button | Screens with forms, filters, infrequent data: refresh by explicit action | The user returns to fresh data less often; the button needs to be kept within reach |
| Auto-refresh on a timer | Dashboards, quotes, order statuses, where up-to-dateness without actions matters | Traffic and battery consumption; when the app is minimized, the timer must be stopped, otherwise you'll get background requests for nothing |
How we choose in a project
We focus on the frequency of data changes and on whether the user is willing to wait. If content updates frequently and is viewed passively, we combine auto-refresh with a manual gesture as a fallback. If data changes rarely, an extra timer only burns battery.
We separately check behavior when the connection drops: the gesture must complete correctly, show an error state, and not leave the indicator spinning endlessly. These are the scenarios that most often break in production and that we close before release.
How we implement pull-to-refresh: stages of engineering work
Pull-to-refresh rarely breaks on its own — more often it conflicts with what already exists in the app: nested lists, horizontal carousels, custom scroll areas.
That's why we start not with code, but with an audit: we look at where lists live, which gestures are already being intercepted, and how scrolling behaves on long and empty screens.
How the process is structured
- Scroll audit. We go through the screens and record all scrollable areas, nesting, and current gesture handlers. The output is a conflict map: where refresh is appropriate and where the gesture is already taken by another action.
- Gesture design. We determine how refresh fits into the existing navigation: direction, capture moment, behavior during a partial swipe and cancellation. We separately spell out what happens if the user lifts their finger before the threshold.
- Trigger threshold tuning. We select the force and distance at which refresh triggers predictably but doesn't interfere with normal scrolling. We test on different screen densities so the gesture doesn't fire accidentally.
- State indication. We agree on what waiting, success, and refresh error look like, and what the user sees if there is no data. The state must not replace content or block the list.
- Testing on devices. We run the gesture on real models with different screen refresh rates, and check behavior under poor network conditions, rapid repeated swipes, and during active scrolling.
- Failure handling. We separately check what happens when the connection drops and the request is retried: the list must not remain in a state of eternal loading.
- Handover of the solution. We deliver the implementation with a description of the logic, threshold settings, and a verification checklist so the team can maintain and evolve the gesture without us.
Each stage ends with an artifact — a screen map, gesture parameters, or a test protocol. This makes it possible to return to any step without rebuilding the entire solution from scratch.
What the client receives after implementing the refresh gesture
Implementing pull-to-refresh is not a single line in the screen markup, but a set of artifacts by which the gesture lives and evolves further without its author. Below is what exactly remains with the client in the repository and in the documentation.
- Gesture configuration. The trigger threshold, inertia, behavior on a short flick and on hold — are extracted into parameters rather than smeared across the screen code. It can be changed without rebuilding the list logic.
- State schema. The transitions are explicitly described: idle → pulled → threshold reached → loading → success or error → return. Each state has a single visual representation, so the indicator does not "stick" and is not duplicated during rapid repeated gestures.
- Error handling policies. It is separately specified what to do on a network drop, timeout, and empty response: whether to show previous data, how long to keep the indicator, when to offer a retry. An error stops being a white screen.
- Documentation. A description of the gesture parameters, a state diagram, and extension rules — so that a new engineer can bring the screen into operation without contacting the author.
- Testing regulations. Scenarios on a slow network, on load interruption, on a parallel gesture, and on return from the background — with the expected behavior of the indicator and the list.
Together this delivers a predictable gesture on all screens and eliminates a class of defects that usually surface already in production. Describe the task — we will come back with an estimate and a plan for your application.
Case: speeding up feed refresh in a delivery app
The client is a food delivery service with a restaurant feed and order tracking.
After the release of a new version, support was flooded with the same type of complaints: "I pulled down — it spins and nothing happens," "I refreshed — but the order status is old." We analyzed the refresh funnel using logs and saw that the pull-to-refresh gesture did fire, but the request was sent at the moment when the previous one had not yet completed — responses arrived out of order, and the interface displayed stale data.
What it was before
Measurements in production over two weeks yielded three key metrics: median response time to the gesture — 2.8 s, share of successful refreshes — 71%, and on slow networks the feed did not refresh at all.
Users perceived this as "the app froze" and left to restart it manually — while some simply closed the screen.
What was changed
We rewrote the gesture logic: we cancel the previous request on a new pull, set a timeout, and show an honest loading indicator instead of an endless spinner.
We added a local cache of the last successful response — when the connection drops, the feed remains readable, and when it is restored, it loads in the background. An error is now visible as text with a "retry" button, rather than a silent freeze.
What it became
Median response time dropped to 0.9 s, the share of successful refreshes rose to 96%, and complaints about the "frozen feed" in support decreased almost fourfold in the first month.
We separately noted growth in repeat orders from the feed — people stopped being afraid to refresh the screen.
How does pull-to-refresh differ from automatic refresh?
Pull-to-refresh is a gesture that the user initiates themselves: they pull the list down, release it, and the data is re-fetched.
Automatic (background) refresh works without human involvement — on a schedule, when returning to the app from a minimized state, when the network appears, or on a signal from the server.
The difference is fundamental: in the first case, the user controls the moment of refresh; in the second, the app does.
Manual refresh is appropriate where data changes unpredictably and the user themselves knows they are waiting for fresh numbers: a feed, quotes, order status, chat list.
The gesture gives a sense of control and instant feedback — the indicator shows that the request has been sent. But if you rely on it alone, the user will be yanking the list every few seconds, and the data will still be outdated until they pull.
Background refresh solves the opposite problem: the interface stays up to date without any action from the user. It is appropriate for reference lists, catalogs, unread counters, where freshness matters but split-second accuracy is not critical.
Its weak spot is invisibility: if the data has refreshed but the user did not see it, they will still pull the list down to make sure.
In practice, the modes are combined.
Background synchronization keeps data fresh and pulls in changes when returning to the app, while pull-to-refresh remains as an explicit way to "refresh now" — for cases when the user does not trust the automation or is waiting for a specific change.
It is important to separate them by frequency: background requests go out at a reasonable interval and do not duplicate each other, while the gesture always forces a request bypassing the schedule.
Another layer is appropriateness per screen. On lists with pagination the gesture makes sense; on input forms and in the cart it only gets in the way: an accidental downward movement resets the filled-in fields.
That is why the refresh mode is tied to a specific screen rather than enabled globally across the entire app.
Common technical concerns when implementing pull-to-refresh
The pull-to-refresh gesture almost always conflicts with scrolling: if the trigger threshold is small, the list "jerks" during normal upward scrolling; if it is large, the user cannot tell whether the gesture fired or not.
We separate the areas of responsibility: the gesture activates only when the list is already at the top, and the indicator stretches with the finger's displacement with resistance — so scrolling and refreshing do not interfere with each other.
Does the gesture work on older devices?
Yes, but with caveats. On weak hardware, the indicator animation may lag, so we keep it on simple transforms without heavy repaints and do not start a network request until the gesture is complete. On devices without gesture support, we leave a visible refresh button — functionality is not lost.
What happens when the network drops?
A refresh should not "eat" the data already shown. If the request fails, we keep the current list on screen, show an unobtrusive notification, and let the user repeat the gesture.
The error does not block the interface and does not reset the scroll position — the user stays where they were.
How are server-side failures handled?
We distinguish between types of failures: timeout, authorization denial, data error. Each has its own text and its own behavior: sometimes retrying is enough, sometimes re-logging in is required. The indicator always completes correctly, even on error — we rule out "eternal" spinning.
Should compatibility be tested separately?
Yes. The gesture behaves differently across platforms and shells, so we test it on real devices, not just in an emulator. This catches conflicts with scrolling and system gestures before release, rather than in user reviews.
Let's discuss your task of updating lists
For pull-to-refresh to work predictably rather than "jerk" on every screen, a short, focused conversation is needed: where exactly updating is critical, what data source stands behind it, and what happens when the network fails. Describe the task in free form — we will come back with an estimate and a work plan.
- Clarify the requirements. We sort out which screens need pull-to-refresh, what counts as fresh data, and how the list behaves when the server response is empty.
- Gather the inputs. We need the current mockups or a description of the screens, examples of responses from the data source, and scenarios where updating is already unstable.
- Pin down the gesture behavior. We define the trigger threshold, the loading indicator, gesture cancellation, and how the screen reacts to a slow response.
- Estimate the scope. We count how many screens are affected, whether there is shared list logic, and whether changes will be needed on the data source side.
- Prepare the work plan. We break it down into stages: interface changes, handling loading and error states, and testing on real devices.
- Approve and start. After you confirm the plan, we assign a responsible engineer and set the order of interim checks.
To come back with an estimate and a plan rather than general words, send access to the repository or a test build and a short description of the update scenarios. That is enough to understand the scope and propose the next steps.







