androidx.compose.material3.pulltorefresh


Interfaces

PullToRefreshState

The state that is associated with a PullToRefreshContainer.

Cmn

Objects

PullToRefreshDefaults

Contains the default values for PullToRefreshContainer

Cmn

Top-level functions summary

Unit
@Composable
@ExperimentalMaterial3Api
PullToRefreshContainer(
    state: PullToRefreshState,
    modifier: Modifier,
    indicator: @Composable (PullToRefreshState) -> Unit,
    shape: Shape,
    containerColor: Color,
    contentColor: Color
)

Material Design pull-to-refresh indicator

Cmn
PullToRefreshState
@ExperimentalMaterial3Api
PullToRefreshState(
    positionalThresholdPx: Float,
    initialRefreshing: Boolean,
    enabled: () -> Boolean
)

Creates a PullToRefreshState.

Cmn
PullToRefreshState
@Composable
@ExperimentalMaterial3Api
rememberPullToRefreshState(
    positionalThreshold: Dp,
    enabled: () -> Boolean
)

Create and remember the default PullToRefreshState.

Cmn

Top-level functions

PullToRefreshContainer

@Composable
@ExperimentalMaterial3Api
fun PullToRefreshContainer(
    state: PullToRefreshState,
    modifier: Modifier = Modifier,
    indicator: @Composable (PullToRefreshState) -> Unit = { pullRefreshState -> Indicator(state = pullRefreshState) },
    shape: Shape = PullToRefreshDefaults.shape,
    containerColor: Color = PullToRefreshDefaults.containerColor,
    contentColor: Color = PullToRefreshDefaults.contentColor
): Unit

Material Design pull-to-refresh indicator

A pull-to-refresh container contains a progress indicator to indicate a users drag progress towards triggering a refresh. On a refresh the progress indicator inside this container is indeterminate.

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.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll

var itemCount by remember { mutableStateOf(15) }
val state = rememberPullToRefreshState()
if (state.isRefreshing) {
    LaunchedEffect(true) {
        // fetch something
        delay(1500)
        itemCount += 5
        state.endRefresh()
    }
}
Scaffold(
    modifier = Modifier.nestedScroll(state.nestedScrollConnection),
    topBar = {
        TopAppBar(
            title = { Text("TopAppBar") },
            // Provide an accessible alternative to trigger refresh.
            actions = {
                IconButton(onClick = { state.startRefresh() }) {
                    Icon(Icons.Filled.Refresh, "Trigger Refresh")
                }
            }
        )
    }
) {
    Box(Modifier.padding(it)) {
        LazyColumn(Modifier.fillMaxSize()) {
            if (!state.isRefreshing) {
                items(itemCount) {
                    ListItem({ Text(text = "Item ${itemCount - it}") })
                }
            }
        }
        PullToRefreshContainer(
            modifier = Modifier.align(Alignment.TopCenter),
            state = state,
        )
    }
}

A custom state implementation can be initialized like this

import androidx.compose.animation.core.animate
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.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity

var itemCount by remember { mutableStateOf(15) }
val state = remember {
    object : PullToRefreshState {
        override val positionalThreshold: Float = 100f
        override val progress get() = verticalOffset / positionalThreshold
        override var verticalOffset: Float by mutableFloatStateOf(0f)
        override var isRefreshing: Boolean by mutableStateOf(false)

        override fun startRefresh() {
            isRefreshing = true
        }
        override fun endRefresh() {
            isRefreshing = false
        }

        // Provide logic for the PullRefreshContainer to consume scrolls within a nested scroll
        override var nestedScrollConnection: NestedScrollConnection =
            object : NestedScrollConnection {
                // Pre and post scroll provide the drag logic for PullRefreshContainer.
                override fun onPreScroll(
                    available: Offset,
                    source: NestedScrollSource,
                ): Offset = when {
                    source == NestedScrollSource.UserInput && available.y < 0 -> {
                        // Swiping up
                        val y = if (isRefreshing) 0f else {
                            val newOffset = (verticalOffset + available.y).coerceAtLeast(0f)
                            val dragConsumed = newOffset - verticalOffset
                            verticalOffset = newOffset
                            dragConsumed
                        }
                        Offset(0f, y)
                    }

                    else -> Offset.Zero
                }

                override fun onPostScroll(
                    consumed: Offset,
                    available: Offset,
                    source: NestedScrollSource
                ): Offset = when {
                    source == NestedScrollSource.UserInput && available.y > 0 -> {
                        // Swiping Down
                        val y = if (isRefreshing) 0f else {
                            val newOffset = (verticalOffset + available.y).coerceAtLeast(0f)
                            val dragConsumed = newOffset - verticalOffset
                            verticalOffset = newOffset
                            dragConsumed
                        }
                        Offset(0f, y)
                    }

                    else -> Offset.Zero
                }

                // Pre-Fling is called when the user releases a drag. This is where you can provide
                // refresh logic, and verify exceeding positional threshold.
                override suspend fun onPreFling(available: Velocity): Velocity {
                    if (isRefreshing) return Velocity.Zero
                    if (verticalOffset > positionalThreshold) {
                        startRefresh()
                        itemCount += 5
                        endRefresh()
                    }
                    animate(verticalOffset, 0f) { value, _ ->
                        verticalOffset = value
                    }
                    val consumed = when {
                        verticalOffset == 0f -> 0f
                        available.y < 0f -> 0f
                        else -> available.y
                    }
                    return Velocity(0f, consumed)
                }
            }
    }
}
Scaffold(
    modifier = Modifier.nestedScroll(state.nestedScrollConnection),
    topBar = {
        TopAppBar(
            title = { Text("TopAppBar") },
            // Provide an accessible alternative to trigger refresh.
            actions = {
                IconButton(onClick = { state.startRefresh() }) {
                    Icon(Icons.Filled.Refresh, "Trigger Refresh")
                }
            }
        )
    }
) {
    Box(Modifier.padding(it)) {
        LazyColumn(Modifier.fillMaxSize()) {
            if (!state.isRefreshing) {
                items(itemCount) {
                    ListItem({ Text(text = "Item ${itemCount - it}") })
                }
            }
        }
        PullToRefreshContainer(
            modifier = Modifier.align(Alignment.TopCenter),
            state = state,
        )
    }
}

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.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll

var itemCount by remember { mutableStateOf(15) }
val state = rememberPullToRefreshState()
if (state.isRefreshing) {
    LaunchedEffect(true) {
        // fetch something
        delay(1500)
        itemCount += 5
        state.endRefresh()
    }
}
val scaleFraction = if (state.isRefreshing) 1f else
    LinearOutSlowInEasing.transform(state.progress).coerceIn(0f, 1f)

Scaffold(
    modifier = Modifier.nestedScroll(state.nestedScrollConnection),
    topBar = {
        TopAppBar(
            title = { Text("TopAppBar") },
            // Provide an accessible alternative to trigger refresh.
            actions = {
                IconButton(onClick = { state.startRefresh() }) {
                    Icon(Icons.Filled.Refresh, "Trigger Refresh")
                }
            }
        )
    }
) {
    Box(Modifier.padding(it)) {
        LazyColumn(Modifier.fillMaxSize()) {
            if (!state.isRefreshing) {
                items(itemCount) {
                    ListItem({ Text(text = "Item ${itemCount - it}") })
                }
            }
        }
        PullToRefreshContainer(
            modifier = Modifier
                .align(Alignment.TopCenter)
                .graphicsLayer(scaleX = scaleFraction, scaleY = scaleFraction),
            state = state,
        )
    }
}
Parameters
state: PullToRefreshState

the state of this PullToRefreshContainer

modifier: Modifier = Modifier

the Modifier to be applied to this container

indicator: @Composable (PullToRefreshState) -> Unit = { pullRefreshState -> Indicator(state = pullRefreshState) }

The indicator placed inside of the PullToRefreshContainer. Has access to state

shape: Shape = PullToRefreshDefaults.shape

the Shape of this container

containerColor: Color = PullToRefreshDefaults.containerColor

the color of this container

contentColor: Color = PullToRefreshDefaults.contentColor

the color of the progress indicator

PullToRefreshState

@ExperimentalMaterial3Api
fun PullToRefreshState(
    positionalThresholdPx: Float,
    initialRefreshing: Boolean = false,
    enabled: () -> Boolean = { true }
): PullToRefreshState

Creates a PullToRefreshState.

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

Parameters
positionalThresholdPx: Float

The positional threshold, in pixels, in which a refresh is triggered

initialRefreshing: Boolean = false

The initial refreshing value of PullToRefreshState

enabled: () -> Boolean = { true }

a callback used to determine whether scroll events are to be handled by this PullToRefreshState

rememberPullToRefreshState

@Composable
@ExperimentalMaterial3Api
fun rememberPullToRefreshState(
    positionalThreshold: Dp = PullToRefreshDefaults.PositionalThreshold,
    enabled: () -> Boolean = { true }
): PullToRefreshState

Create and remember the default PullToRefreshState.

Parameters
positionalThreshold: Dp = PullToRefreshDefaults.PositionalThreshold

The positional threshold when a refresh would be triggered

enabled: () -> Boolean = { true }

a callback used to determine whether scroll events are to be handled by this PullToRefreshState