Pull to Refresh with Compose Material 3

Domen Lanišnik
5 min read5 days ago

--

There are many articles online on how to implement pull-to-refresh with the Compose Material 3 library. However, the pull-to-refresh APIs have completely changed since version 1.2.0, introducing breaking changes and making most blog posts outdated.

This article will show you how to use the new APIs to add pull-to-refresh functionality to your app and how to upgrade your existing app to use the newest version of the Compose Material 3 library.

Google recommends using Material 3 with Compose, even if most of the APIs are experimental and can behave differently from Material 2.

What is pull-to-refresh?

Pull-to-refresh or swipe-to-refresh is a common gesture-based feature in mobile apps that allows users to manually refresh the content of a page by swiping or pulling down on the screen.

It’s typically used on screens that display a list of data loaded from remote services that can change frequently. You can see examples of this in Facebook, Instagram, Twitter, Reddit, and other social and news apps.

Example of using pull-to-refresh in an app.

Starter code

We’ll be adding pull-to-refresh functionality to a simple HomeScreen that shows a scrollable list of random dog facts. The screen state is provided through a ViewModel which is responsible for loading the data from a repository and exposing it through a StateFlow. A standard MVVM architecture in most of today’s apps.

Initial home screen state.
Initial home view model.

We collect the state from the view model in the view (Compose) and then render a scrollable list of facts.

Initial home screen composable.

All of the above results in this screen showing a list of facts. To see new facts, users have to close and re-open the app, resulting in a sub-optimal UX. Therefore, we will add a pull-to-refresh gesture to this screen to let users refresh the data.

Initial screen showing a list of facts.

Implementing pull-to-refresh with Compose Material 3

Compose Material 3 library offers an out-of-the-box solution to add pull-to-refresh to your app. It contains the PullToRefreshBox container and .pullToRefresh modifier.

Adding the latest dependency

To get started, make sure you have added the latest version of the Compose Material 3 dependency to your app-level build file:

Adding Compose Material 3 dependency to app-level build file.

or if you’re using version catalog dependency management:

Adding Compose Material 3 dependency declaration to a version catalog.
Adding Compose Material 3 dependency to app-level build file through a version catalog.

Using the PullToRefreshBox

The easiest way to add pull-to-refresh is to use the PullToRefreshBox container. It requires a scrollable layout as the content and enables gesture support, allowing the user to manually refresh by swiping downward from the top of the content. It also provides a default implementation of the refreshing indicator.

PullToRefresh declaration in the Compose Material 3 library.

We have to provide the following:

  • isRefreshing — true/false whether a refresh is happening, needed for the circle animation to happen
  • onRefresh — callback that is triggered when the user’s gesture exceeds the threshold, initiating a refresh request
  • content — vertically scrollable content/layout, such as a LazyColumn, on top of which the refresh indicator will be displayed when triggered

Adding pull-to-refresh to our example

First, we have to update our screen state object to hold a new property isRefreshing that we can pass on to the PullToRefreshBox.

Home screen state with the new refreshing property.

Next, we have to update our ViewModel to update the isRefreshing state and to trigger data refresh. We’ve added a new _isRefreshing: MutableStateFlow<Boolean> to control the refreshing state. Anytime this flow emits, it updates the screen state.

We’ve also added a onPullToRefreshTrigger() function that’s called from Compose when pull-to-refresh is triggered. It controls the refreshing state and re-fetches the data.

Finally, we have to update our screen composable to add pull-to-refresh. To do that, we wrap our existing LazyColumn with PullToRefreshBox and provide it with the isRefreshing state and a onRefresh callback, which calls the function in the ViewModel.

When we pull down on the screen, the indicator will appear and we receive a onRefresh() callback. We now have to trigger the refresh of the data through the ViewModel and set the isRefreshing to true. If we do not do that, the refresh indicator will be stuck and won’t animate.

It’s also important to set isRefreshing back to false once the data is refreshed to hide the refreshing indicator.

And that’s it! We’ve added a pull-to-refresh gesture to the app.

Working pull-to-refresh on the home screen

Customizing the indicator animation

We can further customize the behavior of the pull-to-refresh component by extending the PullToRefreshState. This enables us to change the animation of the refresh indicator. Here is an example of how to add a spring/bounce animation to the indicator after it’s released.

Customizing the refresh indicator with a spring/bounce animation.

And this is the final result:

Pull-to-refresh behavior with a custom spring animation for the refresh indicator.

Further customizations

PullToRefreshBox offers an easy-to-use API but doesn’t provide many customization options. For example, it’s missing a enabled property with which we could enable or disable the pull-to-refresh gesture.

If we need further control, we can use the .pullToRefresh modifier directly, which is used by PullToRefreshBox under the hood. It exposes the enabled: Boolean property and also allows us to control the threshold of when the refresh is triggered via the treshold: Dpproperty.

Implementation of the pullToRefresh modifier.

We can apply the modifier on any layout that wraps a vertically scrollable layout. Or we can create a wrapper composable and use it in the same way as PullToRefreshBox.

Wrapper composable for pullToRefresh modifier

Migrating from previous APIs

If you already use an older version of Compose Material 3 in your app and have implemented the pull-to-refresh behavior with PullToRefreshContainer, you will need to migrate to PullToRefreshBox. The previous APIs have been deprecated in version 1.3.0 and are no longer included in the library, making the upgrade a breaking change.

We’ve shown how to use the new APIs in the first part of the article. Deciding whether to migrate to PullToRefreshBox or pullToRefresh modifier will depend on your needs. If you require the option to enable/disable the pull-to-refresh gesture, then you will have to use the pullToRefresh modifier. If not, you can simply use PullToRefreshBox.

Key changes:

  • rememberPullToRefreshState(): no longer accepts an enabled argument, it can be controlled through the pullToRefresh modifier
  • PullToRefreshContainer: replaced by PullToRefreshBox or pullToRefresh modifier
  • isRefreshing state is controlled by the user instead of PullToRefreshState
Implementation of pull-to-refresh with the now deprecated PullToRefreshContainer

Conclusion

We’ve taken a look at how to implement a simple pull-to-refresh mechanism in your app using the latest Compose Material 3 library.

The APIs are experimental and can be used in two ways: by applying a wrapper layout or by applying a modifier. Which one you select depends on your requirements, such as custom animations or control over enabling and disabling the gesture.

You can find the full implementation example of pull-to-refresh in my sample project on GitHub.

Resources:

--

--