В сети можно найти множество статей о том, как реализовать функцию pull-to-refresh с помощью библиотеки Compose Material 3. Однако API pull-to-refresh полностью изменились с версии 1.2.0, поэтому большинство материала на эту тему считается сейчас устаревшим.
В этой статье мы расскажем, как использовать новые API для добавления функции pull-to-refresh в приложение и как обновить приложение для использования новейшей версии библиотеки Compose Material 3.
Google рекомендует использовать Material 3 с Compose, даже учитывая то, что большинство API являются экспериментальными и могут вести себя иначе, чем в Material 2.
Что такое pull-to-refresh?
Pull-to-refresh или swipe-to-refresh — это популярная функция на основе жестов в мобильных приложениях, которая позволяет пользователям вручную обновлять содержимое страницы, проводя пальцем по экрану или потянув его вниз.
Обычно она используется на экранах, где отображается список данных, загруженных из удаленных сервисов, которые могут часто меняться. Подобные примеры можно увидеть в Facebook, Instagram, Twitter, Reddit и других социальных и новостных приложениях.

Начальный код
Добавим функцию pull-to-refresh в простой HomeScreen, который показывает прокручиваемый список случайных фактов о собаках. Состояние экрана обеспечивается посредством ViewModel, которая отвечает за загрузку данных из хранилища и их отображение через StateFlow. Это стандартная архитектура MVVM в большинстве современных приложений.
Первоначальное состояние HomeScreen:
data class HomeScreenState(
val items: List<AnimalFact> = listOf(),
)
Первоначальная HomeViewModel:
class HomeViewModel(private val animalFactsRepository: AnimalFactsRepository) : ViewModel() {
val screenState: StateFlow<HomeScreenState> = animalFactsRepository.observeAnimalFacts()
.map { HomeScreenState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = HomeScreenState()
)
init {
viewModelScope.launch {
animalFactsRepository.fetchAnimalFacts()
}
}
}
Собираем состояние из ViewModel в представление (Compose), а затем выводим прокручиваемый список фактов.
Первоначальная composable-функция HomeScreen:
val state by viewModel.screenState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(R.string.app_name)) },
)
},
modifier = Modifier.fillMaxSize()
) { innerPadding ->
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
items(state.items) { fact ->
AnimalFactItem(fact = fact)
}
}
}
В результате вышеперечисленных действий на этом экране отображается список фактов. Чтобы увидеть новые факты, придется закрывать и снова открывать приложение, что портит пользовательский опыт. Поэтому мы добавим на этот экран жест pull-to-refresh, чтобы пользователи могли обновлять данные.

Реализация pull-to-refresh с помощью Compose Material 3
Библиотека Compose Material 3 предлагает готовое решение для добавления pull-to-refresh в приложение. Она содержит контейнер PullToRefreshBox и модификатор .pullToRefresh.
Добавление последней версии зависимости
Перед началом работы убедитесь, что добавили последнюю версию зависимости Compose Material 3 в файл сборки на уровне приложения:
dependencies {
implementation("androidx.compose.material3:material3:1.3.0")
}
Или же, если вы используете управление зависимостями из каталога версий:
[versions]
composeBom = "2024.09.03"
[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.material3)
}
Использование PullToRefreshBox
Самый простой способ добавить функцию pull-to-refresh — использовать контейнер PullToRefreshBox. Он требует прокручиваемого макета в качестве содержимого и включает поддержку жестов, позволяя пользователю вручную обновлять контент, проведя пальцем вниз от верхней части контента. Он также предоставляет реализацию индикатора обновления по умолчанию.
@Composable
@ExperimentalMaterial3Api
fun PullToRefreshBox(
isRefreshing: Boolean,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
state: PullToRefreshState = rememberPullToRefreshState(),
contentAlignment: Alignment = Alignment.TopStart,
indicator: @Composable BoxScope.() -> Unit = {
Indicator(
modifier = Modifier.align(Alignment.TopCenter),
isRefreshing = isRefreshing,
state = state
)
},
content: @Composable BoxScope.() -> Unit
) {
Box(
modifier.pullToRefresh(state = state, isRefreshing = isRefreshing, onRefresh = onRefresh),
contentAlignment = contentAlignment
) {
content()
indicator()
}
}
Мы должны указать следующие параметры:
isRefreshing— true/false (в зависимости от того, происходит ли обновление, необходимое для круговой анимации);
onRefresh— обратный вызов, который срабатывает, когда жест пользователя превышает пороговое значение; при этом инициируется запрос на обновление;
content— вертикально прокручиваемый контент/макет, напримерLazyColumn, поверх которого при срабатывании будет отображаться индикатор обновления.
Добавление функции pull-to-refresh в наш пример
Сначала нужно обновить объект состояния экрана, чтобы он содержал новое свойство isRefreshing, которое мы можем передать PullToRefreshBox.
data class HomeScreenState(
val items: List<AnimalFact> = listOf(),
val isRefreshing: Boolean = false
)
Далее нужно обновить ViewModel, чтобы обновить состояние isRefreshing и вызвать обновление данных. Мы добавили новый поток _isRefreshing: MutableStateFlow<Boolean> для управления состоянием обновления. Каждый раз при выходе этого потока обновляется состояние экрана.
Мы также добавили функцию onPullToRefreshTrigger(), которая вызывается из Compose при срабатывании функции pull-to-refresh. Она управляет состоянием обновления и выполняет повторное извлечение данных.
class HomeViewModel(private val animalFactsRepository: AnimalFactsRepository) : ViewModel() {
private val _isRefreshing = MutableStateFlow(false)
val screenState: StateFlow<HomeScreenState> = animalFactsRepository.observeAnimalFacts()
.combine(_isRefreshing) { items, isRefreshing ->
HomeScreenState(items = items, isRefreshing = isRefreshing)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = HomeScreenState()
)
fun onPullToRefreshTrigger() {
_isRefreshing.update { true }
viewModelScope.launch {
animalFactsRepository.fetchAnimalFacts()
_isRefreshing.update { false }
}
}
}
Наконец, нужно обновить composable-функцию экрана, чтобы добавить функцию pull-to-refresh. Для этого обернем LazyColumn с помощью PullToRefreshBox и предоставим ему состояние isRefreshing и обратный вызов onRefresh, что вызывает функцию во ViewModel.
val state by viewModel.screenState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(R.string.app_name)) },
)
},
modifier = Modifier.fillMaxSize()
) { innerPadding ->
PullToRefreshBox(
isRefreshing = state.isRefreshing,
onRefresh = viewModel::onPullToRefreshTrigger,
modifier = Modifier.padding(innerPadding)
) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize()
) {
items(state.items) { fact ->
AnimalFactItem(fact = fact)
}
}
}
}
Если мы потянем вниз по экрану, появится индикатор, и мы получим обратный вызов onRefresh(). Теперь нужно запустить обновление данных через ViewModel и установить значение isRefreshing в значение true. Если этого не сделать, индикатор обновления «застрянет» и не будет анимироваться.
Также важно установить isRefreshing обратно в false после обновления данных, чтобы скрыть индикатор обновления.
Готово! Мы добавили в приложение жест pull-to-refresh.

Пользовательская настройка анимации индикатора
Можно дополнительно настроить поведение компонента pull-to-refresh, расширив PullToRefreshState. Это позволит изменить анимацию индикатора обновления. Вот пример того, как можно добавить анимацию «пружина/отскок» для индикатора после его отпускания.
val pullToRefreshState = remember {
object : PullToRefreshState {
private val anim = Animatable(0f, Float.VectorConverter)
override val distanceFraction
get () = anim.value
override suspend fun animateToThreshold() {
anim.animateTo(1f, spring(dampingRatio = Spring.DampingRatioHighBouncy))
}
override suspend fun animateToHidden() {
anim.animateTo(0f)
}
override suspend fun snapTo(targetValue: Float) {
anim.snapTo(targetValue)
}
}
}
PullToRefreshBox(
state = pullToRefreshState,
isRefreshing = state.isRefreshing,
onRefresh = viewModel::onPullToRefreshTrigger,
modifier = Modifier.padding(innerPadding)
) {
...
}
Финальный результат:

Дополнительные настройки
PullToRefreshBox предлагает простой в использовании API, но не предоставляет множество опций для настройки. Например, в нем отсутствует свойство enabled, с помощью которого мы могли бы включить или отключить жест pull-to-refresh.
При необходимости в дополнительном контроле, можно использовать модификатор .pullToRefresh напрямую, который задействуется PullToRefreshBox «под капотом». Он раскрывает свойство enabled: Boolean, а также позволяет контролировать порог срабатывания обновления с помощью свойства treshold: Dp.
Реализация модификатора pullToRefresh:
@ExperimentalMaterial3Api
fun Modifier.pullToRefresh(
isRefreshing: Boolean,
state: PullToRefreshState,
enabled: Boolean = true,
threshold: Dp = PullToRefreshDefaults.PositionalThreshold,
onRefresh: () -> Unit,
): Modifier =
this then
PullToRefreshElement(
state = state,
isRefreshing = isRefreshing,
enabled = enabled,
onRefresh = onRefresh,
threshold = threshold
)
Мы можем применить модификатор к любому макету с вертикальной прокруткой. Или же можно создать composable-функцию обертки и использовать ее так же, как PullToRefreshBox.
Реализация модификатора pullToRefresh:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun PullToRefreshWrapper(
isRefreshing: Boolean,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
enabled: Boolean = true,
content: @Composable BoxScope.() -> Unit,
) {
val refreshState = rememberPullToRefreshState()
Box(
modifier.pullToRefresh(
state = refreshState,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
enabled = enabled,
),
contentAlignment = contentAlignment,
) {
content()
PullToRefreshDefaults.Indicator(
modifier = Modifier.align(Alignment.TopCenter),
isRefreshing = isRefreshing,
state = refreshState,
)
}
}
Переход с предыдущих API
Если вы уже используете старую версию Compose Material 3 в приложении и реализовали поведение pull-to-refresh с помощью PullToRefreshContainer, вам нужно будет перейти на PullToRefreshBox. Предыдущие API устарели в версии 1.3.0 и больше не включаются в библиотеку, поэтому обновление критически важно.
Мы показали, как использовать новые API, в первой части статьи. Решение о том, на что переходить — на PullToRefreshBox или модификатор pullToRefresh, — зависит от ваших потребностей. Если вам нужна возможность включать/выключать жест pull-to-refresh, то придется использовать модификатор pullToRefresh. Если нет, то можно просто выбрать PullToRefreshBox.
Основные изменения:
rememberPullToRefreshState(): больше не принимает аргументenabled, им можно управлять с помощью модификатораpullToRefresh;
PullToRefreshContainer: заменен наPullToRefreshBoxили модификаторpullToRefresh;
- состояние
isRefreshingуправляется пользователем вместоPullToRefreshState.
Реализация pull-to-refresh с помощью ныне устаревшего PullToRefreshContainer:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun PullToRefreshWrapper(
onRefreshTrigger: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable BoxScope.() -> Unit,
) {
val refreshState = rememberPullToRefreshState(enabled = { enabled })
if (refreshState.isRefreshing) {
LaunchedEffect(true) {
onRefreshTrigger()
delay(1000)
refreshState.endRefresh()
}
}
Box(
modifier = modifier.nestedScroll(refreshState.nestedScrollConnection),
) {
content()
PullToRefreshContainer(state = refreshState, modifier = Modifier.align(Alignment.TopCenter))
}
}
Заключение
Итак, вы узнали, как реализовать простой механизм pull-to-refresh в приложении с помощью новейшей версии библиотеки Compose Material 3.
API являются экспериментальными и могут быть использованы двумя способами: с помощью обертки макета или модификатора. Выбор зависит от ваших требований, таких как пользовательская анимация или управление включением и выключением жеста.
Полный пример реализации pull-to-refresh можете найти в примере проекта на GitHub.
Читайте также:
- Как создать импульсный эффект в Jetpack Compose
- Как создать анимацию мерцающего текста в Jetpack Compose
- Jetpack Compose: настройка Retrofit и Ktor с помощью Dagger Hilt для внедрения зависимостей
Читайте нас в Telegram, VK и Дзен
Перевод статьи Domen Lanišnik: Pull to Refresh with Compose Material 3





