androidx.compose.material3.pulltorefresh

Interfaces

PullToRefreshState

The state of a PullToRefreshBox which tracks the distance that the container and indicator have been pulled.

Cmn

Objects

PullToRefreshDefaults

Contains the default values for PullToRefreshBox

Cmn

Top-level functions summary

Unit
@Composable
PullToRefreshBox(
    isRefreshing: Boolean,
    onRefresh: () -> Unit,
    modifier: Modifier,
    state: PullToRefreshState,
    contentAlignment: Alignment,
    indicator: @Composable BoxScope.() -> Unit,
    content: @Composable BoxScope.() -> Unit
)

PullToRefreshBox is a container that expects a scrollable layout as content and adds gesture support for manually refreshing when the user swipes downward at the beginning of the content.

Cmn
PullToRefreshState

Creates a PullToRefreshState.

Cmn
PullToRefreshState

Create and remember the default PullToRefreshState.

Cmn

Extension functions summary

Modifier
Modifier.pullToRefresh(
    isRefreshing: Boolean,
    state: PullToRefreshState,
    enabled: Boolean,
    threshold: Dp,
    onRefresh: () -> Unit
)

A Modifier that adds nested scroll to a container to support a pull-to-refresh gesture.

Cmn

Top-level functions

PullToRefreshBox

@Composable
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
): Unit

PullToRefreshBox is a container that expects a scrollable layout as content and adds gesture support for manually refreshing when the user swipes downward at the beginning of the content. By default, it uses PullToRefreshDefaults.Indicator as the refresh indicator, but you may also choose to set your own indicator or use PullToRefreshDefaults.LoadingIndicator.

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier var itemCount by remember { mutableIntStateOf(15) } var isRefreshing by remember { mutableStateOf(false) } val state = rememberPullToRefreshState() val coroutineScope = rememberCoroutineScope() val onRefresh: () -> Unit = {  isRefreshing = true  coroutineScope.launch {  delay(5000)  itemCount += 5  isRefreshing = false  } } Scaffold(  topBar = {  TopAppBar(  title = { Text("Title") },  // Provide an accessible alternative to trigger refresh.  actions = {  IconButton(onClick = onRefresh) {  Icon(Icons.Filled.Refresh, "Trigger Refresh")  }  },  )  } ) {  PullToRefreshBox(  modifier = Modifier.padding(it),  state = state,  isRefreshing = isRefreshing,  onRefresh = onRefresh,  ) {  LazyColumn(Modifier.fillMaxSize()) {  items(itemCount) { ListItem({ Text(text = "Item ${itemCount - it}") }) }  }  } }

Using a androidx.compose.material3.LoadingIndicator as the PullToRefreshBox indicator can be done like this

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier var itemCount by remember { mutableIntStateOf(15) } var isRefreshing by remember { mutableStateOf(false) } val state = rememberPullToRefreshState() val coroutineScope = rememberCoroutineScope() val onRefresh: () -> Unit = {  isRefreshing = true  coroutineScope.launch {  delay(5000)  itemCount += 5  isRefreshing = false  } } Scaffold(  topBar = {  TopAppBar(  title = { Text("Title") },  // Provide an accessible alternative to trigger refresh.  actions = {  IconButton(onClick = onRefresh) {  Icon(Icons.Filled.Refresh, "Trigger Refresh")  }  },  )  } ) {  PullToRefreshBox(  modifier = Modifier.padding(it),  state = state,  isRefreshing = isRefreshing,  onRefresh = onRefresh,  indicator = {  PullToRefreshDefaults.LoadingIndicator(  state = state,  isRefreshing = isRefreshing,  modifier = Modifier.align(Alignment.TopCenter),  )  },  ) {  LazyColumn(Modifier.fillMaxSize()) {  items(itemCount) { ListItem({ Text(text = "Item ${itemCount - it}") }) }  }  } }

View models can be used as source as truth as shown in

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope val viewModel = remember {  object : ViewModel() {  private val refreshRequests = Channel<Unit>(1)  var isRefreshing by mutableStateOf(false)  private set  var itemCount by mutableIntStateOf(15)  private set  init {  viewModelScope.launch {  for (r in refreshRequests) {  isRefreshing = true  try {  itemCount += 5  delay(5000) // simulate doing real work  } finally {  isRefreshing = false  }  }  }  }  fun refresh() {  refreshRequests.trySend(Unit)  }  } } Scaffold(  topBar = {  TopAppBar(  title = { Text("Title") },  // Provide an accessible alternative to trigger refresh.  actions = {  IconButton(  enabled = !viewModel.isRefreshing,  onClick = { viewModel.refresh() },  ) {  Icon(Icons.Filled.Refresh, "Trigger Refresh")  }  },  )  } ) {  PullToRefreshBox(  modifier = Modifier.padding(it),  isRefreshing = viewModel.isRefreshing,  onRefresh = { viewModel.refresh() },  ) {  LazyColumn(Modifier.fillMaxSize()) {  if (!viewModel.isRefreshing) {  items(viewModel.itemCount) {  ListItem({ Text(text = "Item ${viewModel.itemCount - it}") })  }  }  }  } }

A custom state implementation can be initialized like this

import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier var itemCount by remember { mutableIntStateOf(15) } var isRefreshing by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val onRefresh: () -> Unit = {  isRefreshing = true  coroutineScope.launch {  // fetch something  delay(5000)  itemCount += 5  isRefreshing = false  } } val state = remember {  object : PullToRefreshState {  private val anim = Animatable(0f, Float.VectorConverter)  override val distanceFraction  get() = anim.value  override val isAnimating: Boolean  get() = anim.isRunning  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)  }  } } Scaffold(  topBar = {  TopAppBar(  title = { Text("TopAppBar") },  // Provide an accessible alternative to trigger refresh.  actions = {  IconButton(onClick = onRefresh) {  Icon(Icons.Filled.Refresh, "Trigger Refresh")  }  },  )  } ) {  PullToRefreshBox(  modifier = Modifier.padding(it),  isRefreshing = isRefreshing,  onRefresh = onRefresh,  state = state,  ) {  LazyColumn(Modifier.fillMaxSize()) {  if (!isRefreshing) {  items(itemCount) { ListItem({ Text(text = "Item ${itemCount - it}") }) }  }  }  } }

Scaling behavior can be implemented like this

import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer var itemCount by remember { mutableIntStateOf(15) } var isRefreshing by remember { mutableStateOf(false) } val state = rememberPullToRefreshState() val coroutineScope = rememberCoroutineScope() val onRefresh: () -> Unit = {  isRefreshing = true  coroutineScope.launch {  // fetch something  delay(5000)  itemCount += 5  isRefreshing = false  } } val scaleFraction = {  if (isRefreshing) 1f  else LinearOutSlowInEasing.transform(state.distanceFraction).coerceIn(0f, 1f) } Scaffold(  modifier =  Modifier.pullToRefresh(  state = state,  isRefreshing = isRefreshing,  onRefresh = onRefresh,  ),  topBar = {  TopAppBar(  title = { Text("TopAppBar") },  // Provide an accessible alternative to trigger refresh.  actions = {  IconButton(onClick = onRefresh) {  Icon(Icons.Filled.Refresh, "Trigger Refresh")  }  },  )  }, ) {  Box(Modifier.padding(it)) {  LazyColumn(Modifier.fillMaxSize()) {  if (!isRefreshing) {  items(itemCount) { ListItem({ Text(text = "Item ${itemCount - it}") }) }  }  }  Box(  Modifier.align(Alignment.TopCenter).graphicsLayer {  scaleX = scaleFraction()  scaleY = scaleFraction()  }  ) {  PullToRefreshDefaults.Indicator(state = state, isRefreshing = isRefreshing)  }  } }

Custom indicators with default transforms can be seen in

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp var itemCount by remember { mutableIntStateOf(15) } var isRefreshing by remember { mutableStateOf(false) } val state = rememberPullToRefreshState() val coroutineScope = rememberCoroutineScope() val onRefresh: () -> Unit = {  isRefreshing = true  coroutineScope.launch {  delay(1500)  itemCount += 5  isRefreshing = false  } } Scaffold(  topBar = {  TopAppBar(  title = { Text("Title") },  // Provide an accessible alternative to trigger refresh.  actions = {  IconButton(onClick = onRefresh) {  Icon(Icons.Filled.Refresh, "Trigger Refresh")  }  },  )  } ) {  PullToRefreshBox(  modifier = Modifier.padding(it),  state = state,  isRefreshing = isRefreshing,  onRefresh = onRefresh,  indicator = {  PullToRefreshDefaults.IndicatorBox(  state = state,  isRefreshing = isRefreshing,  modifier = Modifier.align(Alignment.TopCenter),  elevation = 0.dp,  ) {  if (isRefreshing) {  CircularProgressIndicator()  } else {  CircularProgressIndicator(  progress = { state.distanceFraction },  trackColor = ProgressIndicatorDefaults.circularIndeterminateTrackColor,  )  }  }  },  ) {  LazyColumn(Modifier.fillMaxSize()) {  items(itemCount) { ListItem({ Text(text = "Item ${itemCount - it}") }) }  }  } }
Parameters
isRefreshing: Boolean

whether a refresh is occurring

onRefresh: () -> Unit

callback invoked when the user gesture crosses the threshold, thereby requesting a refresh.

modifier: Modifier = Modifier

the Modifier to be applied to this container

state: PullToRefreshState = rememberPullToRefreshState()

the state that keeps track of distance pulled

contentAlignment: Alignment = Alignment.TopStart

The default alignment inside the Box.

indicator: @Composable BoxScope.() -> Unit = { Indicator( modifier = Modifier.align(Alignment.TopCenter), isRefreshing = isRefreshing, state = state, ) }

the indicator that will be drawn on top of the content when the user begins a pull or a refresh is occurring

content: @Composable BoxScope.() -> Unit

the content of the pull refresh container, typically a scrollable layout such as LazyColumn or a layout using Modifier.verticalScroll

PullToRefreshState

fun PullToRefreshState(): PullToRefreshState

Creates a PullToRefreshState.

Note that in most cases, you are advised to use rememberPullToRefreshState when in composition.

rememberPullToRefreshState

@Composable
fun rememberPullToRefreshState(): PullToRefreshState

Create and remember the default PullToRefreshState.

Extension functions

pullToRefresh

fun Modifier.pullToRefresh(
    isRefreshing: Boolean,
    state: PullToRefreshState,
    enabled: Boolean = true,
    threshold: Dp = PullToRefreshDefaults.PositionalThreshold,
    onRefresh: () -> Unit
): Modifier

A Modifier that adds nested scroll to a container to support a pull-to-refresh gesture. When the user pulls a distance greater than threshold and releases the gesture, onRefresh is invoked. PullToRefreshBox applies this automatically.

Parameters
isRefreshing: Boolean

whether a refresh is occurring or not, if there is no gesture in progress when isRefreshing is false the state.distanceFraction will animate to 0f, otherwise it will animate to 1f

state: PullToRefreshState

state that keeps track of the distance pulled

enabled: Boolean = true

whether nested scroll events should be consumed by this modifier

threshold: Dp = PullToRefreshDefaults.PositionalThreshold

how much distance can be scrolled down before onRefresh is invoked

onRefresh: () -> Unit

callback that is invoked when the distance pulled is greater than threshold