A full guide to use Paging-3 Library along with Jetpack compose’s LazyRow, LazyColumn and LazyGrid

Atul yadav
4 min readSep 14, 2024

--

Populating infinite (Practically very long) scrolling row, column and grid has always been a point of optimization, over the time numerous things were introduced to address it. For example recycling of same ViewHolder in View System, recyclerViewPool etc. Loading data in a batch of pages instead of loading all content in one go, as user may not go till the end of list, is one of them. How was this done earlier ?

Before paging library, user had to make load data calls manually to get list of data, limited no. 10–15/batch, from data source with logic like

if(totalItem - lastVisibleItem >= thersold) {
// fetch new data
}

once data had come we had to append in the UI list and notify the adapter about this change which was very inefficient and hectic.

After the introduction of paging library user need not worry about this fetching logic as this is going to be handled by library itself and no more notifyDataset changed thanks to JetPack’s compose.

Flow of Data From Repository to UI [HLD]

here UI layer (Activity or Fragment) gets the data from Viewmodel in form of a flow(cold) and uses to display on UI. Here flow, which is a field inside viewmodel acts as bridge between ui and viewmodel, is updated inside viewmodel by Pager which essentially fetches data from repository.

Sample of UI

Now to Fetch the data from remote/local data base we need to implement three steps as depicted in above HLD diagram.

  1. UI layer (Activity /Fragment)
  2. ViewModel to survive and hold the data across the configuration
  3. Paging Source

Sample Code for Each layers are following.

UI Layer

class MainActivity : ComponentActivity() {
lateinit var viewModel: TestViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val a = mutableStateOf("Text")
viewModel = ViewModelProvider(this, TestViewModelFactory(Repository()))[TestViewModel::class.java]
setContent {
MyApplicationTheme {
val coroutineScope = rememberCoroutineScope()
val state = viewModel.mutableStateFlow.collectAsState()
if (state.value == "DataLoaded") {
Greeting(
modifier = Modifier.padding(top = 10.dp),
viewModel = viewModel,
)
}
LaunchedEffect(key1 = true) {
viewModel.attachDataSource()
coroutineScope.launch { viewModel.mutableStateFlow.value = "DataLoaded" }
}
}
}
}

}


// copmose ui for showing list of data.
@Composable
fun Greeting(modifier: Modifier, viewModel: TestViewModel) {
val a = viewModel.b.collectAsLazyPagingItems()
LazyColumn(modifier = modifier) {
items(a.itemCount) {
Box(modifier = Modifier.padding(5.dp)) {
Text(
text = (a[it].toString()),
color = Color.Black,
modifier = Modifier
.background(Color.Cyan)
.fillParentMaxWidth()
.align(Alignment.CenterEnd)
.padding(15.dp),
textAlign = TextAlign.Center
)
}
}
}
}

here in activity class users sets the UI for a list of data and makes attachDataSource() call inside Launched effects Block. After making this call it informs the ui for re-composition upon data fetch.

attachDataSource() is nothing but attaching the flow to data source which can be seen in following Viewmodel class.

Viewmodel.

class TestViewModel(private var repository: Repository) : ViewModel() {
var mutableStateFlow = MutableStateFlow("")

@Transient
var b: Flow<PagingData<String>> = flowOf()


fun attachDataSource() {
b = Pager(PagingConfig(pageSize = 15, prefetchDistance = 5, initialLoadSize = 15)) {
DataSource(repository, 8)
}.flow.cachedIn(viewModelScope)
}
}


class TestViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TestViewModel(repository) as T
}
}

In attachDataSource() method user is initializing the pager library which takes PagingConfig, initialKey, remoteMediator and pagingSourceFactory as input. PagingConfig is used for telling the library about following.

  1. pageSize: size of each pages to be fetched from data source which is passed to this pager.

2. prefetchDistance : distance between recently accessed item and total number of items to start fetching further pages.

Note: when ever we access to any item in the returned lazyPaddingItems library is notified about it internally.

operator fun get(index: Int): T? {
pagingDataPresenter[index] // this registers the value load and informs about index has been accessed now
return itemSnapshotList[index]
}

3. similarly initial load size, max Size are passed to it.

3. Paging Source

const val INITIAL_PAGE_NO = 1

class DataSource(private val repository: Repository, private val maxPage: Int) :
PagingSource<Int, String>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
return try {

val nextPage = params.key ?: INITIAL_PAGE_NO
val cardList: List<String> = repository.getItems(currPage = nextPage)
Log.d("atul_yadav", "loading data from ${nextPage}")
LoadResult.Page(
data = cardList,
prevKey = null,
nextKey = if (nextPage == maxPage) null else nextPage.inc()
)
} catch (ex: Exception) {
LoadResult.Error(Throwable("throww"))
}
}


override fun getRefreshKey(state: PagingState<Int, String>): Int? {
return state.anchorPosition
}
}

this data source class takes repository and maximum number of pages to be fetched as argument. Some more arguments can also be supplied based on need in making the api call. it extends PagingSource class and overrides two of it’s methods load and get refersh key. load method is responsible for loading the data from repository once condition passed in pager class are met. NextPage is a key(initial value for this is null hence user should set it 0 or 1) which informs about which page are to be fetched and updated for next page. getRefershKey method is responsible for assigning the unique refresh key.

--

--

Atul yadav

Passionate Android developer (Senior Executive) Viacom18 Media Private Ltd. (JioCinema)