PullToRefreshBox
Composable Component
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
.
Common
@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,
)
Parameters
isRefreshing | whether a refresh is occurring |
onRefresh | callback invoked when the user gesture crosses the threshold, thereby requesting a refresh. |
modifier | the Modifier to be applied to this container |
state | the state that keeps track of distance pulled |
contentAlignment | The default alignment inside the Box. |
indicator | the indicator that will be drawn on top of the content when the user begins a pull or a refresh is occurring |
content | the content of the pull refresh container, typically a scrollable layout such as LazyColumn or a layout using Modifier.verticalScroll |
Code Examples
PullToRefreshSample
@Composable
@Preview
@OptIn(ExperimentalMaterial3Api::class)
fun PullToRefreshSample() {
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}") }) }
}
}
}
}
PullToRefreshWithLoadingIndicatorSample
@Composable
@Preview
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
fun PullToRefreshWithLoadingIndicatorSample() {
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}") }) }
}
}
}
}
PullToRefreshViewModelSample
@Composable
@Preview
@OptIn(ExperimentalMaterial3Api::class)
fun PullToRefreshViewModelSample() {
val viewModel = remember {
object : ViewModel() {
private val refreshRequests = Channel<Unit>(1)
var isRefreshing by mutableStateOf(false)
private set
var itemCount by mutableStateOf(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}") })
}
}
}
}
}
}
PullToRefreshSampleCustomState
@Composable
@Preview
@OptIn(ExperimentalMaterial3Api::class)
fun PullToRefreshSampleCustomState() {
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}") }) }
}
}
}
}
}
PullToRefreshScalingSample
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun PullToRefreshScalingSample() {
var itemCount by remember { mutableStateOf(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)
}
}
}
}
PullToRefreshCustomIndicatorWithDefaultTransform
@Composable
@Preview
@OptIn(ExperimentalMaterial3Api::class)
fun PullToRefreshCustomIndicatorWithDefaultTransform() {
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}") }) }
}
}
}
}