Реализация Pull-to-Refresh в мобильном приложении
Pull-to-refresh — один из самых узнаваемых жестов в мобильных приложениях. Платформы дают готовые компоненты: RefreshControl в React Native, SwipeRefreshLayout на Android, UIRefreshControl на iOS, RefreshIndicator во Flutter. Основные проблемы — не в реализации самого жеста, а в управлении состоянием обновления и корректном поведении при ошибках.
Частые ошибки
Спиннер не скрывается при ошибке. onRefresh вызывается, запрос падает, но refreshing в RefreshControl остаётся true навсегда. Причина: setRefreshing(false) вызывается только в then(), а не в finally(). Правильно:
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
await fetchData();
} catch (e) {
showError(e.message);
} finally {
setRefreshing(false); // всегда скрываем спиннер
}
}, []);
Конфликт с другими жестами. Pull-to-refresh внутри ScrollView, который сам внутри Modal или BottomSheet — жест иногда перехватывается родительским контейнером. На Android SwipeRefreshLayout решает конфликты через canChildScrollUp(). В React Native при использовании @gorhom/bottom-sheet — enableOverDrag={false} в bottom sheet, чтобы вертикальный свайп вниз не конфликтовал с pull-to-refresh.
Двойное обновление. Если пользователь успел потянуть дважды до получения первого ответа — летят два запроса. Решение: флаг isRefreshing блокирует повторный вызов.
Реализация по платформам
На Flutter — RefreshIndicator оборачивает ListView или CustomScrollView. onRefresh должен возвращать Future — виджет сам скроет индикатор после завершения. Кастомизировать цвет через color и backgroundColor. Для CustomScrollView — SliverRefreshControl (Cupertino-стиль, нативный для iOS look).
На Android Compose — PullToRefreshBox из Material 3 (Compose 1.3+) или SwipeRefresh из accompanist (устарел, но всё ещё встречается). isRefreshing — state из ViewModel, onRefresh — suspend функция через viewModelScope.launch.
На iOS нативно — UIRefreshControl добавляется к scrollView.refreshControl. beginRefreshing() / endRefreshing() управляют состоянием. В SwiftUI — .refreshable {} modifier на List или ScrollView.
UX-детали
После успешного обновления показываем краткое уведомление («Обновлено только что»), если список изменился. Если новых данных нет — молча скрываем спиннер, не пишем «Нет новых данных» — это раздражает.
Порог активации pull-to-refresh: стандартный ~60–80pt достаточен. Не делаем порог слишком маленьким — случайные срабатывания при начале скролла.
Что входит в работу
- Интеграция
RefreshControl/RefreshIndicator/SwipeRefreshLayout - Корректное управление состоянием (always
finallyдля скрытия) - Обработка ошибок без зависания спиннера
- Защита от двойного обновления
- При необходимости: кастомный анимированный refresh indicator
Сроки
4 часа — 1 рабочий день. Включая кастомный индикатор — до 2 дней. Стоимость рассчитывается индивидуально.







