
The "Pull-to-Refresh" gesture is a staple in Android app UI.
While we previously relied on Modifier.pullRefresh, Jetpack Compose has introduced PullToRefreshBox in Material 3 as the new standard. It's more intuitive and requires much less boilerplate code.
In this post, we’ll quickly cover everything from basic implementation to customization!
🧑🏻💻 1. Prerequisites
PullToRefreshBox is available in Material 3 (version 1.3.0 or later).
Make sure to check your build.gradle dependencies:
dependencies {
implementation("androidx.compose.material3:material3:1.3.0")
}
🧑🏻💻 2. Basic Implementation Pattern
The best part about PullToRefreshBox is that it encapsulates both the refresh logic and the indicator UI into a single component.
@Composable
fun RefreshableListScreen() {
var isRefreshing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val items = remember { mutableStateListOf("Initial Item A", "Initial Item B") }
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
scope.launch {
isRefreshing = true
// Perform your refresh logic (e.g., API calls)
delay(2000)
items.add(0, "New Item ${items.size + 1}")
isRefreshing = false
}
}
) {
LazyColumn(Modifier.fillMaxSize()) {
items(items) { item ->
ListItem(headlineContent = { Text(item) })
}
}
}
}
Key Highlights
- isRefreshing: A boolean that controls the visibility of the refresh indicator.
- onRefresh: The callback triggered when the user performs the pull gesture.
- Content Size: Ensure your scrollable content (like LazyColumn) uses Modifier.fillMaxSize() so the pull gesture is detectable across the entire area.
🧑🏻💻 3. Practical Usage with ViewModel
In a production environment, it's best practice to let a ViewModel handle the state.
class MyViewModel : ViewModel() {
var isRefreshing by mutableStateOf(false)
private set
fun refreshData() {
viewModelScope.launch {
isRefreshing = true
// Simulate network call
isRefreshing = false
}
}
}
val viewModel: MyViewModel = viewModel()
PullToRefreshBox(
isRefreshing = viewModel.isRefreshing,
onRefresh = { viewModel.refreshData() }
) {
// ... Content
}
🧑🏻💻 4. Customizing the Design
If you want to change the indicator's color to match your brand, use the indicator parameter.
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = { /* ... */ },
indicator = {
PullToRefreshDefaults.Indicator(
state = it,
isRefreshing = isRefreshing,
containerColor = Color.DarkGray, // Background color
color = Color.Cyan // Progress spinner color
)
}
) {
// ...
}
🧑🏻💻 Conclusion: Simplified Refresh Logic
With the arrival of PullToRefreshBox, implementing this common UI pattern has never been easier.
- Use Material 3 1.3.0+.
- Pass the state (isRefreshing).
- Handle the logic in onRefresh.
That’s it! You now have a modern, native-feeling refresh experience.




