Paging With Clean Architecture In Jetpack Compose

MohAmmad Joumani
4 min readJun 13, 2023

Introduction:
Jetpack Compose, the modern declarative UI toolkit for Android, has revolutionized the way we build user interfaces. Combined with the Clean Architecture principles, it offers a robust framework for structuring Android apps. In this article, we’ll explore how to integrate paging into a Jetpack Compose app with a Clean Architecture approach, enabling efficient data loading and smooth scrolling.

Understanding Paging:
Paging is a technique used to load and display large datasets efficiently by fetching and rendering data incrementally. It’s particularly useful when working with remote APIs or databases that return data in chunks, as it avoids loading the entire dataset at once, reducing memory consumption and improving performance.

// pagingVersion = 3.2.0-alpha06
implementation "androidx.paging:paging-runtime:$pagingVersion"
implementation 'androidx.paging:paging-compose:1.0.0-alpha20'

Integrating Paging in Jetpack Compose:
To integrate paging with Jetpack Compose, we can leverage the Paging library, a component of the Android Jetpack suite. The Paging library provides abstractions for handling pagination and efficiently loading data. Here’s how we can incorporate paging into a Jetpack Compose app using Clean Architecture principles:

  1. Define the Data Source: Create a data source implementation that interacts with the external data provider, such as a remote API or database. This data source should implement the PagingSource interface from the Paging library. It handles fetching data in pages and providing them to the application.
class MoviePagingSource(
private val remoteDataSource: MovieRemoteDataSource,
) : PagingSource<Int, Movie>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
return try {
val currentPage = params.key ?: 1
val movies = remoteDataSource.getMovies(
apiKey = Constants.MOVIE_API_KEY,
pageNumber = currentPage
)
LoadResult.Page(
data = movies.results!!.mapFromListModel(),
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (movies.results.isEmpty()) null else movies.page!! + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}

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

}

2. Implement the Repository: In the Data layer, create a repository that acts as an abstraction for data retrieval. The repository should use the data source to fetch data using the defined pagination strategy. It provides the requested data to the Domain layer.

class MovieRepositoryImpl @Inject constructor(
private val remoteDataSource: MovieRemoteDataSource
) : MovieRepository {

override suspend fun getMovies(): Flow<PagingData<Movie>> {
return Pager(
config = PagingConfig(pageSize = Constants.MAX_PAGE_SIZE, prefetchDistance = 2),
pagingSourceFactory = {
MoviePagingSource(remoteDataSource)
}
).flow
}
}

3. Manage State in the Domain Layer: The Domain layer defines use cases and business logic. It should contain a state object that represents the current loading state, error state, and the data itself. This state object should be observed by the Presentation layer for UI updates.

class GetMoviesUseCase @Inject constructor(
private val repository: MovieRepository
) : BaseUseCase<Unit, Flow<PagingData<Movie>>> {
override suspend fun execute(input: Unit): Flow<PagingData<Movie>> {
return repository.getMovies()
}
}

4. Observe the State in the Presentation Layer: In the Presentation layer, use Jetpack Compose’s collectAsState function to observe the state object provided by the Domain layer. This function ensures that the UI automatically updates whenever the state changes. Display the fetched data and handle loading and error states accordingly.

@HiltViewModel
class HomeViewModel @Inject constructor(
private val getMoviesUseCase: GetMoviesUseCase
) : ViewModel() {

private val _moviesState: MutableStateFlow<PagingData<Movie>> = MutableStateFlow(value = PagingData.empty())
val moviesState: MutableStateFlow<PagingData<Movie>> get() = _moviesState

init {
onEvent(HomeEvent.GetHome)
}

fun onEvent(event: HomeEvent) {
viewModelScope.launch {
when (event) {
is HomeEvent.GetHome -> {
getMovies()
}
}
}
}

private suspend fun getMovies() {
getMoviesUseCase.execute(Unit)
.distinctUntilChanged()
.cachedIn(viewModelScope)
.collect {
_moviesState.value = it
}
}
}

sealed class HomeEvent {
object GetHome : HomeEvent()
}
val moviePagingItems: LazyPagingItems<Movie> = viewModel.moviesState.collectAsLazyPagingItems()
LazyColumn(modifier = Modifier.padding(it)) {
items(moviePagingItems.itemCount) { index ->
Text(
text = moviePagingItems[index]!!.title,
color = MaterialTheme.colorScheme.primary
)
}
moviePagingItems.apply {
when {
loadState.refresh is LoadState.Loading -> {
item { PageLoader(modifier = Modifier.fillParentMaxSize()) }
}

loadState.refresh is LoadState.Error -> {
val error = moviePagingItems.loadState.refresh as LoadState.Error
item {
ErrorMessage(
modifier = Modifier.fillParentMaxSize(),
message = error.error.localizedMessage!!,
onClickRetry = { retry() })
}
}

loadState.append is LoadState.Loading -> {
item { LoadingNextPageItem(modifier = Modifier) }
}

loadState.append is LoadState.Error -> {
val error = moviePagingItems.loadState.append as LoadState.Error
item {
ErrorMessage(
modifier = Modifier,
message = error.error.localizedMessage!!,
onClickRetry = { retry() })
}
}
}
}
}

Integrating paging into a Jetpack Compose app with Clean Architecture principles empowers developers to build efficient and scalable applications. By adopting a modular and testable approach, we can ensure clean code, separation of concerns, and optimal performance. Jetpack Compose and the Paging library work hand in hand to deliver a seamless user experience, even with large datasets.

Api used in this article
https://api.themoviedb.org/3/
movie/

App Demo:

--

--