Skip to content

Commit

Permalink
Merge pull request #95 from vsukharew/dev-release-2.1.0
Browse files Browse the repository at this point in the history
release 2.1.0
  • Loading branch information
vsukharew committed Jan 15, 2022
2 parents 404c6a3 + 106c12e commit 9c995c4
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 58 deletions.
9 changes: 3 additions & 6 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ext {
library : [
publishGroupId : 'io.github.vsukharew',
publishArtifactId: 'anytypeadapter',
versionName : "2.0.0",
versionName : "2.1.0",
versionCode : 1,
],
sample : [
Expand All @@ -16,8 +16,8 @@ ext {
]
versions = [
kotlin : [
'language' : '1.4.30',
'coroutines': '1.3.9'
'language' : '1.6.0',
'coroutines': '1.5.2'
],
androidx : [
'cardview' : '1.0.0',
Expand All @@ -42,7 +42,6 @@ ext {
retrofit : '2.8.1',
leakcanary : '2.2',
junit5 : '5.7.1',
junit4 : '4.12',
mockito : '3.2.0',
]
androidx = [
Expand All @@ -57,7 +56,6 @@ ext {
lifecycle : "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx.lifecycle}"
]
kotlin = [
language : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin.language}",
coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlin.coroutines}",
coroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlin.coroutines}",
coroutinesUnitTest: "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlin.coroutines}",
Expand All @@ -76,6 +74,5 @@ ext {
dagger : "com.google.dagger:dagger:${versions.dagger}",
]
junit5 = "org.junit.jupiter:junit-jupiter-engine:${versions.junit5}"
junit4 = "junit:junit:${versions.junit4}"
mockito = "org.mockito.kotlin:mockito-kotlin:${versions.mockito}"
}
2 changes: 0 additions & 2 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,12 @@ android {

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation kotlin.language
implementation kotlin.coroutines
implementation androidx.appCompat
implementation androidx.ktx
implementation androidx.constraintLayout
implementation androidx.recyclerView
testImplementation junit5
testImplementation junit4
testImplementation mockito
testImplementation kotlin.coroutinesUnitTest
androidTestImplementation androidx.testRunner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import vsukharev.anytypeadapter.holder.AnyTypeViewHolder
import vsukharev.anytypeadapter.item.AdapterItem

/**
* Adapter that is able to display items of any view type at the same time
*/
open class AnyTypeAdapter : RecyclerView.Adapter<AnyTypeViewHolder<Any, ViewBinding>>() {
protected var anyTypeCollection: AnyTypeCollection = AnyTypeCollection.EMPTY
var diffStrategy: DiffStrategy = DiffStrategy.DiscardLatest()
var isPayloadEnabled = false

override fun onCreateViewHolder(
parent: ViewGroup,
Expand All @@ -25,6 +25,18 @@ open class AnyTypeAdapter : RecyclerView.Adapter<AnyTypeViewHolder<Any, ViewBind
return anyTypeCollection.currentItemViewTypeDelegate.createViewHolder(view)
}

override fun onBindViewHolder(
holder: AnyTypeViewHolder<Any, ViewBinding>,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNotEmpty()) {
holder.applyPayload(payloads)
} else {
onBindViewHolder(holder, position)
}
}

override fun onBindViewHolder(holder: AnyTypeViewHolder<Any, ViewBinding>, position: Int) {
with(anyTypeCollection) {
currentItemViewTypeDelegate.bind(items[position], holder)
Expand All @@ -50,12 +62,12 @@ open class AnyTypeAdapter : RecyclerView.Adapter<AnyTypeViewHolder<Any, ViewBind
) {
val diffBlock = suspend {
val adapter = this
val diffResult = DiffUtil.calculateDiff(
DiffUtilCallback(
anyTypeCollection.items,
collection.items
)
)
val diffCallback = if (isPayloadEnabled) {
PayloadDiffUtilCallback(anyTypeCollection, collection)
} else {
DiffUtilCallback(anyTypeCollection, collection)
}
val diffResult = DiffUtil.calculateDiff(diffCallback)
withContext(Dispatchers.Main) {
anyTypeCollection = collection
diffResult.dispatchUpdatesTo(adapter)
Expand All @@ -65,19 +77,34 @@ open class AnyTypeAdapter : RecyclerView.Adapter<AnyTypeViewHolder<Any, ViewBind
diffStrategy.calculateDiff(diffBlock)
}

private class DiffUtilCallback(
private val oldList: List<AdapterItem<Any>>,
private val newList: List<AdapterItem<Any>>
) : DiffUtil.Callback() {
private class PayloadDiffUtilCallback(
oldList: AnyTypeCollection,
newList: AnyTypeCollection
) : DiffUtilCallback(oldList, newList) {

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val delegate = with(oldList) {
itemsMetaData[findCurrentItemViewTypePosition(oldItemPosition)].delegate
}
return delegate.getChangePayload(
oldList.items[oldItemPosition].data,
newList.items[newItemPosition].data
)
}
}

private open class DiffUtilCallback(
protected val oldList: AnyTypeCollection,
protected val newList: AnyTypeCollection
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size

override fun getNewListSize(): Int = newList.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].areItemsTheSame(newList[newItemPosition])
oldList.items[oldItemPosition].areItemsTheSame(newList.items[newItemPosition])

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].areContentsTheSame(newList[newItemPosition])
oldList.items[oldItemPosition].areContentsTheSame(newList.items[newItemPosition])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import vsukharev.anytypeadapter.item.AdapterItem
* @see [RecyclerView.Adapter.onCreateViewHolder]
* @see [RecyclerView.Adapter.getItemViewType]
*/
abstract class AnyTypeDelegate<T, V: ViewBinding, H: AnyTypeViewHolder<T, V>> {
abstract class AnyTypeDelegate<T, V : ViewBinding, H : AnyTypeViewHolder<T, V>> {

/**
* Creates a view holder.
Expand All @@ -35,6 +35,12 @@ abstract class AnyTypeDelegate<T, V: ViewBinding, H: AnyTypeViewHolder<T, V>> {

abstract fun getItemId(item: T): String

/**
* Returns something that may have changed between the two items during the collections diff calculation.
* Called only when [oldItem] and [newItem] have the same id
*/
open fun getChangePayload(oldItem: T, newItem: T): Any? = null

/**
* Binds the data to view holder
* Should be called inside [RecyclerView.Adapter.onBindViewHolder]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import vsukharev.anytypeadapter.adapter.AnyTypeAdapter
import vsukharev.anytypeadapter.delegate.AnyTypeDelegate

/**
* The base class for [AnyTypeAdapter] view holders
*/
abstract class AnyTypeViewHolder<T, V: ViewBinding>(
abstract class AnyTypeViewHolder<T, V : ViewBinding>(
viewBinding: V
) : RecyclerView.ViewHolder(viewBinding.root) {
protected val context: Context = itemView.context
Expand All @@ -17,4 +18,11 @@ abstract class AnyTypeViewHolder<T, V: ViewBinding>(
* Sets the item fields values to views
*/
abstract fun bind(item: T)

/**
* Sets to views only that values that were changed and returned from [AnyTypeDelegate.getChangePayload].
*
* Serves as partial item binding
*/
open fun applyPayload(payloads: List<Any>) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package vsukharev.anytypeadapter.adapter

import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.test.TestCoroutineDispatcher
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito.*
import org.mockito.kotlin.argumentCaptor
import vsukharev.anytypeadapter.common.MainDispatcherExtension
import vsukharev.anytypeadapter.common.MockInitializer
import vsukharev.anytypeadapter.domain.Track
import vsukharev.anytypeadapter.holder.AnyTypeViewHolder

@ExtendWith(MainDispatcherExtension::class)
class AnyTypeAdapterTest : MockInitializer() {

private val dispatcher = TestCoroutineDispatcher()

@Test
fun `onBindViewHolder - call with non-empty payload - verify holder applyPayload get called`() {
val adapter = AnyTypeAdapter()
val captor = argumentCaptor<List<Any>>()
val payload = mutableListOf(Any())
adapter.onBindViewHolder(trackHolder as AnyTypeViewHolder<Any, ViewBinding>, 0, payload)
verify(trackHolder).applyPayload(captor.capture())
assert(payload == captor.firstValue)
}

@Test
fun `onBindViewHolder - call with empty payload - verify holder applyPayload not called`() {
val adapter = AnyTypeAdapter().apply {
diffStrategy = DiffStrategy.DiscardLatest(dispatcher)
}
AnyTypeCollection.Builder()
.add(listOf(Track(), Track()), trackDelegate)
.build()
.let { adapter.setCollection(it) }
val payload = mutableListOf<Any>()
adapter.onBindViewHolder(trackHolder as AnyTypeViewHolder<Any, ViewBinding>, 0, payload)
verify(trackHolder, times(0)).applyPayload(anyList())
}

@Test
fun `onBindViewHolder - call with overload with no payload - verify holder applyPayload not called`() {
val adapter = AnyTypeAdapter().apply {
diffStrategy = DiffStrategy.DiscardLatest(dispatcher)
}
AnyTypeCollection.Builder()
.add(listOf(Track(), Track()), trackDelegate)
.build()
.let { adapter.setCollection(it) }
adapter.onBindViewHolder(trackHolder as AnyTypeViewHolder<Any, ViewBinding>, 0)
verify(trackHolder, times(0)).applyPayload(anyList())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@ package vsukharev.anytypeadapter.adapter

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
import org.junit.Rule
import org.mockito.Mockito.`when`
import org.mockito.kotlin.*
import vsukharev.anytypeadapter.common.CoroutineDispatcherRule

@ExperimentalCoroutinesApi
class DiffStrategyTest {
@Rule
private val coroutineTestRule = CoroutineDispatcherRule()
private val dispatcher = coroutineTestRule.dispatcher
private val dispatcher = TestCoroutineDispatcher()
private val diffChannelMock = mock<Channel<suspend () -> Unit?>>().apply {
stub {
onBlocking { send(any()) }.doReturn(Unit)
Expand All @@ -25,7 +22,7 @@ class DiffStrategyTest {
fun `calculateDiff QueueStrategy Verify that diffBlock is sent to channel`() {
dispatcher.runBlockingTest {
val captor = argumentCaptor<suspend () -> Unit?>()
val strategy = DiffStrategy.Queue(diffChannelMock, dispatcher)
val strategy = DiffStrategy.Queue(diffChannelMock)
val diffBlock: suspend () -> Unit? = { }
strategy.calculateDiff(diffBlock)
verify(diffChannelMock).send(captor.capture())
Expand All @@ -35,24 +32,20 @@ class DiffStrategyTest {

@Test
fun `calculateDiff DiscardLatest verify previous diffJob cancelled`() {
dispatcher.runBlockingTest {
val strategy = DiffStrategy.DiscardLatest().apply { diffJob = jobMock }
val diffBlock = suspend { }
strategy.calculateDiff(diffBlock)
verify(jobMock).cancel()
}
val strategy = DiffStrategy.DiscardLatest().apply { diffJob = jobMock }
val diffBlock = suspend { }
strategy.calculateDiff(diffBlock)
verify(jobMock).cancel()
}

@Test
fun `calculateDiff DiscardLatest verify diffBlock called`() {
dispatcher.runBlockingTest {
val diffBlock = mock<DiffBlock>().apply {
`when`(invoke()).thenReturn { }
}
val strategy = DiffStrategy.DiscardLatest()
strategy.calculateDiff(diffBlock())
verify(diffBlock).invoke()
val diffBlock = mock<DiffBlock>().apply {
`when`(invoke()).thenReturn { }
}
val strategy = DiffStrategy.DiscardLatest()
strategy.calculateDiff(diffBlock())
verify(diffBlock).invoke()
}

private fun interface DiffBlock : () -> suspend () -> Unit?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,18 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext

@ExperimentalCoroutinesApi
class CoroutineDispatcherRule(
val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
class MainDispatcherExtension : BeforeAllCallback, ExtensionContext.Store.CloseableResource {
private val dispatcher = TestCoroutineDispatcher()

override fun starting(description: Description?) {
super.starting(description)
override fun beforeAll(context: ExtensionContext?) {
Dispatchers.setMain(dispatcher)
}

override fun finished(description: Description?) {
super.finished(description)
override fun close() {
Dispatchers.resetMain()
dispatcher.cleanupTestCoroutines()
}
Expand Down
6 changes: 0 additions & 6 deletions sample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,7 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':library')

implementation kotlin.language
implementation kotlin.coroutines
implementation kotlin.coroutinesAndroid

implementation androidx.appCompat
implementation androidx.ktx
implementation androidx.constraintLayout
Expand All @@ -60,8 +56,6 @@ dependencies {
implementation moxy.androidx
kapt moxy.compiler

implementation 'ru.terrakok.cicerone:cicerone:5.1.1'

implementation glide.glide
kapt glide.compiler

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class TracksFragment : BaseFragment(), TracksView {
data: List<TracksListItem>,
state: State<TracksListItem>
) {
val hasMore = state !is State.AllData || state is State.PaginationError || state is State.NewPageLoading
val hasMore = state is State.Data || state is State.PaginationError || state is State.NewPageLoading
scrollListener.apply {
isLoading = false
this.hasMore = hasMore
Expand Down

0 comments on commit 9c995c4

Please sign in to comment.