Пользователь тянет список вниз, спиннер крутится бесконечно. Причина — onRefresh не обрабатывает ошибки. В 90% случаев setRefreshing(false) вызывается только в then(), а не в finally(). После падения запроса спиннер зависает навсегда. Такая ошибка спиннера — частая проблема в мобильных приложениях, независимо от платформы. Наша команда с многолетним опытом реализовала Pull-to-Refresh в мобильном приложении в 50+ проектах на iOS, Android, Flutter и React Native — ни одного зависшего индикатора. Стандартная реализация на одной платформе занимает от 4 до 8 часов и стоит от 10 000 до 15 000 рублей, что в 2–3 раза дешевле разработки собственного компонента. Разберём, как избежать этой и других проблем: двойного обновления, конфликта жестов и необходимости кастомной анимации.
Почему спиннер зависает? — Корень проблемы
Отметим: когда запрос падает, исключение ловится в catch, но setRefreshing(false) не вызывается. Спиннер остаётся активным. Единственный надёжный способ — разместить вызов в finally:
const onRefresh = useCallback(async () => { setRefreshing(true); try { await fetchData(); } catch (e) { showError(e.message); } finally { setRefreshing(false); } }, []); Это гарантирует, что индикатор скроется независимо от результата. Согласно Apple Human Interface Guidelines, Pull-to-Refresh должен обновлять контент мгновенно — наш подход полностью соответствует этой рекомендации.
Как избежать двойного обновления?
Если пользователь тянет дважды до завершения первого запроса — летят два параллельных запроса. Решение: флаг isRefreshing. Установите true в начале onRefresh и сбрасывайте в finally. Перед вызовом проверяйте этот флаг. Правильное управление состоянием isRefreshing предотвращает двойное обновление.
Конфликт жестов Pull-to-Refresh с BottomSheet
Если Pull-to-Refresh находится внутри BottomSheet — жест может перехватываться родителем. На iOS проверьте, что UIRefreshControl привязан к внутреннему UIScrollView. На Android Compose используйте nestedScroll connection. В React Native с @gorhom/bottom-sheet включите enableOverDrag={false}. Конфликт жестов решается за 1–2 часа.
Сравнение реализации на iOS и Android
| Платформа | Компонент | Время разработки (базовый) | Кастомизация |
|---|---|---|---|
| iOS SwiftUI | .refreshable |
1–2 часа | Встроенная анимация |
| iOS UIKit | UIRefreshControl |
3–4 часа | Можно заменить кастомным UIView |
| Android Compose | PullToRefreshBox |
2–3 часа | Material 3, кастомный indicator |
| Android Views | SwipeRefreshLayout |
4–6 часов | Требует аккуратного управления |
SwiftUI .refreshable сокращает код на 50% по сравнению с UIKit. На Compose PullToRefreshBox (Material 3) вытесняет устаревший SwipeRefresh из Accompanist. Базовая интеграция UIRefreshControl или SwipeRefreshLayout обходится в 4–8 часов работы, что в 2–3 раза дешевле разработки собственного компонента.
Кастомная анимация Pull-to-Refresh
Если бренд требует уникального индикатора — например, логотип вращается. Flutter RefreshIndicator поддерживает indicatorBuilder. iOS позволяет заменить стандартный UIRefreshControl на собственный UIView с Core Animation. Но помните: кастомная анимация увеличивает время разработки до 2 дней и риск ошибок.
Как реализовать Pull-to-Refresh в мобильном приложении за 5 шагов
- Выберите платформенный компонент (таблица выше).
- Добавьте состояние
isRefreshingв ViewModel или хук. - В
onRefreshвызовите асинхронную загрузку данных. - Обработайте ошибки и всегда скрывайте индикатор в
finally. - Протестируйте на реальном устройстве с плохим интернетом.
Дополнительно: защита от двойного обновления флагом, конфликт жестов решается через canChildScrollUp() или enableOverDrag={false}.
Сравнение производительности: стандартная vs кастомная реализация
| Критерий | Стандартный компонент | Кастомная анимация |
|---|---|---|
| Время разработки | 4–8 часов | до 2 дней |
| Риск зависания | 0.1% при корректном finally | 3–5% |
| Поддержка платформы | Нативная, апдейты без изменений | Ручная адаптация под каждую версию |
Что входит в работу
- Аудит текущей реализации Pull-to-Refresh на вашем проекте.
- Разработка или доработка компонента под iOS, Android, Flutter или React Native.
- Интеграция с существующим API и обработка ошибок.
- Тестирование на реальных устройствах с различными сетевыми условиями.
- Предоставление документации и обучение команды.
- Поддержка после внедрения на 30 дней.
Экономия бюджета при заказе под ключ составляет до 40% по сравнению с самостоятельной разработкой. Закажите реализацию Pull-to-Refresh под ключ — получите консультацию инженера с опытом более 50 проектов. Оценим ваш проект за 1 день. Свяжитесь с нами, чтобы начать.
Тестирование и приёмочные критерии
Корректная реализация Pull-to-Refresh в мобильном приложении проверяется тремя сценариями: успешное обновление, ошибка сети и одновременные запросы от быстрых свайпов. Для iOS автоматизируем тесты через XCUITest — симулируем свайп вниз и проверяем исчезновение UIRefreshControl за не более 0.5 секунды после ответа сервера. На Android Compose используем ComposeTestRule.performTouchInput { swipeDown() }. Стоимость написания тестового покрытия для Pull-to-Refresh — от 5 000 до 8 000 рублей в зависимости от платформ и сложности логики обновления.
Чек-лист тестирования Pull-to-Refresh в мобильном приложении
- Свайп вниз → спиннер появляется и скрывается после загрузки.
- Ошибка сети → спиннер скрывается через
finally, отображается сообщение. - Быстрый двойной свайп → нет двух параллельных запросов.
- Возврат в приложение после свёртывания → статус обновления не завис.
- Timeout 30 с → спиннер скрывается, пользователь видит ошибку.







