Build apps faster with our new App builder! Check it out →

pullRefresh

Common

Modifier in Material Compose

A nested scroll modifier that provides scroll events to [state].

Last updated:

Installation

dependencies {
   implementation("androidx.compose.material:material:1.8.0-alpha04")
}

Overloads

@ExperimentalMaterialApi
fun Modifier.pullRefresh(state: PullRefreshState, enabled: Boolean = true)

Parameters

namedescription
stateThe [PullRefreshState] associated with this pull-to-refresh component. The state will be updated by this modifier.
enabledIf not enabled, all scroll delta and fling velocity will be ignored.
@ExperimentalMaterialApi
fun Modifier.pullRefresh(
    onPull: (pullDelta: Float) -> Float,
    onRelease: suspend (flingVelocity: Float) -> Float,
    enabled: Boolean = true
)

Parameters

namedescription
onPullCallback for dispatching vertical scroll delta, takes float pullDelta as argument. Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling down despite being at the top of a scrollable component), whereas negative delta (swiping up) is dispatched first (in case it is needed to push the indicator back up), and then the unconsumed delta is passed on to the child. The callback returns how much delta was consumed.
onReleaseCallback for when drag is released, takes float flingVelocity as argument. The callback returns how much velocity was consumed - in most cases this should only consume velocity if pull refresh has been dragged already and the velocity is positive (the fling is downwards), as an upwards fling should typically still scroll a scrollable component beneath the pullRefresh. This is invoked before any remaining velocity is passed to the child.
enabledIf not enabled, all scroll delta and fling velocity will be ignored and neither [onPull] nor [onRelease] will be invoked.

Code Examples

PullRefreshSample

@Composable
@OptIn(ExperimentalMaterialApi::class)
fun PullRefreshSample() {
    val refreshScope = rememberCoroutineScope()
    var refreshing by remember { mutableStateOf(false) }
    var itemCount by remember { mutableStateOf(15) }

    fun refresh() =
        refreshScope.launch {
            refreshing = true
            delay(1500)
            itemCount += 5
            refreshing = false
        }

    val state = rememberPullRefreshState(refreshing, ::refresh)

    Box(Modifier.pullRefresh(state)) {
        LazyColumn(Modifier.fillMaxSize()) {
            if (!refreshing) {
                items(itemCount) { ListItem { Text(text = "Item ${itemCount - it}") } }
            }
        }

        PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
    }
}

CustomPullRefreshSample

/**
 * An example to show how [pullRefresh] could be used to build a custom pull to refresh component.
 */
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun CustomPullRefreshSample() {
    val refreshScope = rememberCoroutineScope()
    val threshold = with(LocalDensity.current) { 160.dp.toPx() }

    var refreshing by remember { mutableStateOf(false) }
    var itemCount by remember { mutableStateOf(15) }
    var currentDistance by remember { mutableStateOf(0f) }

    val progress = currentDistance / threshold

    fun refresh() =
        refreshScope.launch {
            refreshing = true
            delay(1500)
            itemCount += 5
            refreshing = false
        }

    fun onPull(pullDelta: Float): Float =
        when {
            refreshing -> 0f
            else -> {
                val newOffset = (currentDistance + pullDelta).coerceAtLeast(0f)
                val dragConsumed = newOffset - currentDistance
                currentDistance = newOffset
                dragConsumed
            }
        }

    fun onRelease(velocity: Float): Float {
        if (refreshing) return 0f // Already refreshing - don't call refresh again.
        if (currentDistance > threshold) refresh()

        refreshScope.launch {
            animate(initialValue = currentDistance, targetValue = 0f) { value, _ ->
                currentDistance = value
            }
        }

        // Only consume if the fling is downwards and the indicator is visible
        return if (velocity > 0f && currentDistance > 0f) {
            velocity
        } else {
            0f
        }
    }

    Box(Modifier.pullRefresh(::onPull, ::onRelease)) {
        LazyColumn {
            if (!refreshing) {
                items(itemCount) { ListItem { Text(text = "Item ${itemCount - it}") } }
            }
        }

        // Custom progress indicator
        AnimatedVisibility(visible = (refreshing || progress > 0)) {
            if (refreshing) {
                LinearProgressIndicator(Modifier.fillMaxWidth())
            } else {
                LinearProgressIndicator(progress, Modifier.fillMaxWidth())
            }
        }
    }
}
by @alexstyl