Skip to content

Commit

Permalink
Improve diffing (#11)
Browse files Browse the repository at this point in the history
* improved diffing by using onBindViewHolder with payload
* fixed crash when transitioning from empty item to the first one.
* improved empty item handling, notifying item change only when it's the first position. 
* enhanced demo app 
* updated readme.
  • Loading branch information
gotev authored Apr 2, 2019
1 parent f336018 commit 0512d9b
Show file tree
Hide file tree
Showing 16 changed files with 82 additions and 39 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ In this way every item of the recycler view has its own set of files, resulting
## <a name="setup"></a>Setup
In your gradle dependencies add:
```groovy
def recyclerAdapterVersion = "2.4.2"
def recyclerAdapterVersion = "2.5.0"
implementation "net.gotev:recycleradapter:$recyclerAdapterVersion"
```

Expand Down Expand Up @@ -76,7 +76,9 @@ open class ExampleItem(private val context: Context, private val text: String)

override fun getLayoutId() = R.layout.item_example

override fun bind(holder: ExampleItem.Holder) {
override fun bind(firstTime: Boolean, holder: ExampleItem.Holder) {
// you can use firstTime to discriminate between bindings you
// need only the first time the item is binded from the others
holder.titleField.text = text
}

Expand Down Expand Up @@ -294,7 +296,7 @@ open class ExampleItem(private val context: Context, private val text: String)

override fun onFilter(searchTerm: String) = text.contains(searchTerm)

override fun bind(holder: Holder) {
override fun bind(firstTime: Boolean, holder: Holder) {
holder.titleField.text = text
holder.subtitleField.text = "subtitle"
}
Expand Down Expand Up @@ -396,7 +398,7 @@ class TextWithButtonItem(private val text: String) : AdapterItem<TextWithButtonI

override fun diffingId() = javaClass.name + text

override fun bind(holder: Holder) {
override fun bind(firstTime: Boolean, holder: Holder) {
holder.textViewField.text = text
holder.buttonField.isChecked = pressed
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class SelectionActivity : AppCompatActivity() {
recyclerAdapter.setEmptyItem(LabelItem(getString(R.string.empty_list)))

recycler_view.apply {
itemAnimator?.changeDuration = 0
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
adapter = recyclerAdapter
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ class SyncActivity : AppCompatActivity() {
SyncItem(2, "listA")
)

private fun listC() =
arrayListOf(
SyncItem(1, "listC")
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync)
Expand Down Expand Up @@ -85,6 +90,14 @@ class SyncActivity : AppCompatActivity() {
recyclerAdapter.syncWithItems(listB())
}

syncC.setOnClickListener {
recyclerAdapter.syncWithItems(listC())
}

empty.setOnClickListener {
recyclerAdapter.clear()
}

shuffle.setOnClickListener {
scheduledOperation = if (scheduledOperation == null) {
shuffle.text = getString(R.string.button_shuffle_stop)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class LabelItem(private val text: String, private val selectionGroup: String? =

override fun getSelectionGroup() = selectionGroup

override fun bind(holder: Holder) {
override fun bind(firstTime: Boolean, holder: Holder) {
holder.textViewField.text = text
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ open class SelectableItem(val label: String, private val group: String)
return super.onSelectionChanged(isNowSelected)
}

override fun bind(holder: Holder) {
override fun bind(firstTime: Boolean, holder: Holder) {
holder.toggleField.apply {
isChecked = selected
text = label
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class TextWithToggleItem(private val text: String) : AdapterItem<TextWithToggleI

override fun onFilter(searchTerm: String) = text.contains(searchTerm, ignoreCase = true)

override fun bind(holder: Holder) {
override fun bind(firstTime: Boolean, holder: Holder) {
holder.textViewField.text = text
holder.buttonField.isChecked = pressed
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ open class TitleSubtitleItem(private val title: String, private val subtitle: St

override fun onFilter(searchTerm: String) = title.contains(searchTerm, ignoreCase = true)

override fun bind(holder: Holder) {
override fun bind(firstTime: Boolean, holder: Holder) {
holder.titleField.text = title
holder.subtitleField.text = subtitle
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class TitledCarousel(

override fun getLayoutId() = R.layout.item_titled_carousel

override fun bind(holder: Holder) {
super.bind(holder)
override fun bind(firstTime: Boolean, holder: Holder) {
super.bind(firstTime, holder)
holder.title.text = title
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MyLeaveBehindItem(private val value: String, private val background: Strin

override fun onFilter(searchTerm: String) = value.contains(searchTerm, ignoreCase = true)

override fun bind(holder: Holder) {
override fun bind(firstTime: Boolean, holder: Holder) {
holder.nameField.text = value
holder.deleteField.text = background
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private class PagedRecyclerAdapter
adapter.onCreateViewHolder(parent, viewType)

override fun onBindViewHolder(holder: RecyclerAdapterViewHolder, position: Int) =
(getItem(position) as AdapterItem<RecyclerAdapterViewHolder>).bind(holder)
(getItem(position) as AdapterItem<RecyclerAdapterViewHolder>).bind(true, holder)

override fun submitList(pagedList: PagedList<AdapterItem<*>>?) {
if (viewType == null) {
Expand Down
23 changes: 22 additions & 1 deletion app/demo/src/main/res/layout/activity_sync.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
android:layout_height="match_parent"
tools:context="net.gotev.recycleradapterdemo.activities.MainActivity">

<com.google.android.material.button.MaterialButton
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacer_default"
android:layout_marginBottom="@dimen/spacer_default"
android:text="@string/button_empty"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
Expand All @@ -16,7 +27,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/empty" />

<com.google.android.material.button.MaterialButton
android:id="@+id/shuffle"
Expand Down Expand Up @@ -50,4 +61,14 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/syncA" />

<com.google.android.material.button.MaterialButton
android:id="@+id/syncC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacer_default"
android:layout_marginBottom="@dimen/spacer_default"
android:text="@string/button_sync_group_c"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/syncB" />

</androidx.constraintlayout.widget.ConstraintLayout>
6 changes: 4 additions & 2 deletions app/demo/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
<string name="multiple_selection">Group with multiple selection</string>
<string name="show_selections">Show Selections</string>
<string name="search_hint">Search…</string>
<string name="button_shuffle_start">Start Shuffle</string>
<string name="button_shuffle_stop">Stop Shuffle</string>
<string name="button_shuffle_start">Shuffle</string>
<string name="button_shuffle_stop">Stop</string>
<string name="button_sync_group_a">Sync A</string>
<string name="button_sync_group_b">Sync B</string>
<string name="button_sync_group_c">Sync C</string>
<string name="button_empty">Empty list</string>
<string name="infinite_scrolling">Infinite Scrolling</string>
<string name="carousels_plain">Carousels (plain)</string>
<string name="carousels_pool">Carousels (with pool)</string>
Expand Down
2 changes: 1 addition & 1 deletion manifest.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ext {
library_licenses = ["Apache-2.0"]
library_licenses_url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
library_project_group = 'net.gotev'
library_version = '2.4.2'
library_version = '2.5.0'
version_code = 5
min_sdk = 18
target_sdk = 28
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ abstract class NestedRecyclerAdapterItem<T : NestedRecyclerAdapterItem.Holder>(
open fun getLayoutManager(context: Context): RecyclerView.LayoutManager =
LinearLayoutManager(context, HORIZONTAL, false)

override fun bind(holder: T) {
override fun bind(firstTime: Boolean, holder: T) {
with(holder.recyclerView) {
// reset layout manager and adapter
layoutManager = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@ abstract class AdapterItem<T : RecyclerAdapterViewHolder> : Comparable<AdapterIt
// hierarchy is present, as the first one should be the last of the subclasses
for (cl in javaClass.classes) {
if (RecyclerAdapterViewHolder::class.java.isAssignableFrom(cl)) {
val clazz = cl as Class<T>
val holder = clazz.getConstructor(View::class.java).newInstance(view)
onHolderCreated(holder)
return holder
return (cl as Class<T>).getConstructor(View::class.java).newInstance(view)
}
}

Expand All @@ -95,17 +92,12 @@ abstract class AdapterItem<T : RecyclerAdapterViewHolder> : Comparable<AdapterIt

}

/**
* Perform initialization stuff on the holder. This is done only once after the holder has
* been created.
*/
open fun onHolderCreated(holder: T) {}

/**
* Bind the current item with the view
* @param firstTime true if it's the first time this item is being bound
* @param holder ViewHolder on which to bind data
*/
abstract fun bind(holder: T)
abstract fun bind(firstTime: Boolean, holder: T)

/**
* Returns the ID of this item's selection group. By default it's null.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
setHasStableIds(true)
}

private fun notifyChangedPosition(position: Int) {
notifyItemChanged(position, true)
}

@Suppress("UNCHECKED_CAST")
private fun <T : RecyclerAdapterViewHolder> AdapterItem<out T>.castAsIn(): AdapterItem<in T> {
return this as AdapterItem<in T>
Expand All @@ -110,7 +114,7 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
this.let {
selected = newStatus
if (onSelectionChanged(isNowSelected = newStatus)) {
notifyItemChanged(position)
notifyChangedPosition(position)
}
}
}
Expand All @@ -120,7 +124,7 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
private fun updateItemAtPosition(item: AdapterItem<in RecyclerAdapterViewHolder>,
position: Int) {
items[position] = item
notifyItemChanged(position)
notifyChangedPosition(position)
}

private fun registerItemType(item: AdapterItem<*>) {
Expand All @@ -131,10 +135,10 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
}
}

private fun removeEmptyItemIfItHasBeenConfigured() {
private fun removeEmptyItemIfItHasBeenConfigured(insertPosition: Int) {
// this is necessary to prevent IndexOutOfBoundsException on RecyclerView when the
// first item gets added and an empty item has been configured
if (items.size >= 1 && emptyItem != null) {
if (insertPosition == 0 && items.size >= 1 && emptyItem != null) {
notifyItemChanged(0)
}
}
Expand Down Expand Up @@ -163,15 +167,23 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
return createItemViewHolder(parent, item)
}

override fun onBindViewHolder(holder: RecyclerAdapterViewHolder, position: Int) {
private fun bindItem(holder: RecyclerAdapterViewHolder, position: Int, firstTime: Boolean) {
val item = if (adapterIsEmptyAndEmptyItemIsDefined()) {
emptyItem!!
} else {
items[position]
}

holder.setAdapter(this)
item.bind(holder)
item.bind(firstTime, holder)
}

override fun onBindViewHolder(holder: RecyclerAdapterViewHolder, position: Int, payloads: MutableList<Any>) {
bindItem(holder, position, payloads.isEmpty())
}

override fun onBindViewHolder(holder: RecyclerAdapterViewHolder, position: Int) {
bindItem(holder, position, true)
}

override fun getItemCount() = if (adapterIsEmptyAndEmptyItemIsDefined()) 1 else items.size
Expand Down Expand Up @@ -199,7 +211,7 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
override fun notifyItemChanged(holder: RecyclerAdapterViewHolder) {
val position = holder.adapterPosition.takeIf { !it.isOutOfItemsRange() } ?: return

notifyItemChanged(position)
notifyChangedPosition(position)
}

private fun selectItemAtPosition(position: Int) {
Expand Down Expand Up @@ -301,7 +313,7 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
}

registerItemType(item)
removeEmptyItemIfItHasBeenConfigured()
removeEmptyItemIfItHasBeenConfigured(insertPosition)
notifyItemInserted(insertPosition)
return this
}
Expand Down Expand Up @@ -336,7 +348,7 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
}
}

removeEmptyItemIfItHasBeenConfigured()
removeEmptyItemIfItHasBeenConfigured(firstIndex)
notifyItemRangeInserted(firstIndex, newItems.size)
return this
}
Expand Down Expand Up @@ -433,8 +445,8 @@ class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapterViewHolder>(), Recyc
} else {
items[internalItemIndex] = newItem.castAsIn()
registerItemType(newItem)
removeEmptyItemIfItHasBeenConfigured()
notifyItemChanged(internalItemIndex)
removeEmptyItemIfItHasBeenConfigured(internalItemIndex)
notifyChangedPosition(internalItemIndex)
}
} else {
if (internalItemIndex != newItemsIndex) {
Expand Down

0 comments on commit 0512d9b

Please sign in to comment.