On this article, we’ll implement caching and paging with Paging 3. We’ll be utilizing Jetpack Compose, however you too can comply with alongside and study from this text, even in the event you will not be utilizing Jetpack Compose. Apart from the UI layer, most of it is going to be related.
Desk of Contents
We’ll be utilizing Room, Retrofit, and Hilt on this article, so that you higher understand how they work.
I am going to additionally assume you understand the fundamentals of how Paging 3 works. In case you do not, I like to recommend testing this text earlier than this one.
software stage 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"
Remember 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 possibly can register and get your API key from this hyperlink. We are going to use /film/fashionable
last level
response fashions,
Put them in numerous recordsdata. I’ve put them in a code block to make it simpler to learn.
knowledge class MovieResponse(
val web page: Int,
@SerializedName(worth = "outcomes")
val motion pictures: Listing<Film>,
@SerializedName("total_pages")
val totalPages: Int,
@SerializedName("total_results")
val totalResults: Int
)@Entity(tableName = "motion pictures")
knowledge 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 might be quite simple since we’re going to use only 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’ll create an replace occasion on the finish of this half after ending the Room deployment.
That is it for Retrofit, now we are able to implement Room. Earlier than we start, we’ll have to create a brand new mannequin for caching.
@Entity(tableName = "remote_key")
knowledge 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 usually are not instantly related to record gadgets, it’s higher to retailer them in a separate desk within the native database. Though this may be carried out within the
Film
desk, creating a brand new desk for the subsequent and former distant keys related to aFilm
it permits us to have a greater separation of issues.
This mannequin is required to maintain observe of pagination. When we now have the final aspect loaded from the PagingState
, there is no such thing as a technique to know the index of the web page it belonged to. To unravel this downside, 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. In case you needn’t verify once we final cached knowledge, you’ll be able to delete it.
Now we are able to create Dao for each. Film
Y RemoteKeys
,
@Dao
interface MoviesDao
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insertAll(motion pictures: Listing<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: Listing<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 situations of Retrofit & Room.
@Module
@InstallIn(SingletonComponent::class)
class SingletonModule
@Singleton
@Offers
enjoyable provideRetrofitInstance(): MoviesApiService =
Retrofit.Builder()
.baseUrl("https://api.themoviedb.org/3/")
.addConverterFactory(GsonConverterFactory.create())
.construct()
.create(MoviesApiService::class.java)@Singleton
@Offers
enjoyable provideMovieDatabase(@ApplicationContext context: Context): MoviesDatabase =
Room
.databaseBuilder(context, MoviesDatabase::class.java, "movies_database")
.construct()
@Singleton
@Offers
enjoyable provideMoviesDao(moviesDatabase: MoviesDatabase): MoviesDao = moviesDatabase.getMoviesDao()
@Singleton
@Offers
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 applying has run out of cached knowledge. You should use this token to load extra knowledge 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 extra knowledge is required, the paging library calls the load()
methodology of the Distant Mediator implementation. This operate usually will get the brand new knowledge from a community supply and saves it to native storage.
A Distant Mediator implementation helps load paged knowledge from the community to the database, however doesn’t load knowledge on to the person interface. As a substitute, the applying makes use of the database as a supply of data. In different phrases, the app solely shows knowledge that has been cached within the database.
Now, we are able to begin implementing Distant Mediator. Let’s implement half by half. First, we’ll implement load
methodology.
@OptIn(ExperimentalPagingApi::class)
class MoviesRemoteMediator (
non-public val moviesApiService: MoviesApiService,
non-public 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 not too long ago accessed index within the record, and thePagingConfig
we outline when initializing the paging circulation.
loadType
tells us if we have to load knowledge on the finish (LoadType.APPEND) or firstly of the information (LoadType.PREPEND) that we beforehand loaded,
or if it’s the first time we’re loading knowledge (LoadType.REFRESH).
we’ll 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 aren’t any gadgets left to add, we assume it’s out of inventory.
Then we begin the database transaction. Inside it, we verify if loadType
is REFRESH
and clear caches. After that, we create RemoteKeys
by mapping motion pictures
and extract film.id
. Lastly, we cache every little thing retrieved. motion pictures
Y remoteKeys
.
Now, let’s examine how we retrieve the web page quantity with RemoteKeys
,
@OptIn(ExperimentalPagingApi::class)
class MoviesRemoteMediator (
non-public val moviesApiService: MoviesApiService,
non-public 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 applied
//...
non-public droop enjoyable getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, Film>): RemoteKeys?
return state.anchorPosition?.let place ->
state.closestItemToPosition(place)?.id?.let id ->
moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(id)
non-public droop enjoyable getRemoteKeyForFirstItem(state: PagingState<Int, Film>): RemoteKeys?
return state.pages.firstOrNull
it.knowledge.isNotEmpty()
?.knowledge?.firstOrNull()?.let film ->
moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(film.id)
non-public droop enjoyable getRemoteKeyForLastItem(state: PagingState<Int, Film>): RemoteKeys?
return state.pages.lastOrNull
it.knowledge.isNotEmpty()
?.knowledge?.lastOrNull()?.let film ->
moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(film.id)
LoadType.REFRESH, receives a name when it’s the first time we load knowledge, or when refresh()
is called.
LoadType.ANTEPEND, when we have to load knowledge to the start of the at present loaded knowledge set, the load parameter is LoadType.PREPEND
.
LoadType.APPEND, when we have to load knowledge on the finish of the at present loaded knowledge set, the load parameter is LoadType.APPEND
.
getRemoteKeyClosestToCurrentPosition
based mostly on anchorPosition
of the state, we are able to get nearer Film
merchandise to that place by calling closestItemToPosition
and get well RemoteKeys
from the database Sure RemoteKeys
is null, we return the primary web page quantity which is 1 in our instance.
getRemoteKeyForFirstItem
we 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 (
non-public val moviesApiService: MoviesApiService,
non-public 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 methodology is to verify if the cached knowledge is outdated and resolve whether or not to set off a distant replace. This methodology is executed earlier than any add is completed, so you’ll be able to manipulate the database (for instance, to delete previous knowledge) earlier than triggering any native or distant add.
In instances the place native knowledge must be absolutely 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 instances the place there is no such thing as a have to replace native knowledge, initialize
I ought to return SKIP_INITIAL_REFRESH
. This causes the Distant Mediator to skip the distant replace and cargo the cached knowledge.
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, you too can 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(
non-public val moviesApiService: MoviesApiService,
non-public 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 knowledge supply, however there are two issues you might want to do in another way:As a substitute of spending a
PagingSource
constructor instantly, it’s essential to present the question methodology that returns aPagingSource
dao object.It’s essential to present an occasion of your
RemoteMediator
implementation just like theremoteMediator
parameter.
The pagingSourceFactory
lambda ought to all the time return a brand new one PagingSource
when invoked as PagingSource
situations usually are not reusable.
Lastly, we are able to begin to implement the UI layer.
record configuration
The implementation of the record might be quite simple,
@Composable
enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()val motion pictures = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()
LazyColumn {
gadgets(
gadgets = motion pictures
) { film ->
film?.let {
Row(
horizontalArrangement = Association.Middle,
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.Middle
)
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),
coloration = MaterialTheme.colours.main,
)
Textual content(
modifier = Modifier
.padding(vertical = 18.dp, horizontal = 8.dp),
textual content = it.title
)
Divider()
}
}
}
}
For an in depth clarification of the record implementation, you’ll be able to confer with this hyperlink.
Loading and error dealing with
@Composable
enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()val motion pictures = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()
LazyColumn {
//... Film gadgets
val loadState = motion pictures.loadState.mediator
merchandise
}
}
Since we’re utilizing Distant Mediator, we’ll use loadState.mediator
. we’ll simply verify refresh
Y append
,
When refresh
is LoadState.Loading
we’ll present the loading display screen.
refresh Loading State
When append
is LoadState.Loading
we’ll present the pagination load.
For errors, we verify if refresh
both append
is LoadState.Error
. If we now have an error in refresh
which means we obtained an error within the preliminary search and can present an error display screen. If we now have an error in append
which means we obtained an error whereas paginating and we’ll present the error on the finish of the record.
Let’s have a look at the tip consequence.
That is it! I hope you’ve gotten been useful. ??
full code
MrNtlu/JetpackCompose-PaginationCaching (github.com)
Sources:
–
Caching and Pagination with Paging 3 in Android