Skip to content

Commit

Permalink
Implement working paging
Browse files Browse the repository at this point in the history
  • Loading branch information
Serchinastico committed Mar 15, 2019
1 parent 6100eb2 commit 7072c5f
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ abstract class AcceptanceTest<T : Activity>(clazz: Class<T>) : ScreenshotTest {
val testRule: IntentsTestRule<T> = IntentsTestRule(clazz, true, doNotLaunchActivityAtLunch)

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 @@ -28,12 +28,12 @@ abstract class BaseActivity<T : ViewDataBinding> : AppCompatActivity(), KodeinAw
abstract val toolbarView: Toolbar
abstract val activityModules: Kodein.Module
abstract val viewModel: AndroidViewModel
protected lateinit var binding: T
private lateinit var binding: T

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutId)
binding.setLifecycleOwner(this)
binding.lifecycleOwner = this
configureBinding(binding)
setSupportActionBar(toolbarView)
}
Expand Down
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 @@ -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 7072c5f

Please sign in to comment.