On this article, we’ll implement caching and paging with Paging 3. We’ll be utilizing Jetpack Compose, however you can even observe alongside and be taught from this text, even should you will not be utilizing Jetpack Compose. Apart from the UI layer, most of it will likely be comparable.
Desk of Contents
We’ll be utilizing Room, Retrofit, and Hilt on this article, so that you higher know the way they work.
I am going to additionally assume you understand the fundamentals of how Paging 3 works. Should you do not, I like to recommend testing 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:title="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/well-liked
remaining level
response fashions,
Put them in several 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 films: Listing<Film>,
@SerializedName("total_pages")
val totalPages: Int,
@SerializedName("total_results")
val totalResults: Int
)@Entity(tableName = "films")
knowledge class Film(
@PrimaryKey(autoGenerate = false)
val id: Int,
@ColumnInfo(title = "original_title")
@SerializedName("original_title")
val ogTitle: String,
@ColumnInfo(title = "overview")
val overview: String,
@ColumnInfo(title = "reputation")
val reputation: Double,
@ColumnInfo(title = "poster_path")
@SerializedName("poster_path")
val posterPath: String?,
@ColumnInfo(title = "release_date")
@SerializedName("release_date")
val releaseDate: String,
@ColumnInfo(title = "title")
val title: String,
@ColumnInfo(title = "web page")
var web page: Int,
)
That is all for this half.
Let’s begin by creating and implementing Retrofit. The API service will likely be quite simple since we’re going to use only one endpoint.
interface MoviesApiService
@GET("film/well-liked?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 must create a brand new mannequin for caching.
@Entity(tableName = "remote_key")
knowledge class RemoteKeys(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(title = "movie_id")
val movieID: Int,
val prevKey: Int?,
val currentPage: Int,
val nextKey: Int?,
@ColumnInfo(title = "created_at")
val createdAt: Lengthy = System.currentTimeMillis()
)
WWhen distant keys should not immediately related to checklist gadgets, it’s higher to retailer them in a separate desk within the native database. Though this may be accomplished 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 considerations.
This mannequin is required to maintain monitor of pagination. When we have now the final ingredient loaded from the PagingState
, there isn’t any option to know the index of the web page it belonged to. To resolve 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. Should you needn’t examine after we final cached knowledge, you may delete it.
Now we will create Dao for each. Film
Y RemoteKeys
,
@Dao
interface MoviesDao
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insertAll(films: Listing<Film>)@Question("Choose * From films Order By web page")
enjoyable getMovies(): PagingSource<Int, Film>
@Question("Delete From films")
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 appliance has run out of cached knowledge. You need to use this token to load extra knowledge from the community and retailer it within the native database, the place a PagingSource
you may 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 an alternative, the appliance makes use of the database as a supply of knowledge. In different phrases, the app solely shows knowledge 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
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 ->
//...
attempt
val apiResponse = moviesApiService.getPopularMovies(web page = web page)
val films = apiResponse.films
val endOfPaginationReached = films.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 = films.map
RemoteKeys(movieID = it.id, prevKey = prevKey, currentPage = web page, nextKey = nextKey)
moviesDatabase.getRemoteKeysDao().insertAll(remoteKeys)
moviesDatabase.getMoviesDao().insertAll(films.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 offers us details about the pages that had been loaded earlier than, probably the most just lately accessed index within the checklist, 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 are going to implement web page
attribute later, so let’s begin with the attempt/catch block. First, we make an API request and get films
and set up endOfPaginationReach
a films.isEmpty
. If there are not any gadgets left to add, we assume it’s out of inventory.
Then we begin the database transaction. Inside it, we examine if loadType
is REFRESH
and clear caches. After that, we create RemoteKeys
by mapping films
and extract film.id
. Lastly, we cache all the things retrieved. films
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)
attempt
//Beforehand carried out
//...
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 known as.
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 will 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 examine 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 may manipulate the database (for instance, to delete previous knowledge) earlier than triggering any native or distant add.
In circumstances the place native knowledge 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 totally reload the information.
In circumstances the place there isn’t any must 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 will discover the RemoteMediator
code right here, you can even 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 much like making a
Pager
from a easy community knowledge supply, however there are two issues it is advisable to do otherwise:As an alternative of spending a
PagingSource
constructor immediately, it’s essential to present the question methodology that returns aPagingSource
dao object.You should present an occasion of your
RemoteMediator
implementation just like theremoteMediator
parameter.
The pagingSourceFactory
lambda ought to at all times return a brand new one PagingSource
when invoked as PagingSource
situations should not reusable.
Lastly, we will begin to implement the UI layer.
checklist configuration
The implementation of the checklist will likely be quite simple,
@Composable
enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()val films = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()
LazyColumn {
gadgets(
gadgets = films
) { 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 rationalization of the checklist implementation, you may seek advice from this hyperlink.
Loading and error dealing with
@Composable
enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()val films = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()
LazyColumn {
//... Film gadgets
val loadState = films.loadState.mediator
merchandise loadState?.append is LoadState.Error)
val isPaginatingError = (loadState.append is LoadState.Error)
}
}
Since we’re utilizing Distant Mediator, we are going to use loadState.mediator
. we’ll simply examine 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.
For errors, we examine if refresh
both append
is LoadState.Error
. If we have now an error in refresh
meaning we received an error within the preliminary search and can present an error display screen. If we have now an error in append
meaning we received 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 top end result.
That is it! I hope you may have been useful. 👋👋
full code
MrNtlu/JetpackCompose-PaginationCaching (github.com)
Sources:
–
Caching and Pagination with Paging 3 in Android