Skip to content

Commit

Permalink
Merge pull request #1 from JayaSuryaT/char-list-paging
Browse files Browse the repository at this point in the history
Add pagination for character list
  • Loading branch information
jayasuryat authored Oct 24, 2021
2 parents 33e1da5 + dc1826c commit 161c0c2
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 107 deletions.
3 changes: 3 additions & 0 deletions buildSrc/src/main/java/Dependency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ object Dependency {
const val roomRuntime = "androidx.room:room-runtime:2.3.0"
const val roomKtx = "androidx.room:room-ktx:2.3.0"
const val roomCompiler = "androidx.room:room-compiler:2.3.0"
const val roomPaging = "androidx.room:room-paging:2.4.0-beta01"

const val pagingRuntime = "androidx.paging:paging-runtime:3.1.0-beta01"

const val ktorAndroid = "io.ktor:ktor-client-android:1.6.3"
const val ktorCio = "io.ktor:ktor-client-cio:1.6.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public open class BaseFragment : Fragment() {
private val jobDelegate: Lazy<Job> = lazy { SupervisorJob() }
private val job by jobDelegate
protected val uiScope: CoroutineScope by lazy { CoroutineScope(Dispatchers.Main + job) }
protected val ioScope: CoroutineScope by lazy { CoroutineScope(Dispatchers.IO + job) }

protected fun NavDirections.navigate(): Unit = findNavController().navigate(this)

Expand Down
6 changes: 6 additions & 0 deletions ui-character-list/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ android {
}
}

tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinCompile::class).all {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}

dependencies {

// Test
Expand All @@ -61,6 +65,8 @@ dependencies {
implementation(Dependency.roomRuntime)
implementation(Dependency.roomKtx)
kapt(Dependency.roomCompiler)
implementation(Dependency.roomPaging)
implementation(Dependency.pagingRuntime)

// Hilt
implementation(Dependency.hilt)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.jayasuryat.characterlist.data.repositories

import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import com.apollographql.apollo.exception.ApolloNetworkException
import com.bumptech.glide.load.HttpException
import com.jayasuryat.basedata.mappers.Mapper
import com.jayasuryat.basedata.mappers.map
import com.jayasuryat.characterlist.CharacterListQuery
import com.jayasuryat.characterlist.data.sources.local.definitions.CharacterListLocalDataSource
import com.jayasuryat.characterlist.data.sources.local.entities.CharacterEntity
import com.jayasuryat.characterlist.data.sources.remote.definitions.CharacterListNetworkDataSource
import java.io.IOException


@OptIn(ExperimentalPagingApi::class)
internal class CharacterListRemoteMediator(
private val networkClient: CharacterListNetworkDataSource,
private val cacheClient: CharacterListLocalDataSource,
private val characterDtoToEntityMapper: Mapper<CharacterListQuery.Result, CharacterEntity>,
) : RemoteMediator<Int, CharacterEntity>() {

override suspend fun initialize(): InitializeAction = InitializeAction.SKIP_INITIAL_REFRESH

override suspend fun load(
loadType: LoadType,
state: PagingState<Int, CharacterEntity>,
): MediatorResult {

return try {

val loadKey: Int = when (loadType) {
LoadType.REFRESH -> REMOTE_API_PAGE_START_INDEX
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> (state.getPageNumber() ?: 0) + 1
}

if (loadType == LoadType.REFRESH) cacheClient.deleteAllCharacters()

val characters = networkClient.getCharacters(loadKey).data?.characters()?.results()
characters?.let {
cacheClient.saveCharacters(characterDtoToEntityMapper.map(characters))
}

MediatorResult.Success(endOfPaginationReached = characters.isNullOrEmpty())

} catch (ex: ApolloNetworkException) {
MediatorResult.Error(ex)
} catch (e: IOException) {
return MediatorResult.Error(e)
} catch (e: HttpException) {
return MediatorResult.Error(e)
}
}

private fun PagingState<Int, CharacterEntity>.getPageNumber(): Int? {
val last = lastItemOrNull() ?: return null
val position = last.id.toInt()
val size = config.pageSize
val currentPage = (position / size) + (if (position % size == 0) -1 else 0)
return currentPage + REMOTE_API_PAGE_START_INDEX
}

private companion object {

const val REMOTE_API_PAGE_START_INDEX: Int = 1
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,41 @@
package com.jayasuryat.characterlist.data.repositories

import androidx.paging.*
import com.jayasuryat.basedata.mappers.Mapper
import com.jayasuryat.basedata.mappers.map
import com.jayasuryat.basedata.models.KResult
import com.jayasuryat.basedata.models.wrapAsResult
import com.jayasuryat.basedata.providers.DispatcherProvider
import com.jayasuryat.characterlist.CharacterListQuery
import com.jayasuryat.characterlist.data.sources.local.definitions.CharacterListLocalDataSource
import com.jayasuryat.characterlist.data.sources.local.entities.CharacterEntity
import com.jayasuryat.characterlist.data.sources.remote.definitions.CharacterListNetworkDataSource
import com.jayasuryat.characterlist.domain.models.Character
import com.jayasuryat.characterlist.domain.repos.definitions.CharacterListRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

internal class CharacterListRepo(
private val dispatcher: DispatcherProvider,
private val networkClient: CharacterListNetworkDataSource,
@OptIn(ExperimentalPagingApi::class)
internal class CharacterListRepo constructor(
private val mediator: RemoteMediator<Int, CharacterEntity>,
private val cacheClient: CharacterListLocalDataSource,
private val characterDtoToEntityMapper: Mapper<CharacterListQuery.Result, CharacterEntity>,
private val characterEntityToDomainMapper: Mapper<CharacterEntity, Character>,
) : CharacterListRepository {

override suspend fun getCharacters(
page: Int,
): KResult<List<Character>> = wrapAsResult(dispatcher.io()) {
override fun getPagedCharacters(): Flow<PagingData<Character>> {

val networkResponse = networkClient.getCharacters(page).data?.characters()
val networkCharacters = networkResponse?.results()
val pagingConfig = PagingConfig(
pageSize = PAGE_SIZE,
initialLoadSize = PAGE_SIZE,
)

if (networkCharacters.isNullOrEmpty())
throw RuntimeException("No characters found") // TODO: 15/10/21

val mappedCharacters = characterDtoToEntityMapper.map(networkCharacters)
cacheClient.saveCharacters(mappedCharacters)

val cachedCharacters = cacheClient.getCharacters(limit = PAGE_SIZE, offset = page - 1)

characterEntityToDomainMapper.map(cachedCharacters)
}

override suspend fun getAllCharactersInCache(): KResult<List<Character>> =
wrapAsResult(dispatcher.io()) {
characterEntityToDomainMapper.map(cacheClient.getAllCharacters())
return Pager(
config = pagingConfig,
remoteMediator = mediator,
) {
cacheClient.getPagedCharacters()
}.flow.map {
it.map(characterEntityToDomainMapper::map)
}
}

companion object {
private companion object {

private const val PAGE_SIZE: Int = 20
const val PAGE_SIZE: Int = 20
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.jayasuryat.characterlist.data.sources.local.definitions

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
Expand All @@ -10,12 +11,12 @@ import com.jayasuryat.characterlist.data.sources.local.entities.CharacterEntity
@Dao
internal interface CharacterListLocalDataSource {

@Query("SELECT * FROM characters")
fun getPagedCharacters(): PagingSource<Int, CharacterEntity>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveCharacters(characters: List<CharacterEntity>)

@Query("SELECT * FROM characters LIMIT :limit OFFSET :offset")
suspend fun getCharacters(limit: Int, offset: Int): List<CharacterEntity>

@Query("SELECT * FROM characters")
suspend fun getAllCharacters(): List<CharacterEntity>
@Query("DELETE FROM characters")
suspend fun deleteAllCharacters()
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.jayasuryat.characterlist.di

import com.jayasuryat.basedata.mappers.Mapper
import com.jayasuryat.basedata.providers.DispatcherProvider
import com.jayasuryat.characterlist.CharacterListQuery
import com.jayasuryat.characterlist.data.repositories.CharacterListRemoteMediator
import com.jayasuryat.characterlist.data.repositories.CharacterListRepo
import com.jayasuryat.characterlist.data.sources.local.definitions.CharacterListLocalDataSource
import com.jayasuryat.characterlist.data.sources.local.entities.CharacterEntity
import com.jayasuryat.characterlist.data.sources.remote.definitions.CharacterListNetworkDataSource
import com.jayasuryat.characterlist.di.MapperModule.C_D_DTO_TO_ENTITY
import com.jayasuryat.characterlist.di.MapperModule.C_D_ENTITY_TO_DOMAIN
import com.jayasuryat.characterlist.domain.models.Character
import com.jayasuryat.characterlist.domain.repos.definitions.CharacterListRepository
import dagger.Module
Expand All @@ -25,23 +24,29 @@ internal object RepositoryModule {

@Provides
@Singleton
fun providesCharacterListRepo(
dispatcherProvider: DispatcherProvider,
fun providesRemoteMediator(
networkDataSource: CharacterListNetworkDataSource,
localDataSource: CharacterListLocalDataSource,

@Named(C_D_DTO_TO_ENTITY)
characterDtoToEntityMapper:
Mapper<@JvmSuppressWildcards CharacterListQuery.Result, @JvmSuppressWildcards CharacterEntity>,
): CharacterListRemoteMediator = CharacterListRemoteMediator(
networkClient = networkDataSource,
cacheClient = localDataSource,
characterDtoToEntityMapper = characterDtoToEntityMapper
)

@Named(C_D_ENTITY_TO_DOMAIN)
characterEntityToDtoMapper:
@Provides
@Singleton
fun test(
remoteMediator: CharacterListRemoteMediator,
localDataSource: CharacterListLocalDataSource,
@Named(MapperModule.C_D_ENTITY_TO_DOMAIN)
characterEntityToDomainMapper:
Mapper<@JvmSuppressWildcards CharacterEntity, @JvmSuppressWildcards Character>,
): CharacterListRepository = CharacterListRepo(
dispatcher = dispatcherProvider,
networkClient = networkDataSource,
mediator = remoteMediator,
cacheClient = localDataSource,
characterDtoToEntityMapper = characterDtoToEntityMapper,
characterEntityToDomainMapper = characterEntityToDtoMapper,
characterEntityToDomainMapper = characterEntityToDomainMapper,
)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.jayasuryat.characterlist.domain.repos.definitions

import com.jayasuryat.basedata.models.KResult
import androidx.paging.PagingData
import com.jayasuryat.characterlist.domain.models.Character
import kotlinx.coroutines.flow.Flow

interface CharacterListRepository {

suspend fun getCharacters(
page: Int,
): KResult<List<Character>>

suspend fun getAllCharactersInCache(): KResult<List<Character>>
fun getPagedCharacters(): Flow<PagingData<Character>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.jayasuryat.characterlist.presentation

internal data class CharacterDef(
val id: Long,
val name: String,
val imageUrl: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import com.jayasuryat.base.anim.impl.AlphaAnim
import com.jayasuryat.base.anim.impl.CircleRevealHelper
import com.jayasuryat.base.anim.impl.TranslateAnim
import com.jayasuryat.base.arch.BaseAbsFragment
import com.jayasuryat.base.show
import com.jayasuryat.base.shrinkOnClick
import com.jayasuryat.characterlist.NavigateBack
import com.jayasuryat.characterlist.OpenCharacter
import com.jayasuryat.characterlist.databinding.FragmentCharacterListBinding
import dagger.hilt.android.AndroidEntryPoint
import jp.wasabeef.recyclerview.animators.SlideInUpAnimator
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import java.util.concurrent.atomic.AtomicBoolean

Expand Down Expand Up @@ -70,10 +70,8 @@ class CharacterListFragment : BaseAbsFragment<CharacterListViewModel,
override fun setupObservers(): CharacterListViewModel.() -> Unit = {

obsCharactersList.observe(viewLifecycleOwner) { characters ->
characterListAdapter.submitList(characters)
binding.rvCharactersList.show()
(view?.parent as? ViewGroup)
?.doOnPreDraw { startPostponedEnterTransition() }
uiScope.launch { characterListAdapter.submitData(characters) }
(view?.parent as? ViewGroup)?.doOnPreDraw { startPostponedEnterTransition() }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,30 @@
package com.jayasuryat.characterlist.presentation

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.paging.cachedIn
import androidx.paging.map
import com.jayasuryat.base.arch.BaseViewModel
import com.jayasuryat.characterlist.domain.models.Character
import com.jayasuryat.characterlist.domain.repos.definitions.CharacterListRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.map
import javax.inject.Inject

@HiltViewModel
class CharacterListViewModel @Inject constructor(
private val charactersRepository: CharacterListRepository,
private val charactersPagingRepository: CharacterListRepository,
) : BaseViewModel() {

private val _obsCharactersList: MutableLiveData<List<CharacterDef>> = MutableLiveData()
internal val obsCharactersList: LiveData<List<CharacterDef>> = _obsCharactersList

init {
ioScope.launch { doWhileLoading { loadCharacters() } }
internal val obsCharactersList by lazy {
charactersPagingRepository.getPagedCharacters()
.cachedIn(ioScope)
.map { it.map { character -> character.mapToDef() } }
.asLiveData(ioScope.coroutineContext)
}

private suspend fun loadCharacters() {

doWhileLoading {

charactersRepository.getAllCharactersInCache()
.getOrNull()
.mapToDef()
.let(_obsCharactersList::postValue)

charactersRepository.getCharacters(0)
.logError()
.getOrNull()
.mapToDef()
.let(_obsCharactersList::postValue)
}
}

private fun List<Character>?.mapToDef(): List<CharacterDef>? {
return if (this.isNullOrEmpty()) null
else this.map { character ->
CharacterDef(
id = character.id,
name = character.name,
imageUrl = character.imageUrl,
)
}
}
private fun Character.mapToDef(): CharacterDef = CharacterDef(
id = id,
name = name,
imageUrl = imageUrl,
)
}
Loading

0 comments on commit 161c0c2

Please sign in to comment.