From 320c8d6e7e2b379c363b888c7bb9183d2e92bdb9 Mon Sep 17 00:00:00 2001 From: Serchinastico <54cymru@gmail.com> Date: Tue, 5 Feb 2019 19:54:24 +0100 Subject: [PATCH] Implement working paging --- app/build.gradle | 1 + .../superheroes/ui/view/AcceptanceTest.kt | 2 +- .../superheroes/ui/view/MainActivityTest.kt | 40 ++++++++++++++++++- .../superheroes/SuperHeroesApplication.kt | 8 +++- .../repository/LocalSuperHeroDataSource.kt | 13 ++++-- .../repository/RemoteSuperHeroDataSource.kt | 8 +++- .../data/repository/SuperHeroRepository.kt | 18 ++++----- .../repository/SuperHeroesBoundaryCallback.kt | 34 ++++++++++++++++ .../data/repository/room/SuperHeroDao.kt | 3 +- .../domain/usecase/GetSuperHeroes.kt | 4 +- .../superheroes/ui/view/MainActivity.kt | 9 ++--- .../superheroes/ui/view/SingleLiveEvent.kt | 1 - .../ui/view/adapter/SuperHeroesAdapter.kt | 26 ++++++------ .../ui/viewmodel/SuperHeroesViewModel.kt | 3 +- 14 files changed, 126 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroesBoundaryCallback.kt diff --git a/app/build.gradle b/app/build.gradle index 4d40fc1..2b93265 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/AcceptanceTest.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/AcceptanceTest.kt index cfd9fcb..d8bc98c 100644 --- a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/AcceptanceTest.kt +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/AcceptanceTest.kt @@ -29,7 +29,7 @@ abstract class AcceptanceTest(clazz: Class) : ScreenshotTest { val testRule: IntentsTestRule = IntentsTestRule(clazz, true, false) private val executorServiceOnUiThread = mock { - on(it.submit(any())).thenAnswer { invocation -> + on(it.execute(any())).thenAnswer { invocation -> testRule.runOnUiThread { (invocation.getArgument(0) as Runnable).run() } FutureTask { null } } diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt index 414c938..ae7db5e 100644 --- a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt @@ -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 @@ -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::class.java) { @@ -64,6 +68,11 @@ class MainActivityTest : AcceptanceTest(MainActivity::class.java) compareScreenshot(activity) } + private fun compareScreenshot(activity: MainActivity) { + Thread.sleep(100) + super.compareScreenshot(activity) + } + private fun givenThereAreSomeAvengers(numberOfAvengers: Int): List = givenThereAreSomeSuperHeroes(numberOfAvengers, areAvengers = true) @@ -84,15 +93,42 @@ class MainActivityTest : AcceptanceTest(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().toPagedList())) } override val testDependencies = Kodein.Module("Test dependencies", allowSilentOverride = true) { bind() with instance(repository) } + + private fun List.toPagedList(): PagedList = + PagedList.Builder(object : PositionalDataSource() { + override fun loadRange( + params: LoadRangeParams, + callback: LoadRangeCallback + ) { + callback.onResult(this@toPagedList) + } + + override fun loadInitial( + params: LoadInitialParams, + callback: LoadInitialCallback + ) { + callback.onResult( + this@toPagedList, + 0, + this@toPagedList.size + ) + } + }, 100) + .setNotifyExecutor(newSingleThreadExecutor { Looper.getMainLooper().thread }) + .setFetchExecutor(newSingleThreadExecutor { Looper.getMainLooper().thread }) + .build() } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt b/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt index 12b36ee..8b26bf2 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt @@ -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 @@ -41,9 +42,12 @@ class SuperHeroesApplication : Application(), KodeinAware { database.superHeroesDao() } bind() with provider { - SuperHeroRepository(instance(), instance()) + SuperHeroRepository(instance(), instance(), instance()) } - bind() with singleton { + bind() with provider { + SuperHeroesBoundaryCallback(instance(), instance()) + } + bind() with provider { LocalSuperHeroDataSource(instance(), instance()) } bind() with provider { diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt index 44ca70c..e16a8db 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt @@ -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 @@ -11,14 +13,18 @@ class LocalSuperHeroDataSource( private val dao: SuperHeroDao, private val executor: ExecutorService ) { - fun getAllSuperHeroes(): LiveData> = - Transformations.map(dao.getAll()) { it.toSuperHeroes() } + fun getAllSuperHeroes( + pageSize: Int, + boundaryCallback: PagedList.BoundaryCallback + ): LiveData> = + LivePagedListBuilder(dao.getAll().map { it.toSuperHero() }, pageSize) + .setBoundaryCallback(boundaryCallback) + .build() fun get(id: String): LiveData = Transformations.map(dao.getById(id)) { it?.toSuperHero() } fun saveAll(all: List) = executor.execute { - dao.deleteAll() dao.insertAll(all.map { it.toEntity() }) } @@ -27,7 +33,6 @@ class LocalSuperHeroDataSource( return superHero } - private fun List.toSuperHeroes(): List = map { it.toSuperHero() } private fun SuperHeroEntity.toSuperHero(): SuperHero = superHero private fun SuperHero.toEntity(): SuperHeroEntity = SuperHeroEntity(this) } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt index 3e30cdd..196a4d6 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt @@ -15,11 +15,15 @@ class RemoteSuperHeroDataSource( private val superHeroes: MutableMap = fakeData().associateBy { it.id }.toMutableMap() - fun getAllSuperHeroes(): LiveData> { + fun getSuperHeroesPage(pageIndex: Int, pageSize: Int): LiveData> { val allSuperHeroes = MutableLiveData>() 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 } diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt index 27cb8a7..c247f3b 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt @@ -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> = MediatorLiveData>().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> = + local.getAllSuperHeroes(PAGE_SIZE, boundaryCallback) + fun get(id: String): LiveData = MediatorLiveData().apply { addSource(local.get(id)) { if (it == null) { diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroesBoundaryCallback.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroesBoundaryCallback.kt new file mode 100644 index 0000000..3a78872 --- /dev/null +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroesBoundaryCallback.kt @@ -0,0 +1,34 @@ +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() { + + private var pageIndexAboutToLoad = 0 + + override fun onZeroItemsLoaded() { + loadNextPage() + } + + override fun onItemAtEndLoaded(itemAtEnd: SuperHero) { + loadNextPage() + } + + private fun loadNextPage() { + val remoteSuperHeroesPage = + remote.getSuperHeroesPage(pageIndexAboutToLoad, SuperHeroRepository.PAGE_SIZE) + + remoteSuperHeroesPage.observeForever(object : Observer> { + override fun onChanged(superHeroes: List) { + local.saveAll(superHeroes) + remoteSuperHeroesPage.removeObserver(this) + pageIndexAboutToLoad++ + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt index b9e87aa..2415ee9 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt @@ -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 @@ -10,7 +11,7 @@ import androidx.room.Update @Dao interface SuperHeroDao { @Query("SELECT * FROM superheroes ORDER BY superhero_id ASC") - fun getAll(): LiveData> + fun getAll(): DataSource.Factory @Query("SELECT * FROM superheroes WHERE superhero_id = :id") fun getById(id: String): LiveData diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt index fa97ecb..2140231 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt @@ -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> = superHeroesRepository.getAllSuperHeroes() + operator fun invoke(): LiveData> = + superHeroesRepository.getAllSuperHeroes() } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt index 07986e2..b463615 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt @@ -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 @@ -32,7 +33,7 @@ class MainActivity : BaseActivity() { initializeRecyclerView() viewModel.prepare() viewModel.idOfSuperHeroToOpen.observe(this, Observer { openDetail(it) }) - viewModel.superHeroes.observe(this, Observer> { showSuperHeroes(it) }) + viewModel.superHeroes.observe(this, Observer> { showSuperHeroes(it) }) } override fun configureBinding(binding: MainActivityBinding) { @@ -49,10 +50,8 @@ class MainActivity : BaseActivity() { recycler_view.adapter = adapter } - private fun showSuperHeroes(superHeroes: List) { - adapter.clear() - adapter.addAll(superHeroes) - adapter.notifyDataSetChanged() + private fun showSuperHeroes(superHeroes: PagedList) { + adapter.submitList(superHeroes) } private fun openDetail(id: String) { diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt index f7d219c..9d1d29d 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt @@ -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. diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt index af8ad63..6591f04 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt @@ -3,7 +3,8 @@ 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 @@ -11,13 +12,7 @@ import com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesViewModel internal class SuperHeroesAdapter( private val viewModel: SuperHeroesViewModel -) : RecyclerView.Adapter() { - private val superHeroes: MutableList = ArrayList() - - fun addAll(collection: Collection) { - superHeroes.addAll(collection) - } - +) : PagedListAdapter(diffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuperHeroViewHolder { val binding: SuperHeroRowBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), @@ -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() { + 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 + } } } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt index d467cfb..53f0021 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt @@ -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 @@ -15,7 +16,7 @@ class SuperHeroesViewModel( val isLoading = MutableLiveData() val isShowingEmptyCase = MutableLiveData() - val superHeroes = MediatorLiveData>() + val superHeroes = MediatorLiveData>() val idOfSuperHeroToOpen = SingleLiveEvent() fun prepare() {