Caching and Pagination with Paging 3 in Android | Saga Tech

virtually Caching and Pagination with Paging 3 in Android will cowl the newest and most present advice on this space the world. admittance slowly thus you perceive capably and appropriately. will development your information skillfully and reliably

On this article, we’ll implement caching and paging with Paging 3. We’ll be utilizing Jetpack Compose, however it’s also possible to comply with alongside and study from this text, even when you will not be utilizing Jetpack Compose. Apart from the UI layer, most of it is going to be comparable.

Desk of Contents

We’ll be utilizing Room, Retrofit, and Hilt on this article, so that you higher understand how they work.

I will additionally assume you recognize the fundamentals of how Paging 3 works. Should you do not, I like to recommend trying out this text earlier than this one.

utility degree construct.gradle proceedings,

//Paging 3
def paging_version = "3.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"
implementation "androidx.paging:paging-compose:1.0.0-alpha17"

//Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

//Hilt
def hilt_version = "2.44"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"

//Room
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-paging:$room_version"

//Coil
implementation "io.coil-kt:coil-compose:2.2.2"

Do not forget so as to add web permission in AndroidManifest.xml,

<uses-permission android:identify="android.permission.INTERNET" />

We’re going to use model 3 of TheMovieDB API. You’ll be able to register and get your API key from this hyperlink. We’ll use /film/fashionable remaining level

API key

response fashions,

Put them in several information. I’ve put them in a code block to make it simpler to learn.

information class MovieResponse(
val web page: Int,
@SerializedName(worth = "outcomes")
val motion pictures: Record<Film>,
@SerializedName("total_pages")
val totalPages: Int,
@SerializedName("total_results")
val totalResults: Int
)

@Entity(tableName = "motion pictures")
information class Film(
@PrimaryKey(autoGenerate = false)
val id: Int,
@ColumnInfo(identify = "original_title")
@SerializedName("original_title")
val ogTitle: String,
@ColumnInfo(identify = "overview")
val overview: String,
@ColumnInfo(identify = "reputation")
val reputation: Double,
@ColumnInfo(identify = "poster_path")
@SerializedName("poster_path")
val posterPath: String?,
@ColumnInfo(identify = "release_date")
@SerializedName("release_date")
val releaseDate: String,
@ColumnInfo(identify = "title")
val title: String,
@ColumnInfo(identify = "web page")
var web page: Int,
)

That is all for this half.

Let’s begin by creating and implementing Retrofit. The API service can be quite simple since we’re going to use just one endpoint.

interface MoviesApiService 
@GET("film/fashionable?api_key=$MOVIE_API_KEY&language=en-US")
droop enjoyable getPopularMovies(
@Question("web page") web page: Int
): MovieResponse

The API service is prepared, we are going to create an replace occasion on the finish of this half after ending the Room deployment.

That is it for Retrofit, now we will implement Room. Earlier than we start, we’ll have to create a brand new mannequin for caching.

@Entity(tableName = "remote_key")
information class RemoteKeys(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(identify = "movie_id")
val movieID: Int,
val prevKey: Int?,
val currentPage: Int,
val nextKey: Int?,
@ColumnInfo(identify = "created_at")
val createdAt: Lengthy = System.currentTimeMillis()
)

WWhen distant keys are usually not immediately related to checklist objects, it’s higher to retailer them in a separate desk within the native database. Though this may be completed within the Film desk, creating a brand new desk for the subsequent and former distant keys related to a Film it permits us to have a greater separation of issues.

This mannequin is required to maintain monitor of pagination. When now we have the final component loaded from the PagingState, there is no such thing as a method to know the index of the web page it belonged to. To unravel this drawback, we added one other desk that shops the subsequent, present, and former web page keys for every film. The keys are web page numbers. createdAt it’s wanted for the cache timeout. Should you need not test after we final cached information, you’ll be able to delete it.

Now we will create Dao for each. Film Y RemoteKeys,

@Dao
interface MoviesDao
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insertAll(motion pictures: Record<Film>)

@Question("Choose * From motion pictures Order By web page")
enjoyable getMovies(): PagingSource<Int, Film>

@Question("Delete From motion pictures")
droop enjoyable clearAllMovies()

@Dao
interface RemoteKeysDao
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insertAll(remoteKey: Record<RemoteKeys>)

@Question("Choose * From remote_key The place movie_id = :id")
droop enjoyable getRemoteKeyByMovieID(id: Int): RemoteKeys?

@Question("Delete From remote_key")
droop enjoyable clearRemoteKeys()

@Question("Choose created_at From remote_key Order By created_at DESC LIMIT 1")
droop enjoyable getCreationTime(): Lengthy?

Lastly, we have to create the database class.

@Database(
entities = [Movie::class, RemoteKeys::class],
model = 1,
)
summary class MoviesDatabase: RoomDatabase()
summary enjoyable getMoviesDao(): MoviesDao
summary enjoyable getRemoteKeysDao(): RemoteKeysDao

That is it. Now we’re going to create cases of Retrofit & Room.

@Module
@InstallIn(SingletonComponent::class)
class SingletonModule
@Singleton
@Supplies
enjoyable provideRetrofitInstance(): MoviesApiService =
Retrofit.Builder()
.baseUrl("https://api.themoviedb.org/3/")
.addConverterFactory(GsonConverterFactory.create())
.construct()
.create(MoviesApiService::class.java)

@Singleton
@Supplies
enjoyable provideMovieDatabase(@ApplicationContext context: Context): MoviesDatabase =
Room
.databaseBuilder(context, MoviesDatabase::class.java, "movies_database")
.construct()

@Singleton
@Supplies
enjoyable provideMoviesDao(moviesDatabase: MoviesDatabase): MoviesDao = moviesDatabase.getMoviesDao()

@Singleton
@Supplies
enjoyable provideRemoteKeysDao(moviesDatabase: MoviesDatabase): RemoteKeysDao = moviesDatabase.getRemoteKeysDao()

Earlier than we begin implementing, let’s attempt to perceive what Distant Mediator is and why we’d like it.

Distant Mediator acts as a sign to the paging library when the appliance has run out of cached information. You need to use this token to load further information from the community and retailer it within the native database, the place a PagingSource you’ll be able to load it and supply it to the UI to show.

When further information is required, the paging library calls the load() technique of the Distant Mediator implementation. This operate usually will get the brand new information from a community supply and saves it to native storage.

A Distant Mediator implementation helps load paged information from the community to the database, however doesn’t load information on to the person interface. As an alternative, the appliance makes use of the database as a supply of data. In different phrases, the app solely shows information that has been cached within the database.

Now, we will begin implementing Distant Mediator. Let’s implement half by half. First, we are going to implement load technique.

@OptIn(ExperimentalPagingApi::class)
class MoviesRemoteMediator (
personal val moviesApiService: MoviesApiService,
personal val moviesDatabase: MoviesDatabase,
): RemoteMediator<Int, Film>() {

override droop enjoyable load(
loadType: LoadType,
state: PagingState<Int, Film>
): MediatorResult
val web page: Int = when (loadType)
LoadType.REFRESH ->
//...

LoadType.PREPEND ->
//...

LoadType.APPEND ->
//...

strive
val apiResponse = moviesApiService.getPopularMovies(web page = web page)

val motion pictures = apiResponse.motion pictures
val endOfPaginationReached = motion pictures.isEmpty()

moviesDatabase.withTransaction
if (loadType == LoadType.REFRESH)
moviesDatabase.getRemoteKeysDao().clearRemoteKeys()
moviesDatabase.getMoviesDao().clearAllMovies()

val prevKey = if (web page > 1) web page - 1 else null
val nextKey = if (endOfPaginationReached) null else web page + 1
val remoteKeys = motion pictures.map
RemoteKeys(movieID = it.id, prevKey = prevKey, currentPage = web page, nextKey = nextKey)

moviesDatabase.getRemoteKeysDao().insertAll(remoteKeys)
moviesDatabase.getMoviesDao().insertAll(motion pictures.onEachIndexed _, film -> film.web page = web page )

return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
catch (error: IOException)
return MediatorResult.Error(error)
catch (error: HttpException)
return MediatorResult.Error(error)


}

state The parameter provides us details about the pages that had been loaded earlier than, essentially the most lately accessed index within the checklist, and the PagingConfig we outline when initializing the paging circulation.

loadType tells us if we have to load information on the finish (LoadType.APPEND) or in the beginning of the information (LoadType.PREPEND) that we beforehand loaded,
or if it’s the first time we’re loading information (LoadType.REFRESH).

we are going to implement web page attribute later, so let’s begin with the strive/catch block. First, we make an API request and get motion pictures and set up endOfPaginationReach a motion pictures.isEmpty. If there are not any objects left to add, we assume it’s out of inventory.

Then we begin the database transaction. Inside it, we test if loadType is REFRESH and clear caches. After that, we create RemoteKeys by mapping motion pictures and extract film.id. Lastly, we cache every thing retrieved. motion pictures Y remoteKeys.

Now, let’s examine how we retrieve the web page quantity with RemoteKeys,

@OptIn(ExperimentalPagingApi::class)
class MoviesRemoteMediator (
personal val moviesApiService: MoviesApiService,
personal val moviesDatabase: MoviesDatabase,
): RemoteMediator<Int, Film>()
override droop enjoyable load(
loadType: LoadType,
state: PagingState<Int, Film>
): MediatorResult
val web page: Int = when (loadType)
LoadType.REFRESH ->
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: 1

LoadType.PREPEND ->
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevKey = remoteKeys?.prevKey
prevKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)

LoadType.APPEND ->
val remoteKeys = getRemoteKeyForLastItem(state)
val nextKey = remoteKeys?.nextKey
nextKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)

strive
//Beforehand carried out
//...

personal droop enjoyable getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, Film>): RemoteKeys?
return state.anchorPosition?.let place ->
state.closestItemToPosition(place)?.id?.let id ->
moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(id)


personal droop enjoyable getRemoteKeyForFirstItem(state: PagingState<Int, Film>): RemoteKeys?
return state.pages.firstOrNull
it.information.isNotEmpty()
?.information?.firstOrNull()?.let film ->
moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(film.id)

personal droop enjoyable getRemoteKeyForLastItem(state: PagingState<Int, Film>): RemoteKeys?
return state.pages.lastOrNull
it.information.isNotEmpty()
?.information?.lastOrNull()?.let film ->
moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(film.id)


LoadType.REFRESH, receives a name when it’s the first time we load information, or when refresh() is called.

LoadType.ANTEPEND, when we have to load information to the start of the presently loaded information set, the load parameter is LoadType.PREPEND.

LoadType.APPEND, when we have to load information on the finish of the presently loaded information set, the load parameter is LoadType.APPEND.

getRemoteKeyClosestToCurrentPositionprimarily based on anchorPosition of the state, we will get nearer Film merchandise to that place by calling closestItemToPosition and recuperate RemoteKeys from the database Sure RemoteKeys is null, we return the primary web page quantity which is 1 in our instance.

getRemoteKeyForFirstItemwe get the primary Film merchandise loaded from database.

getRemoteKeyForLastItem, we get the final Film merchandise loaded from database.

Lastly, let’s implement the caching timeout,

@OptIn(ExperimentalPagingApi::class)
class MoviesRemoteMediator (
personal val moviesApiService: MoviesApiService,
personal val moviesDatabase: MoviesDatabase,
): RemoteMediator<Int, Film>()

override droop enjoyable initialize(): InitializeAction
val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)

return if (System.currentTimeMillis() - (moviesDatabase.getRemoteKeysDao().getCreationTime() ?: 0) < cacheTimeout)
InitializeAction.SKIP_INITIAL_REFRESH
else
InitializeAction.LAUNCH_INITIAL_REFRESH

//...

initialize this technique is to test if the cached information is old-fashioned and resolve whether or not to set off a distant replace. This technique is executed earlier than any add is completed, so you’ll be able to manipulate the database (for instance, to delete previous information) earlier than triggering any native or distant add.

In circumstances the place native information must be totally up to date, initialize I ought to return LAUNCH_INITIAL_REFRESH. This causes the Distant Mediator to carry out a distant replace to completely reload the information.

In circumstances the place there is no such thing as a have to replace native information, initialize I ought to return SKIP_INITIAL_REFRESH. This causes the Distant Mediator to skip the distant replace and cargo the cached information.

In our instance, we set the timeout to 1 hour and retrieve the cache time from RemoteKeys database.

That is it. you could find the RemoteMediator code right here, it’s also possible to discover the complete code on the finish of this text.

That is going to be a easy one,

const val PAGE_SIZE = 20

@HiltViewModel
class MoviesViewModel @Inject constructor(
personal val moviesApiService: MoviesApiService,
personal val moviesDatabase: MoviesDatabase,
): ViewModel()
@OptIn(ExperimentalPagingApi::class)
enjoyable getPopularMovies(): Stream<PagingData<Film>> =
Pager(
config = PagingConfig(
pageSize = PAGE_SIZE,
prefetchDistance = 10,
initialLoadSize = PAGE_SIZE,
),
pagingSourceFactory =
moviesDatabase.getMoviesDao().getMovies()
,
remoteMediator = MoviesRemoteMediator(
moviesApiService,
moviesDatabase,
)
).circulation

That is just like making a Pager from a easy community information supply, however there are two issues you’ll want to do in a different way:

As an alternative of spending a PagingSource constructor immediately, it’s essential to present the question technique that returns a PagingSource dao object.

You should present an occasion of your RemoteMediator implementation just like the remoteMediator parameter.

The pagingSourceFactory lambda ought to at all times return a brand new one PagingSource when invoked as PagingSource cases are usually not reusable.

Lastly, we will begin to implement the UI layer.

checklist configuration

The implementation of the checklist can be quite simple,

@Composable
enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()

val motion pictures = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()

LazyColumn {
objects(
objects = motion pictures
) { film ->
film?.let {
Row(
horizontalArrangement = Association.Heart,
verticalAlignment = Alignment.CenterVertically,
)
if (film.posterPath != null)
var isImageLoading by bear in mind mutableStateOf(false)

val painter = rememberAsyncImagePainter(
mannequin = "https://picture.tmdb.org/t/p/w154" + film.posterPath,
)

isImageLoading = when(painter.state)
is AsyncImagePainter.State.Loading -> true
else -> false

Field (
contentAlignment = Alignment.Heart
)
Picture(
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 3.dp)
.top(115.dp)
.width(77.dp)
.clip(RoundedCornerShape(8.dp)),
painter = painter,
contentDescription = "Poster Picture",
contentScale = ContentScale.FillBounds,
)

if (isImageLoading)
CircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 3.dp),
shade = MaterialTheme.colours.major,
)



Textual content(
modifier = Modifier
.padding(vertical = 18.dp, horizontal = 8.dp),
textual content = it.title
)

Divider()
}
}
}
}

For an in depth rationalization of the checklist implementation, you’ll be able to consult with this hyperlink.

checklist UI

Loading and error dealing with

@Composable
enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()

val motion pictures = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()

LazyColumn {
//... Film objects

val loadState = motion pictures.loadState.mediator
merchandise
if (loadState?.refresh == LoadState.Loading)
Column(
modifier = Modifier
.fillParentMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Association.Heart,
)
Textual content(
modifier = Modifier
.padding(8.dp),
textual content = "Refresh Loading"
)

CircularProgressIndicator(shade = MaterialTheme.colours.major)

if (loadState?.append == LoadState.Loading)
Field(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Heart,
)
CircularProgressIndicator(shade = MaterialTheme.colours.major)

if (loadState?.refresh is LoadState.Error
}
}

Since we’re utilizing Distant Mediator, we are going to use loadState.mediator. we’ll simply test refresh Y append,

When refresh is LoadState.Loading we are going to present the loading display screen.

refresh Loading State

When append is LoadState.Loading we are going to present the pagination load.

add Loading

For errors, we test if refresh both append is LoadState.Error. If now we have an error in refresh meaning we acquired an error within the preliminary search and can present an error display screen. If now we have an error in append meaning we acquired an error whereas paginating and we are going to present the error on the finish of the checklist.

Let’s have a look at the tip end result.

That is it! I hope you’ve gotten been useful. 👋👋

full code

MrNtlu/JetpackCompose-PaginationCaching (github.com)

Sources:

I hope the article virtually Caching and Pagination with Paging 3 in Android provides perception to you and is beneficial for calculation to your information

Caching and Pagination with Paging 3 in Android