1. Introduction
This is a codelab that demonstrates advanced usage of the Paging Library. If you're new to pagination as a concept, or to the Paging Library as a whole, check out the Paging basics codelab.
What you'll learn
- What the main components of Paging 3 are.
- How to add Paging 3 to your project.
- How to add a header or footer to your list using the Paging 3 API.
- How to add list separators using the Paging 3 API.
- How to page from network and database.
What you'll build
In this codelab, you start with a sample app that already displays a list of GitHub repositories. Whenever the user scrolls to the end of the displayed list, a new network request is triggered and its result is displayed on the screen.
You will add code through a series of steps, to achieve the following:
- Migrate to the Paging library components.
- Add a loading status header and footer to your list.
- Show loading progress between every new repository search.
- Add separators in your list.
- Add database support for paging from network and database.
Here's what your app will look like in the end:

What you'll need
- Android Studio Arctic Fox.
- Familiarity with the following Architecture Components: LiveData, ViewModel, View. Binding and with the architecture suggested in the " Guide to App Architecture".
- Familiarity with coroutines and Kotlin Flow.
For an introduction to Architecture Components, check out the Room with a View codelab. For an introduction to Flow, check out the Advanced Coroutines with Kotlin Flow and LiveData codelab.
2. Setup Your Environment
In this step, you will download the code for the entire codelab and then run a simple example app.
To get you started as quickly as possible, we have prepared a starter project for you to build on.
If you have git installed, you can simply run the command below. (You can check by typing git --version in the terminal / command line and verify it executes correctly.)
git clone https://github.com/android/codelab-android-paging
The code is inside the /advanced folder. Open the start project.
The end project contains the code that you should end with, so feel free to check it out if you are stuck.
If you do not have git, you can click the following button to download all the code for this codelab:
- Unzip the code, and then open the project Android Studio.
- Run the
apprun configuration on a device or emulator.

The app runs and displays a list of GitHub repositories similar to this one:

3. Project overview
The app allows you to search GitHub for repositories whose name or description contains a specific word. The list of repositories is displayed in descending order based on the number of stars, then alphabetically by name.
The app follows the architecture recommended in the " Guide to app architecture". Here's what you will find in each package:
- api - Github API calls, using Retrofit.
- data - the repository class, responsible for triggering API requests and caching the responses in memory.
- model - the
Repodata model, which is also a table in the Room database; andRepoSearchResult, a class that is used by the UI to observe both search results data and network errors. - ui - classes related to displaying an
Activitywith aRecyclerView.
The GithubRepository class retrieves the list of repository names from the network every time the user scrolls towards the end of the list, or when the user searches for a new repository. The list of results for a query is kept in memory in the GithubRepository in a ConflatedBroadcastChannel and exposed as a Flow.
SearchRepositoriesViewModel requests the data from GithubRepository and exposes it to the SearchRepositoriesActivity. Because we want to ensure that we're not requesting the data multiple times on configuration change (e.g. rotation), we're converting the Flow to LiveData in the ViewModel using the liveData() builder method. That way, the LiveData caches the latest list of results in memory, and when the SearchRepositoriesActivity gets recreated, the content of the LiveData will be displayed on the screen. The ViewModel exposes:
- A
LiveData<UiState> - A function
(UiAction) -> Unit
The UiState is a representation of everything needed to render the app's UI, with different fields corresponding to different UI components. It is an immutable object, which means it can't be changed; however, new versions of it can be produced and observed by the UI. In our case, new versions of it are produced as a result of the user's actions: either searching for a new query, or scrolling the list to fetch more.
The user actions are aptly represented by the UiAction type. Enclosing the API for interactions to the ViewModel in a single type has the following benefits:
- Small API surface: Actions can be added, removed, or changed, but the method signature of the
ViewModelnever changes. This makes refactoring local and less likely to leak abstractions or interface implementations. - Easier concurrency management: As you'll see later in the codelab, it's important to be able to guarantee the execution order of certain requests. By typing the API strongly with
UiAction, we can write code with strict requirements for what can happen, and when it can happen.
From a usability perspective, we have the following issues:
- The user has no information on the list loading state: they see an empty screen when they search for a new repository or just an abrupt end of the list while more results for the same query are being loaded.
- The user can't retry a failed query.
- The list always scrolls to the top after orientation changes or after process death.
From an implementation perspective, we have the following issues:
- The list grows unbounded in memory, wasting memory as the user scrolls.
- We have to convert our results from
FlowtoLiveDatato cache them, increasing the complexity of our code. - If our app needed to show multiple lists, we'd see that there is a lot of boilerplate to write for each list.
Let's find out how the Paging library can help us with these issues and what components it includes.
4. Paging library components
The Paging library makes it easier for you to load data incrementally and gracefully within your app's UI. The Paging API provides support for many of the functionalities that you would otherwise need to implement manually when you need to load data in pages:
- Keeps track of the keys to be used for retrieving the next and previous page.
- Automatically requests the correct page when the user has scrolled to the end of the list.
- Ensures that multiple requests aren't triggered at the same time.
- Allows you to cache data: if you're using Kotlin, this is done in a
CoroutineScope; if you're using Java, this can be done withLiveData. - Tracks loading state and allows you to display it in a
RecyclerViewlist item or elsewhere in your UI, and easily retry failed loads. - Allows you to execute common operations like
maporfilteron the list that will be displayed, independently of whether you're usingFlow,LiveData, or RxJavaFlowableorObservable. - Provides an easy way of implementing list separators.
The Guide to app architecture proposes an architecture with the following main components:
- A local database that serves as a single source of truth for data presented to the user and manipulated by the user.
- A web API service.
- A repository that works with the database and the web API service, providing a unified data interface.
- A
ViewModelthat provides data specific to the UI. - The UI, which shows a visual representation of the data in the
ViewModel.
The Paging library works with all of these components and coordinates the interactions between them, so that it can load "pages" of content from a data source and display that content in the UI.
This codelab introduces you to the Paging library and its main components:
PagingData- a container for paginated data. Each refresh of data will have a separate correspondingPagingData.PagingSource- aPagingSourceis the base class for loading snapshots of data into a stream ofPagingData.Pager.flow- builds aFlow<PagingData>, based on aPagingConfigand a function that defines how to construct the implementedPagingSource.PagingDataAdapter- aRecyclerView.Adapterthat presentsPagingDatain aRecyclerView. ThePagingDataAdaptercan be connected to a KotlinFlow, aLiveData, an RxJavaFlowable, or an RxJavaObservable. ThePagingDataAdapterlistens to internalPagingDataloading events as pages are loaded and usesDiffUtilon a background thread to compute fine-grained updates as updated content is received in the form of newPagingDataobjects.RemoteMediator- helps implement pagination from network and database.
In this codelab, you will implement examples of each of the components described above.
5. Define the source of data
The PagingSource implementation defines the source of data and how to retrieve data from that source. The PagingData object queries data from the PagingSource in response to loading hints that are generated as the user scrolls in a RecyclerView.
Currently, the GithubRepository has a lot of the responsibilities of a data source that the Paging library will handle once we're done adding it:
- Loads the data from
GithubService, ensuring that multiple requests aren't triggered at the same time. - Keeps an in-memory cache of the retrieved data.
- Keeps track of the page to be requested.
To build the PagingSource you need to define the following:
- The type of the paging key - in our case, the Github API uses 1-based index numbers for pages, so the type is
Int. - The type of data loaded - in our case, we're loading
Repoitems. - Where is the data retrieved from - we're getting the data from
GithubService. Our data source will be specific to a certain query, so we need to make sure we're also passing the query information toGithubService.
So, in the data package, let's create a PagingSource implementation called GithubPagingSource:
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
TODO("Not yet implemented")
}
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
TODO("Not yet implemented")
}
}
We'll see that PagingSource requires us to implement two functions: load() and getRefreshKey().
The load() function will be called by the Paging library to asynchronously fetch more data to be displayed as the user scrolls around. The LoadParams object keeps information related to the load operation, including the following:
- Key of the page to be loaded. If this is the first time that load is called,
LoadParams.keywill benull. In this case, you will have to define the initial page key. For our project, you'll have to moveGITHUB_STARTING_PAGE_INDEXconstant fromGithubRepositoryto yourPagingSourceimplementation since this is the initial page key. - Load size - the requested number of items to load.
The load function returns a LoadResult. This will replace the usage of RepoSearchResult in our app, as LoadResult can take one of the following types:
LoadResult.Page, if the result was successful.LoadResult.Error, in case of error.
When constructing the LoadResult.Page, pass null for nextKey or prevKey if the list can't be loaded in the corresponding direction. For example, in our case, we could consider that if the network response was successful but the list was empty, we don't have any data left to be loaded; so the nextKey can be null.
Based on all of this information, we should be able to implement the load() function!
Next we need to implement getRefreshKey(). The refresh key is used for subsequent refresh calls to PagingSource.load() (the first call is initial load which uses initialKey provided by Pager). A refresh happens whenever the Paging library wants to load new data to replace the current list, e.g., on swipe to refresh or on invalidation due to database updates, config changes, process death, etc. Typically, subsequent refresh calls will want to restart loading data centered around PagingState.anchorPosition which represents the most recently accessed index.
The GithubPagingSource implementation looks like this:
// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
private const val GITHUB_STARTING_PAGE_INDEX = 1
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
return try {
val response = service.searchRepos(apiQuery, position, params.loadSize)
val repos = response.items
val nextKey = if (repos.isEmpty()) {
null
} else {
// initial load size = 3 * NETWORK_PAGE_SIZE
// ensure we're not requesting duplicating items, at the 2nd request
position + (params.loadSize / NETWORK_PAGE_SIZE)
}
LoadResult.Page(
data = repos,
prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
nextKey = nextKey
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
// The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
// We need to get the previous key (or next key if previous is null) of the page
// that was closest to the most recently accessed index.
// Anchor position is the most recently accessed index
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
6. Build and configure PagingData
In our current implementation, we use a Flow<RepoSearchResult> in the GitHubRepository to get the data from the network and pass it to the ViewModel. The ViewModel then transforms it into a LiveData and exposes it to the UI. Whenever we get to the end of the displayed list and more data is loaded from the network, the Flow<RepoSearchResult> will contain the entire list of previously retrieved data for that query in addition to the latest data.
RepoSearchResult encapsulates both the success and error cases. The success case holds the repository data. The error case contains the Exception reason. With Paging 3 we don't need the RepoSearchResult anymore, as the library models both the success and error cases with LoadResult. Feel free to delete RepoSearchResult as in the next few steps we'll replace it.
To construct the PagingData, we first need to decide what API we want to use to pass the PagingData to other layers of our app:
- Kotlin
Flow- usePager.flow. LiveData- usePager.liveData.- RxJava
Flowable- usePager.flowable. - RxJava
Observable- usePager.observable.
As we're already using Flow in our app, we'll continue with this approach; but instead of using Flow<RepoSearchResult>, we'll use Flow<PagingData<Repo>>.
No matter which PagingData builder you use, you'll have to pass the following parameters:
PagingConfig. This class sets options regarding how to load content from aPagingSourcesuch as how far ahead to load, the size request for the initial load, and others. The only mandatory parameter you have to define is the page size—how many items should be loaded in each page. By default, Paging will keep in memory all the pages you load. To ensure that you're not wasting memory as the user scrolls, set themaxSizeparameter inPagingConfig. By default Paging will return null items as a placeholder for content that is not yet loaded if Paging can count the unloaded items and if theenablePlaceholdersconfig flag is true. Like this, you will be able to display a placeholder view in your adapter. To simplify the work in this codelab, let's disable the placeholders by passingenablePlaceholders = false.- A function that defines how to create the
PagingSource. In our case, we'll create a newGithubPagingSourcefor each new query.
Let's modify our GithubRepository!
Update GithubRepository.getSearchResultStream
- Remove the
suspendmodifier. - Return
Flow<PagingData<Repo>>. - Construct
Pager.
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false
),
pagingSourceFactory = { GithubPagingSource(service, query) }
).flow
}
Cleanup GithubRepository
Paging 3 does a lot of things for us:
- Handles in-memory cache.
- Requests data when the user is close to the end of the list.
This means that everything else in our GithubRepository can be removed, except getSearchResultStream and the companion object where we defined the NETWORK_PAGE_SIZE. Your GithubRepository should now look like this:
class GithubRepository(private val service: GithubService) {
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false
),
pagingSourceFactory = { GithubPagingSource(service, query) }
).flow
}
companion object {
const val NETWORK_PAGE_SIZE = 50
}
}
You should now have compile errors in the SearchRepositoriesViewModel. Let's see what changes need to be made there!
7. Request and cache PagingData in the ViewModel
Before addressing the compile errors, let's review the types in the ViewModel:
sealed class UiAction {
data class Search(val query: String) : UiAction()
data class Scroll(
val visibleItemCount: Int,
val lastVisibleItemPosition: Int,
val totalItemCount: Int
) : UiAction()
}
data class UiState(
val query: String,
val searchResult: RepoSearchResult
)
In our UiState we expose a searchResult; the role of searchResult is to be an in-memory cache for result searches that survives configuration changes. With Paging 3 we don't need to convert our Flow to LiveData anymore. Instead, SearchRepositoriesViewModel will now expose a StateFlow<UiState>. Furthermore, we drop the searchResult val entirely, opting instead to expose a separate Flow<PagingData<Repo>> that serves the same purpose that searchResult did.
PagingData is a self-contained type that contains a mutable stream of updates to the data to be displayed in the RecyclerView. Each emission of PagingData is completely independent, and multiple PagingData may be emitted for a single query. As such, Flows of PagingData should be exposed independent of other Flows.
Furthermore, as a user experience perk, for every new query entered, we want to scroll to the top of the list to show the first search result. However, as paging data may be emitted multiple times we only want to scroll to the top of the list if the user has not begun scrolling.
To do this, let's update UiState and add fields for the lastQueryScrolled and hasNotScrolledForCurrentSearch. These flags will prevent us from scrolling to the top of the list when we shouldn't:
data class UiState(
val query: String = DEFAULT_QUERY,
val lastQueryScrolled: String = DEFAULT_QUERY,
val hasNotScrolledForCurrentSearch: Boolean = false
)
Let's revisit our architecture. Because all requests to the ViewModel go through a single entry point - the accept field defined as a (UiAction) -> Unit - we need to do the following:
- Convert that entry point into streams containing types we're interested in.
- Transform those streams.
- Combine the streams back into a
StateFlow<UiState>.
In more functional terms, we are going to reduce emissions of UiAction into UiState. It's sort of like an assembly line: the UiAction types are the raw materials that come in, they cause effects (sometimes called mutations), and the UiState is the finished output ready to be bound to the UI. This is sometimes called making the UI a function of the UiState.
Let's rewrite the ViewModel to handle each UiAction type in two different streams, and then transform them into a StateFlow<UiState> using a few Kotlin Flow operators.
First we update the definitions for state in the ViewModel to use a StateFlow instead of a LiveData while also adding a field for exposing a Flow of PagingData:
/**
* Stream of immutable states representative of the UI.
*/
val state: StateFlow<UiState>
val pagingDataFlow: Flow<PagingData<Repo>>
Next, we update the definition for the UiAction.Scroll subclass:
sealed class UiAction {
...
data class Scroll(val currentQuery: String) : UiAction()
}
Notice that we removed all of the fields in the UiAction.Scroll data class and replaced them with the single currentQuery string. This lets us associate a scroll action with a particular query. We also delete the shouldFetchMore extension because it is no longer used. This is also something that needs to be restored after process death, so we make sure we update the onCleared() method in the SearchRepositoriesViewModel:
class SearchRepositoriesViewModel{
...
override fun onCleared() {
savedStateHandle[LAST_SEARCH_QUERY] = state.value.query
savedStateHandle[LAST_QUERY_SCROLLED] = state.value.lastQueryScrolled
super.onCleared()
}
}
// This is outside the ViewModel class, but in the same file
private const val LAST_QUERY_SCROLLED: String = "last_query_scrolled"
This is also a good time to introduce the method that will actually create the pagingData Flow from the GithubRepository:
class SearchRepositoriesViewModel(
...
) : ViewModel() {
override fun onCleared() {
...
}
private fun searchRepo(queryString: String): Flow<PagingData<Repo>> =
repository.getSearchResultStream(queryString)
}
Flow<PagingData> has a handy cachedIn() method that allows us to cache the content of a Flow<PagingData> in a CoroutineScope. Since we're in a ViewModel, we will use the androidx.lifecycle.viewModelScope.
Now, we can start converting the accept field in the ViewModel into a UiAction stream. Replace the init block of SearchRepositoriesViewModel with the following:
class SearchRepositoriesViewModel(
...
) : ViewModel() {
...
init {
val initialQuery: String = savedStateHandle.get(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY
val lastQueryScrolled: String = savedStateHandle.get(LAST_QUERY_SCROLLED) ?: DEFAULT_QUERY
val actionStateFlow = MutableSharedFlow<UiAction>()
val searches = actionStateFlow
.filterIsInstance<UiAction.Search>()
.distinctUntilChanged()
.onStart { emit(UiAction.Search(query = initialQuery)) }
val queriesScrolled = actionStateFlow
.filterIsInstance<UiAction.Scroll>()
.distinctUntilChanged()
// This is shared to keep the flow "hot" while caching the last query scrolled,
// otherwise each flatMapLatest invocation would lose the last query scrolled,
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
replay = 1
)
.onStart { emit(UiAction.Scroll(currentQuery = lastQueryScrolled)) }
}
}
Let's go over the code snippet above. We start with two items, the initialQuery String, pulled from saved state or a default, along with lastQueryScrolled, a String representing the last searched term where the user has interacted with the list. Next we begin to split the Flow into specific UiAction types:
UiAction.Searchfor each time the user enters a particular query.UiAction.Scrollfor each time the user scrolls the list with a particular query in focus.
The UiAction.Scroll Flow has some extra transformations applied to it. Let's go over them:
shareIn: This is needed because when thisFlowis ultimately consumed, it is consumed using aflatmapLatestoperator. Each time the upstream emits,flatmapLatestwill cancel the lastFlowit was operating on, and start working based on the new flow it was given. In our case, this would make us lose the value of the last query the user has scrolled through. So, we use theFlowoperator with areplayvalue of 1 to cache the last value so that it isn't lost when a new query comes in.onStart: Also used for caching. If the app was killed, but the user had already scrolled through a query, we don't want to scroll the list to the top causing them to lose their place again.
There should still be compile errors because we have not defined the state, pagingDataFlow , and accept fields yet. Let's fix that. With the transformations applied to each UiAction, we can now use them to create flows for both our PagingData and the UiState.
init {
...
pagingDataFlow = searches
.flatMapLatest { searchRepo(queryString = it.query) }
.cachedIn(viewModelScope)
state = combine(
searches,
queriesScrolled,
::Pair
).map { (search, scroll) ->
UiState(
query = search.query,
lastQueryScrolled = scroll.currentQuery,
// If the search query matches the scroll query, the user has scrolled
hasNotScrolledForCurrentSearch = search.query != scroll.currentQuery
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = UiState()
)
accept = { action ->
viewModelScope.launch { actionStateFlow.emit(action) }
}
}
}
We use the flatmapLatest operator on the searches flow because each new search query requires a new Pager to be created. Next, we apply the cachedIn operator to the PagingData flow to keep it active within the viewModelScope and assign the result to the pagingDataFlow field. On the UiState side of things, we use the combine operator to populate the required UiState fields and assign the resulting Flow to the exposed state field. We also define accept as a lambda that launches a suspending function that feeds our state machine.
That's it! We now have a functional ViewModel from both a literal and reactive programming point of view!
8. Make the Adapter work with PagingData
To bind a PagingData to a RecyclerView, use a PagingDataAdapter. The PagingDataAdapter gets notified whenever the PagingData content is loaded and then it signals the RecyclerView to update.
Update the ui.ReposAdapter to work with a PagingData stream:
- Right now,
ReposAdapterimplementsListAdapter. Make it implementPagingDataAdapterinstead. The rest of the class body remains unchanged:
class ReposAdapter : PagingDataAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
// body is unchanged
}
We've been making a lot of changes so far, but now we're just one step away from being able to run the app—we just need to connect the UI!
9. Trigger network updates
Replace LiveData with Flow
Let's update SearchRepositoriesActivity to work with Paging 3. To be able to work with Flow<PagingData>, we need to launch a new coroutine. We will do that in the lifecycleScope, which is responsible for canceling the request when the activity is recreated.
Fortunately, we needn't change much. Rather than observe() a LiveData, we'll instead launch() a coroutine and collect() a Flow. The UiState will be combined with the PagingAdapter LoadState Flow to give us the guarantee that we will not scroll the list back up to the top with new emissions of PagingData if the user has already scrolled.
First off, as we are now returning state as a StateFlow instead of a LiveData, all references in the Activity to LiveData should be replaced with a StateFlow, making sure to add an argument for the pagingData Flow as well. The first place is in the bindState method:
private fun ActivitySearchRepositoriesBinding.bindState(
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
uiActions: (UiAction) -> Unit
) {
...
}
This change has a cascading effect, as we now have to update bindSearch() and bindList(). bindSearch() has the smallest change, so let's start there:
private fun ActivitySearchRepositoriesBinding.bindSearch(
uiState: StateFlow<UiState>,
onQueryChanged: (UiAction.Search) -> Unit
) {
searchRepo.setOnEditorActionListener {...}
searchRepo.setOnKeyListener {...}
lifecycleScope.launch {
uiState
.map { it.query }
.distinctUntilChanged()
.collect(searchRepo::setText)
}
}
The major change here is the need to launch a coroutine, and collect the query change from the UiState Flow.
Address the scrolling issue and bind data
Now for the scrolling part. First, like the last two changes, we replace the LiveData with a StateFlow and add an argument for the pagingData Flow. With that done, we can move on to the scroll listener. Notice that previously, we used an OnScrollListener attached to the RecyclerView to know when to trigger more data. The Paging library handles list scrolling for us, but we still need the OnScrollListener as a signal for if the user has scrolled the list for the current query. In the bindList() method, let's replace setupScrollListener() with an inline RecyclerView.OnScrollListener. We also delete the setupScrollListener() method entirely.
private fun ActivitySearchRepositoriesBinding.bindList(
repoAdapter: ReposAdapter,
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
onScrollChanged: (UiAction.Scroll) -> Unit
) {
list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy != 0) onScrollChanged(UiAction.Scroll(currentQuery = uiState.value.query))
}
})
// the rest of the code is unchanged
}
Next, we set up the pipeline to create a shouldScrollToTop boolean flag. With that done, we have two flows we can collect from: The PagingData Flow and the shouldScrollToTop Flow.
private fun ActivitySearchRepositoriesBinding.bindList(
repoAdapter: ReposAdapter,
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
onScrollChanged: (UiAction.Scroll) -> Unit
) {
list.addOnScrollListener(...)
val notLoading = repoAdapter.loadStateFlow
// Only emit when REFRESH LoadState for the paging source changes.
.distinctUntilChangedBy { it.source.refresh }
// Only react to cases where REFRESH completes i.e., NotLoading.
.map { it.source.refresh is LoadState.NotLoading }
val hasNotScrolledForCurrentSearch = uiState
.map { it.hasNotScrolledForCurrentSearch }
.distinctUntilChanged()
val shouldScrollToTop = combine(
notLoading,
hasNotScrolledForCurrentSearch,
Boolean::and
)
.distinctUntilChanged()
lifecycleScope.launch {
pagingData.collectLatest(repoAdapter::submitData)
}
lifecycleScope.launch {
shouldScrollToTop.collect { shouldScroll ->
if (shouldScroll) list.scrollToPosition(0)
}
}
}
In the above, we use collectLatest on the pagingData Flow so we can cancel collection on previous pagingData emissions on new emissions of pagingData. For the shouldScrollToTop flag, the emissions of PagingDataAdapter.loadStateFlow are synchronous with what is displayed in the UI, so it's safe to immediately call list.scrollToPosition(0) as soon as the boolean flag emitted is true.
The type in a LoadStateFlow is a CombinedLoadStates object.
CombinedLoadStates allows us to get the load state for the three different types of load operations:
CombinedLoadStates.refresh- represents the load state for loading thePagingDatafor the first time.CombinedLoadStates.prepend- represents the load state for loading data at the start of the list.CombinedLoadStates.append- represents the load state for loading data at the end of the list.
In our case, we want to reset the scroll position only when the refresh has completed i.e. LoadState is refresh, NotLoading.
We can now remove binding.list.scrollToPosition(0) from updateRepoListFromInput().
With all that done, your activity should look like:
class SearchRepositoriesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivitySearchRepositoriesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
// get the view model
val viewModel = ViewModelProvider(this, Injection.provideViewModelFactory(owner = this))
.get(SearchRepositoriesViewModel::class.java)
// add dividers between RecyclerView's row items
val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
binding.list.addItemDecoration(decoration)
// bind the state
binding.bindState(
uiState = viewModel.state,
pagingData = viewModel.pagingDataFlow,
uiActions = viewModel.accept
)
}
/**
* Binds the [UiState] provided by the [SearchRepositoriesViewModel] to the UI,
* and allows the UI to feed back user actions to it.
*/
private fun ActivitySearchRepositoriesBinding.bindState(
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
uiActions: (UiAction) -> Unit
) {
val repoAdapter = ReposAdapter()
list.adapter = repoAdapter
bindSearch(
uiState = uiState,