[Jetpack Compose] Implement "Pull-to-Refresh" with the New PullToRefreshBox

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.


Related Categories :  AndroidAndroidStudioappsJetBrainsJetpackComposeKotlinNewbieRecommendedTools