Effortlessly Add Pull-to-Refresh to Your Android App with Jetpack Compose

Katie Barnett
Google Developer Experts
4 min readFeb 21


Up until recently, when using Jetpack Compose you needed to use the Swipe Refresh Accompanist library to implement pull to refresh. Now, the Accompanist version has been deprecated and Swipe Refresh has morphed to Pull Refresh in Compose Material 1.3.0.

If you are migrating from the old version to the new, the above mentioned Accompanist documentation has a good guide, but follow along below if you are implementing from scratch.

How to add Pull Refresh to your app

The first step is to add the Jetpack Compose Material library to your build.gradle file.

Note — as of writing there is a bug with the Pull Refresh library where the indicator does not reset to the top of the view after the refresh occurs (and appears to get stuck), to fix this you cannot use the current stable version of the Material library (1.3.1 or Jetpack Compose Bill of Materials (BOM) 2023.01.00 ). Instead, use the beta version 1.4.0-beta01:

If you cannot use the 1.4.0-beta01 version of the Material library, check the bug responses for some suggestions on how to fix the issue.

Keeping track of the refresh

Next, you want to add a isRefreshing flag to your ViewModel to track when the refresh action has started and finished. You can do this with a Kotlin Flow as I have below, a LiveData or simple boolean variable. Using a Flow or LiveData is better as it will enable your UI to react to the change.

You also need a refresh method to call when the pull to refresh happens, this will likely be the same method that initially loads your data. Make sure the last thing you do is set your isRefreshing flag to false when the data fetch has completed.

Using Kotlin Flows and a simple data service fetch, this is as follows:

If you are chaining together multiple data fetch calls, then use an asyncblock and make sure to set the isRefreshing flag after the async so that it is only set to true when all calls are completed (I don’t really need it here as I just have one call, but I have added it above for illustration purposes).

Collecting states

In the screen composable you need to keep track of the refreshing StateFlow using collectAsStateWithLifecycle. Using collectAsStateWithLifecycle will allow your app to be more efficient by collecting flows in a lifecycle-aware manner (check out this great deep-dive article on this API). If you are using LiveData you can observe this using your choice of method.

The next state to keep track of is the pull refresh state, this requires the refreshing state and a lambda method that will be called when the pull to refresh action is called.

Finally, you should also collect your data state.

Implementing the Pull to Refresh action

This is the part that majorly differs from the original Accompanist Swipe Refresh, in that original implementation a SwipeRefresh layout was wrapped around the UI layout, similar to how it was done with androidx.swiperefreshlayout.widget.SwipeRefreshLayout in view based UIs.

Now, this work is done using the pullRefresh modifier on your containing layout, passing in the pullRefreshState .

You can use a layout that naturally scrolls like a LazyColumn or you can use a layout that does not, such as a Column or a Box . To use a non-scrolling layout you must use a verticalScroll modifier and remember the scroll state so that the pull action is recognised.

Finally, the PullRefreshIndicator is then added

Putting the state tracking and this UI together:

Pull down to get a new fox!

If you use a naturally scrolling component such as a LazyColumn there is no need to add the verticalScroll modifier:

All the cute foxes!

To see some examples of the code above, check out my Github Experiments repository here:

Fox images found via Randomfox.



Katie Barnett
Google Developer Experts

Android GDE | Android Evangelist @ Bilue | katiebarnett.dev