Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

05-paging #12

Open
wants to merge 1 commit into
base: 04-live-data-and-view-model
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
[email protected]
)
}
}, 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()
Serchinastico marked this conversation as resolved.
Show resolved Hide resolved
}

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,7 +28,7 @@ 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)
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 @@ -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