Skip to content

Commit

Permalink
Implement working paging
Browse files Browse the repository at this point in the history
  • Loading branch information
Serchinastico committed Feb 6, 2019
1 parent 592849a commit 190583a
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 44 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.0.0"
implementation "androidx.lifecycle:lifecycle-runtime:2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
implementation "androidx.room:room-runtime:2.1.0-alpha04"
kapt "androidx.room:room-compiler:2.1.0-alpha04"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ abstract class AcceptanceTest<T : Activity>(clazz: Class<T>) : ScreenshotTest {
val testRule: IntentsTestRule<T> = IntentsTestRule(clazz, true, false)

private val executorServiceOnUiThread = mock<ExecutorService> {
on(it.submit(any())).thenAnswer { invocation ->
on(it.execute(any())).thenAnswer { invocation ->
testRule.runOnUiThread { (invocation.getArgument(0) as Runnable).run() }
FutureTask { null }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.karumi.jetpack.superheroes.ui.view

import android.os.Looper
import androidx.paging.PagedList
import androidx.paging.PositionalDataSource
import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository
import com.karumi.jetpack.superheroes.data.singleValueLiveData
import com.karumi.jetpack.superheroes.domain.model.SuperHero
Expand All @@ -9,6 +12,7 @@ import org.kodein.di.Kodein
import org.kodein.di.erased.bind
import org.kodein.di.erased.instance
import org.mockito.Mock
import java.util.concurrent.Executors.newSingleThreadExecutor

class MainActivityTest : AcceptanceTest<MainActivity>(MainActivity::class.java) {

Expand Down Expand Up @@ -64,6 +68,11 @@ class MainActivityTest : AcceptanceTest<MainActivity>(MainActivity::class.java)
compareScreenshot(activity)
}

private fun compareScreenshot(activity: MainActivity) {
Thread.sleep(100)
super.compareScreenshot(activity)
}

private fun givenThereAreSomeAvengers(numberOfAvengers: Int): List<SuperHero> =
givenThereAreSomeSuperHeroes(numberOfAvengers, areAvengers = true)

Expand All @@ -84,15 +93,42 @@ class MainActivityTest : AcceptanceTest<MainActivity>(MainActivity::class.java)
)
}

whenever(repository.getAllSuperHeroes()).thenReturn(singleValueLiveData(superHeroes))
whenever(repository.getAllSuperHeroes())
.thenReturn(singleValueLiveData(superHeroes.toPagedList()))

return superHeroes
}

private fun givenThereAreNoSuperHeroes() {
whenever(repository.getAllSuperHeroes()).thenReturn(singleValueLiveData(emptyList()))
whenever(repository.getAllSuperHeroes())
.thenReturn(singleValueLiveData(emptyList<SuperHero>().toPagedList()))
}

override val testDependencies = Kodein.Module("Test dependencies", allowSilentOverride = true) {
bind<SuperHeroRepository>() with instance(repository)
}

private fun List<SuperHero>.toPagedList(): PagedList<SuperHero> =
PagedList.Builder(object : PositionalDataSource<SuperHero>() {
override fun loadRange(
params: LoadRangeParams,
callback: LoadRangeCallback<SuperHero>
) {
callback.onResult(this@toPagedList)
}

override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<SuperHero>
) {
callback.onResult(
this@toPagedList,
0,
this@toPagedList.size
)
}
}, 100)
.setNotifyExecutor(newSingleThreadExecutor { Looper.getMainLooper().thread })
.setFetchExecutor(newSingleThreadExecutor { Looper.getMainLooper().thread })
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.karumi.jetpack.superheroes.common.module
import com.karumi.jetpack.superheroes.data.repository.LocalSuperHeroDataSource
import com.karumi.jetpack.superheroes.data.repository.RemoteSuperHeroDataSource
import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository
import com.karumi.jetpack.superheroes.data.repository.SuperHeroesBoundaryCallback
import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroDao
import org.kodein.di.DKodein
import org.kodein.di.Kodein
Expand Down Expand Up @@ -41,9 +42,12 @@ class SuperHeroesApplication : Application(), KodeinAware {
database.superHeroesDao()
}
bind<SuperHeroRepository>() with provider {
SuperHeroRepository(instance(), instance())
SuperHeroRepository(instance(), instance(), instance())
}
bind<LocalSuperHeroDataSource>() with singleton {
bind<SuperHeroesBoundaryCallback>() with provider {
SuperHeroesBoundaryCallback(instance(), instance())
}
bind<LocalSuperHeroDataSource>() with provider {
LocalSuperHeroDataSource(instance(), instance())
}
bind<RemoteSuperHeroDataSource>() with provider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.karumi.jetpack.superheroes.data.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroDao
import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroEntity
import com.karumi.jetpack.superheroes.domain.model.SuperHero
Expand All @@ -11,14 +13,20 @@ class LocalSuperHeroDataSource(
private val dao: SuperHeroDao,
private val executor: ExecutorService
) {
fun getAllSuperHeroes(): LiveData<List<SuperHero>> =
Transformations.map(dao.getAll()) { it.toSuperHeroes() }
fun getAllSuperHeroes(
pageSize: Int,
boundaryCallback: PagedList.BoundaryCallback<SuperHero>
): LiveData<PagedList<SuperHero>> {
val superHeroesFactory = dao.getAll().map { it.toSuperHero() }
return LivePagedListBuilder(superHeroesFactory, pageSize)
.setBoundaryCallback(boundaryCallback)
.build()
}

fun get(id: String): LiveData<SuperHero?> =
Transformations.map(dao.getById(id)) { it?.toSuperHero() }

fun saveAll(all: List<SuperHero>) = executor.execute {
dao.deleteAll()
dao.insertAll(all.map { it.toEntity() })
}

Expand All @@ -27,7 +35,6 @@ class LocalSuperHeroDataSource(
return superHero
}

private fun List<SuperHeroEntity>.toSuperHeroes(): List<SuperHero> = map { it.toSuperHero() }
private fun SuperHeroEntity.toSuperHero(): SuperHero = superHero
private fun SuperHero.toEntity(): SuperHeroEntity = SuperHeroEntity(this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ class RemoteSuperHeroDataSource(
private val superHeroes: MutableMap<String, SuperHero> =
fakeData().associateBy { it.id }.toMutableMap()

fun getAllSuperHeroes(): LiveData<List<SuperHero>> {
fun getSuperHeroesPage(pageIndex: Int, pageSize: Int): LiveData<List<SuperHero>> {
val allSuperHeroes = MutableLiveData<List<SuperHero>>()
executor.execute {
waitABit()
allSuperHeroes.postValue(superHeroes.values.toList().sortedBy { it.id })
val superHeroesPage = superHeroes.values.toList()
.sortedBy { it.id }
.drop(pageIndex * pageSize)
.take(pageSize)
allSuperHeroes.postValue(superHeroesPage)
}
return allSuperHeroes
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@ package com.karumi.jetpack.superheroes.data.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.paging.PagedList
import com.karumi.jetpack.superheroes.domain.model.SuperHero

class SuperHeroRepository(
private val local: LocalSuperHeroDataSource,
private val remote: RemoteSuperHeroDataSource
private val remote: RemoteSuperHeroDataSource,
private val boundaryCallback: SuperHeroesBoundaryCallback
) {
fun getAllSuperHeroes(): LiveData<List<SuperHero>> = MediatorLiveData<List<SuperHero>>().apply {
val localSource = local.getAllSuperHeroes()
val remoteSource = remote.getAllSuperHeroes()

addSource(remoteSource) { superHeroes ->
removeSource(remoteSource)
addSource(localSource) { postValue(it) }
local.saveAll(superHeroes)
}
companion object {
const val PAGE_SIZE = 4
}

fun getAllSuperHeroes(): LiveData<PagedList<SuperHero>> =
local.getAllSuperHeroes(PAGE_SIZE, boundaryCallback)

fun get(id: String): LiveData<SuperHero?> = MediatorLiveData<SuperHero?>().apply {
addSource(local.get(id)) {
if (it == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.karumi.jetpack.superheroes.data.repository

import androidx.lifecycle.Observer
import androidx.paging.PagedList
import com.karumi.jetpack.superheroes.domain.model.SuperHero

class SuperHeroesBoundaryCallback(
private val local: LocalSuperHeroDataSource,
private val remote: RemoteSuperHeroDataSource
) : PagedList.BoundaryCallback<SuperHero>() {

private var pageIndexAboutToLoad = 0

override fun onZeroItemsLoaded() {
pageIndexAboutToLoad = 0
loadNextPage()
}

override fun onItemAtEndLoaded(itemAtEnd: SuperHero) {
loadNextPage()
}

private fun loadNextPage() {
val remoteSuperHeroesPage =
remote.getSuperHeroesPage(pageIndexAboutToLoad, SuperHeroRepository.PAGE_SIZE)

remoteSuperHeroesPage.observeForever(object : Observer<List<SuperHero>> {
override fun onChanged(superHeroes: List<SuperHero>) {
local.saveAll(superHeroes)
remoteSuperHeroesPage.removeObserver(this)
pageIndexAboutToLoad++
}
})
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.karumi.jetpack.superheroes.data.repository.room

import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
Expand All @@ -10,7 +11,7 @@ import androidx.room.Update
@Dao
interface SuperHeroDao {
@Query("SELECT * FROM superheroes ORDER BY superhero_id ASC")
fun getAll(): LiveData<List<SuperHeroEntity>>
fun getAll(): DataSource.Factory<Int, SuperHeroEntity>

@Query("SELECT * FROM superheroes WHERE superhero_id = :id")
fun getById(id: String): LiveData<SuperHeroEntity?>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.karumi.jetpack.superheroes.domain.usecase

import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository
import com.karumi.jetpack.superheroes.domain.model.SuperHero

class GetSuperHeroes(private val superHeroesRepository: SuperHeroRepository) {
operator fun invoke(): LiveData<List<SuperHero>> = superHeroesRepository.getAllSuperHeroes()
operator fun invoke(): LiveData<PagedList<SuperHero>> =
superHeroesRepository.getAllSuperHeroes()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.karumi.jetpack.superheroes.ui.view
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import com.karumi.jetpack.superheroes.R
import com.karumi.jetpack.superheroes.common.bindViewModel
Expand Down Expand Up @@ -32,7 +33,7 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
initializeRecyclerView()
viewModel.prepare()
viewModel.idOfSuperHeroToOpen.observe(this, Observer<String> { openDetail(it) })
viewModel.superHeroes.observe(this, Observer<List<SuperHero>> { showSuperHeroes(it) })
viewModel.superHeroes.observe(this, Observer<PagedList<SuperHero>> { showSuperHeroes(it) })
}

override fun configureBinding(binding: MainActivityBinding) {
Expand All @@ -49,10 +50,8 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
recycler_view.adapter = adapter
}

private fun showSuperHeroes(superHeroes: List<SuperHero>) {
adapter.clear()
adapter.addAll(superHeroes)
adapter.notifyDataSetChanged()
private fun showSuperHeroes(superHeroes: PagedList<SuperHero>) {
adapter.submitList(superHeroes)
}

private fun openDetail(id: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean


/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@ package com.karumi.jetpack.superheroes.ui.view.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import com.karumi.jetpack.superheroes.R
import com.karumi.jetpack.superheroes.databinding.SuperHeroRowBinding
import com.karumi.jetpack.superheroes.domain.model.SuperHero
import com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesViewModel

internal class SuperHeroesAdapter(
private val viewModel: SuperHeroesViewModel
) : RecyclerView.Adapter<SuperHeroViewHolder>() {
private val superHeroes: MutableList<SuperHero> = ArrayList()

fun addAll(collection: Collection<SuperHero>) {
superHeroes.addAll(collection)
}

) : PagedListAdapter<SuperHero, SuperHeroViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuperHeroViewHolder {
val binding: SuperHeroRowBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
Expand All @@ -30,14 +25,17 @@ internal class SuperHeroesAdapter(
}

override fun onBindViewHolder(holder: SuperHeroViewHolder, position: Int) {
holder.render(superHeroes[position], viewModel)
val superHero = getItem(position) ?: return
holder.render(superHero, viewModel)
}

override fun getItemCount(): Int {
return superHeroes.size
}
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<SuperHero>() {
override fun areItemsTheSame(oldItem: SuperHero, newItem: SuperHero): Boolean =
oldItem.id == newItem.id

fun clear() {
superHeroes.clear()
override fun areContentsTheSame(oldItem: SuperHero, newItem: SuperHero): Boolean =
oldItem == newItem
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedList
import com.karumi.jetpack.superheroes.domain.model.SuperHero
import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroes
import com.karumi.jetpack.superheroes.ui.view.SingleLiveEvent
Expand All @@ -15,7 +16,7 @@ class SuperHeroesViewModel(

val isLoading = MutableLiveData<Boolean>()
val isShowingEmptyCase = MutableLiveData<Boolean>()
val superHeroes = MediatorLiveData<List<SuperHero>>()
val superHeroes = MediatorLiveData<PagedList<SuperHero>>()
val idOfSuperHeroToOpen = SingleLiveEvent<String>()

fun prepare() {
Expand Down

0 comments on commit 190583a

Please sign in to comment.