Android Kaki

Build beautiful, usable products using required Components for Android.

Paging in Android Jetpack Compose: From knowledge cache with room to show in LazyColumn | by Ugurcan Yildirim | June 2023


Ugurcan Yildirim

ProAndroidDev

In nearly each sort of cellular challenge, we cellular builders typically discover ourselves coping with paginated knowledge. It is important if the info checklist is an excessive amount of to retrieve from the server in a single name. Thus, our backend colleagues present us with an endpoint that returns a listing of knowledge in pages and expects us to know learn how to deal with it on the shopper aspect.

On this article, we are going to concentrate on learn how to fetch, cache, and show paginated knowledge utilizing the newest strategies advisable by Android as of June 2023. comply with these steps:

  • Get checklist of Pokemon knowledge in pages from a Public GraphQL API
  • Retailer fetched knowledge in native database utilizing Room
  • Use the newest pagination library parts to deal with pagination
  • Show web page gadgets intelligently (present solely what’s seen) utilizing LazyColumn

For the pattern challenge that I’ll share the GitHub repository with on the finish of the article, we are going to use hilt of sword as our dependency injection library and use Clear structure (presentation → area ← knowledge). So I’ll clarify the whole lot ranging from knowledge layer, then scroll to area layer, and end with presentation class.

This layer is the place by far most of what’s going on by way of paging and caching. So if you may get via this half, you are nearly completed.

Distant knowledge supply

As a distant knowledge supply, we are going to use a Public Pokemon GraphQL API. Versus Retrofit which is what we use to work together with REST APIs we use Apollo‘S Kotlin . shopper for the GraphQL API. It permits us to execute GraphQL queries and mechanically generate Kotlin fashions from requests and responses.

First we have to add the next traces to our module degree construct.gradle doc:

plugins {
// ...
id "com.apollographql.apollo3" model "$apollo_version"
}
apollo {
service("pokemon") {
packageName.set("dev.thunderbolt.pokemonpager.knowledge")
}
}


dependencies {
// ...
implementation "com.apollographql.apollo3:apollo-runtime:$apollo_version"
}

right here in apollo block, we set the configuration of the Apollo library. It presents many settings, all of which you’ll be able to examine via doc. For now, we simply must set the package deal title to dev.thunderbolt.pokemonpager.knowledge in order that the generated Kotlin recordsdata can be within the appropriate package deal, which is the package deal knowledge class.

Then we have to obtain the server’s schema in order that the library can generate fashions and we will write queries utilizing autocomplete. To obtain the schema we use the next command offered by Apollo:

./gradlew :app:downloadApolloSchema --endpoint=' --schema=app/src/foremost/graphql/schema.graphqls

This can obtain the server’s schema in ‘s listing app/src/foremost/graphql/schema.graphqls.

Now it is time to write our question in a file named pokemon.graphql which we created in the identical listing because the schema file.

question PokemonList(
$offset: Int!
$restrict: Int!
) {
pokemons(
offset: $offset,
restrict: $restrict
) {
nextOffset
outcomes {
id
title
picture
}
}
}

Once we construct our challenge, Apollo Kotlin will generate fashions for this question by mechanically operating a Gradle process named generateApolloSources.

Again within the Kotlin world, we are going to outline PokemonApi class to encapsulate all of our interactions with GraphQL, like this:

class PokemonApi {
non-public val BASE_URL = "https://graphql-pokeapi.graphcdn.app/graphql"


non-public val apolloClient = ApolloClient.Builder()
.serverUrl(BASE_URL)
.addHttpInterceptor(LoggingInterceptor())
.construct()


droop enjoyable getPokemonList(offset: Int, restrict: Int): PokemonListQuery.Pokemons? {
val response = apolloClient.question(
PokemonListQuery(
offset = offset,
restrict = restrict,
)
).execute()
// IF RESPONSE HAS ERRORS OR DATA IS NULL, THROW EXCEPTION
if (response.hasErrors() || response.knowledge == null) {
throw ApolloException(response.errors.toString())
}
return response.knowledge!!.pokemons
}
}

Right here, we initialize Apollo clients instance with the required configuration and implementing our operate to execute the generated Kotlin model of the question we wrote in pokemon.graphql doc. This operate is mainly offset And restrict parameters, execute the question, and if all goes properly, return the question response, which is once more mechanically generated by Apollo.

Native knowledge supply/Storage

To retailer relational knowledge domestically and create offline most popular purposes, we are going to rely on Roomis an Android persistence library written on SQLite.

First we have to add the Room dependencies in construct.gradle doc:

dependencies {
// ...
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
}

Then we are going to outline two entity lessons, one to retailer Pokemon knowledge in our database and one other to maintain monitor of what number of pages can be fetched subsequent.

@Entity("pokemon")
knowledge class PokemonEntity(
@PrimaryKey val id: Int,
val title: String,
val imageUrl: String,
)
@Entity("remote_key")
knowledge class RemoteKeyEntity(
@PrimaryKey val id: String,
val nextOffset: Int,
)

Relating to these, we additionally want two DAO (Information Entry Object) to determine all of our database interactions therein.

@Dao
interface PokemonDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insertAll(gadgets: Record)
@Question("SELECT * FROM pokemon")
enjoyable pagingSource(): PagingSource


@Question("DELETE FROM pokemon")
droop enjoyable clearAll()
}


@Dao
interface RemoteKeyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insert(merchandise: RemoteKeyEntity)


@Question("SELECT * FROM remote_key WHERE id = :id")
droop enjoyable getById(id: String): RemoteKeyEntity?


@Question("DELETE FROM remote_key WHERE id = :id")
droop enjoyable deleteById(id: String)
}

Right here, the essential operate we have to take note of is pagingSource() one. Room can return knowledge checklist as PagingSourceallow us to Pager object (which we are going to create later) will use it as the only supply to create the stream PagingData.

Lastly, we have to have a RoomDatabase the category creates tables for these entities within the native database and offers DAOs to work together with the tables.

@Database(
entities = [PokemonEntity::class, RemoteKeyEntity::class],
model = 1,
)
summary class PokemonDatabase : RoomDatabase() {
summary val pokemonDao: PokemonDao
summary val remoteKeyDao: RemoteKeyDao
}

each of those PokemonDatabase and predefined PokemonApi lessons are instantiated and offered as singleton cases by our Hilt module knowledge class.

@Module
@InstallIn(SingletonComponent::class)
class DataModule {
@Gives
@Singleton
enjoyable providePokemonDatabase(@ApplicationContext context: Context): PokemonDatabase {
return Room.databaseBuilder(
context,
PokemonDatabase::class.java,
"pokemon.db",
).fallbackToDestructiveMigration().construct()
}


@Gives
@Singleton
enjoyable providePokemonApi(): PokemonApi {
return PokemonApi()
}


// ...
}

Distant mediator

Now it is time to do it RemoteMediator can be liable for loading the paginated knowledge from the distant API into the native database every time wanted. It is very important notice that the distant mediator doesn’t present knowledge on to the person interface. If the paginated knowledge is exhausted, the paging library will set off a distant mediator load(…) strategies to fetch and retailer extra knowledge domestically. Due to this fact, our native database can all the time be the one supply of fact.

class PokemonRemoteMediator @Inject constructor(
non-public val pokemonDatabase: PokemonDatabase,
non-public val pokemonApi: PokemonApi,
) : RemoteMediator() {
non-public val REMOTE_KEY_ID = "pokemon"


override droop enjoyable load(
loadType: LoadType,
state: PagingState,
): MediatorResult {
return strive {
val offset = when (loadType) {
LoadType.REFRESH -> 0
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND ->
}
// MAKE API CALL
val apiResponse = pokemonApi.getPokemonList(
offset = offset,
restrict = state.config.pageSize,
)
val outcomes = apiResponse?.outcomes ?: emptyList()
val nextOffset = apiResponse?.nextOffset ?: 0
// SAVE RESULTS AND NEXT OFFSET TO DATABASE
pokemonDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
// IF REFRESHING, CLEAR DATABASE FIRST
pokemonDatabase.pokemonDao.clearAll()
pokemonDatabase.remoteKeyDao.deleteById(REMOTE_KEY_ID)
}
pokemonDatabase.pokemonDao.insertAll(
outcomes.mapNotNull { it?.toPokemonEntity() }
)
pokemonDatabase.remoteKeyDao.insert(
RemoteKeyEntity(
id = REMOTE_KEY_ID,
nextOffset = nextOffset,
)
)
}
// CHECK IF END OF PAGINATION REACHED
MediatorResult.Success(endOfPaginationReached = outcomes.measurement < state.config.pageSize)
} catch (e: ApolloException) {
MediatorResult.Error(e)
}
}
}

inside load(…) operate, we have to examine first what sort of load we’re coping with. If LoadType To be…

  • REFRESH, it means we’re on preliminary load or knowledge is by some means disabled and we have to fetch knowledge from scratch. So if that is so, we put offset worth to “0” as a result of we need to fetch the primary knowledge web page.
  • PREPEND, we have to fetch the info web page earlier than the present web page. That is pointless within the scope of this instance as we do not need to fetch something whereas scrolling to the highest. So we’re merely again MediatorResult.Success(endOfPaginationReached = true) to point that knowledge loading will now not happen.
  • APPEND, we have to fetch the info web page after the present web page. On this case, we go and fetch the distant key object that was saved in our native database utilizing the earlier load of the info. If there isn’t a or it nextOffset worth is “0”, this implies no extra knowledge is loaded and appended. By the best way, that is how this API works. Your API could symbolize the tip of the info otherwise, so it’s worthwhile to write APPEND respective logic.

After we determine the precise worth of offsetnow it is time to make API name by this offset and in addition pageSize offered within the configuration. We’ll set the web page measurement after we create Pager object within the subsequent step.

If the API name efficiently returns a brand new web page of knowledge, we retailer the entries and in addition the following offset in our database utilizing the corresponding DAO features. Right here, we have to do all of the database interactions in a single transaction blocking, in order that if any interplay fails, no modifications can be made to the database.

Lastly, if the whole lot goes properly after calling the database, we are going to return a MediatorResult.Success examine if we’ve got reached the tip of pagination by evaluating the variety of gadgets returned by the newest load with the web page measurement we are going to outline within the configuration.

pager

Now, we’ll come again to ours knowledge layer hilt module once more and we are going to create Pager Factor. This object will collect all that we’ve got outlined to this point and act as a constructor for PagingData run.

@Module
@InstallIn(SingletonComponent::class)
class DataModule {
// ...


@Gives
@Singleton
enjoyable providePokemonPager(
pokemonDatabase: PokemonDatabase,
pokemonApi: PokemonApi,
): Pager {
return Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = PokemonRemoteMediator(
pokemonDatabase = pokemonDatabase,
pokemonApi = pokemonApi,
),
pagingSourceFactory = {
pokemonDatabase.pokemonDao.pagingSource()
},
)
}
}

Right here we offer three issues to the constructor of Pager. First, we arrange a PagingConfig with the specified web page measurement, as I discussed earlier than. Second, we provide a distant model of mediation. And third, we set the pagination supply offered by Room as the only knowledge supply for Pager.

Warehouse

Since we’ve got accomplished many of the work within the distant mediator, we deploy repository can be fairly easy.

class PokemonRepositoryImpl @Inject constructor(
non-public val pokemonPager: Pager
) : PokemonRepository {
override enjoyable getPokemonList(): Movement> {
return pokemonPager.movement.map { pagingData ->
pagingData.map { it.toPokemon() }
}
}
}

our use Pager for instance we simply return its stream PagingData to customers. Nonetheless earlier than we do this, we additionally must map PokemonEntity to area title Pokemon mannequin. It is as a result of we area class is aware of nothing about knowledge or presentation lessons kind the idea of Clear Structure, so we must always not deliver knowledge mannequin to area class.

On this pure Kotlin class, not a lot is definitely happening. Right here we’ve got Pokemon our mannequin repository interfaceand easy In case of used class that interacts with this repository.

// REPOSITORY INTERFACE
interface PokemonRepository {
enjoyable getPokemonList(): Movement>
}
// USE CASE
class GetPokemonList @Inject constructor(
non-public val pokemonRepository: PokemonRepository
) {
operator enjoyable invoke(): Movement> {
return pokemonRepository.getPokemonList()
.flowOn(Dispatchers.IO)
}
}


// MODEL
knowledge class Pokemon(
val id: Int,
val title: String,
val imageUrl: String,
)

Right here, a query you might have in your thoughts is likely to be learn how to use PagingData in a pure Kotlin class the place we do not rely on any Android parts. It is actually easy: There’s a particular dependency offered by the Pagination library for non-Android modules, in order that we will entry all of the Pagination parts so simple as PagingSource, PagingData, Pagerand even RemoteMediator.

dependencies {
// ...
implementation "androidx.paging:paging-common:$paging_version"
}

After reporting this fast information area class, let’s leap immediately in presentation layer, the place the remainder of the essential stuff is going on. However first we have to add the next Pagination dependencies construct.gradle doc:

dependencies {
// ...
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
implementation "androidx.paging:paging-compose:$paging_version"
}

Extra differrent runtime-ktx dependence, dependence compose dependency can also be required right here because it offers some center floor between the pagination knowledge movement and our UI.

ViewModel

That is once more one of many easy lessons of this text the place we merely take the stream offered by the use case (which is already offered by the repository) and retailer it in an rvalue.

@HiltViewModel
class PokemonListViewModel @Inject constructor(
non-public val getPokemonList: GetPokemonList
) : ViewModel() {
val pokemonPagingDataFlow: Movement> = getPokemonList()
.cachedIn(viewModelScope)
}

We retailer the stream by calling cachedIn(viewModelScope) let it proceed to work so long as our lifetime ViewModel. In addition to, it persists on configuration modifications like display screen rotation, so that you get the identical current knowledge as a substitute of fetching it from scratch.

This technique additionally preserves our chilly stream and does not flip it into sizzling stream (StateFlow) like stateIn(…) technique will do. Because of this if the stream is just not collected, then no pointless code can be executed.

Display (Person Interface)

Now we’re on the ultimate step of the pagination course of the place we are going to show our pagination gadgets in a LazyColumn. Wouldn’t have RecylerViews or extra adapters in Jetpack Compose. All of that is now dealt with beneath, and our giant variety of gadgets are nonetheless intelligently sorted with out inflicting any efficiency points.

@Composable
enjoyable PokemonListScreen(
snackbarHostState: SnackbarHostState
) {
val viewModel = hiltViewModel()
val pokemonPagingItems = viewModel.pokemonPagingDataFlow.collectAsLazyPagingItems()
if (pokemonPagingItems.loadState.refresh is LoadState.Error) {
LaunchedEffect(key1 = snackbarHostState) {
snackbarHostState.showSnackbar(
(pokemonPagingItems.loadState.refresh as LoadState.Error).error.message ?: ""
)
}
}


Field(modifier = Modifier.fillMaxSize()) {
if (pokemonPagingItems.loadState.refresh is LoadState.Loading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Middle))
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
gadgets(
rely = pokemonPagingItems.itemCount,
key = pokemonPagingItems.itemKey { it.id },
) { index ->
val pokemon = pokemonPagingItems[index]
if (pokemon != null) {
PokemonItem(
pokemon,
modifier = Modifier.fillMaxWidth(),
)
}
}
merchandise {
if (pokemonPagingItems.loadState.append is LoadState.Loading) {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
}
}
}
}
}
}

The very first thing to do in our compositing display screen is to create ViewModel instance and gather the paging knowledge stream saved in it utilizing a helper operate collectAsLazyPagingItems(). This converts the chilly movement right into a LazyPagingItems For instance. By this case we will entry the gadgets already loaded, in addition to the totally different loading states to vary the UI accordingly. Along with these, we will even allow knowledge refresh or retry a beforehand failed load utilizing this model.

in a single Field format, if the “refresh” load state of LazyPagingItems To be Loading, then we all know that we’re on first load and there aren’t any gadgets to show but. Due to this fact, we present a progress indicator. In any other case, we present a LazyColumntogether with the checklist gadgets whose rely And key Parameters are set utilizing our LazyPagingItems For instance. And in every merchandise we simply must entry the corresponding Pokemon object utilizing the given index and show a PokemonItem composable, its implementation particulars will not be given right here for the sake of simplicity.

We even have a selected case the place we have to show a loading indicator under this stuff. And it occurs each time we’re within the technique of fetching extra knowledge, which might be detected via the “append” load standing of LazyPagingItems. Due to this fact, if that’s the case, we are going to add a progress indicator to the tip of the checklist.

And lastly, do not suppose we missed out LaunchedEffect half at first. The LaunchedEffect composables are used to soundly name droop features inside a composable. We’d like a coroutine vary to show a Snackbar in Jetpack Compose phrases SnackbarHostState.showSnackbar(…) is a droop operate. And right here we present a message on the snackbar in case of a refresh failure, which mainly corresponds to the “preliminary load” error in our case. Nonetheless, as I discussed earlier, right here we’ve got created an offline desire app, so if we’ve got cached knowledge in Rooms, the person will see that together with error message.

John Wick: Chapter 4 (FREE) FULLMOVIE The Super Mario Bros Movie avatar 2 Where To Watch Creed 3 Free At Home Knock at the Cabin (2023) FullMovie Where To Watch Ant-Man 3 and the Wasp: Quantumania Cocaine Bear 2023 (FullMovie) Scream 6 Full Movie
Updated: June 28, 2023 — 5:28 am

Leave a Reply

Your email address will not be published. Required fields are marked *

androidkaki.com © 2023 Android kaki