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
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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
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(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun PullToRefreshViewModelSample() {
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}") })
}
}
}
}
}
}
PullToRefreshSampleCustomState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
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 { 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)
}
}
}
}
PullToRefreshCustomIndicatorWithDefaultTransform
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
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}") }) }
}
}
}
}
Create your own Component Library
Material Components are meant to be used as is and they do not allow customizations. To build your own Jetpack Compose component library use Compose Unstyled