Endless scroll kotlin recycling view/ListView - android

I am desperately trying to implement endless scrolling on an android app using kotlin. All the tutorials are either useless since they dont explain things properly. So for example:
https://github.com/chetdeva/recyclerview-bindings
it looks promising but the author uses phrases like
"put this in your BindingAdapter" so i look what this BindingAdapter is, I found a java file but if you insert anything in there I get errors. Its like anything I try fails directly.
The other tutorials are written in java and even with "translate to kotlin" option its useless since the translated code throws 100 errors.
I tried things like :
setContentView(R.layout.activity_main)
list.layoutManager = LinearLayoutManager(this)
list.hasFixedSize()
list.adapter = ListAdapter(this, getLists())
val list_view: RecyclerView = findViewById(R.id.list)
fun setRecyclerViewScrollListener() {
list_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
val height = list_view.getHeight()
val diff = height-dy
if (diff < 1000){
/*load next list */
}
}
})
}
setRecyclerViewScrollListener()
}
or this
val inflater = LayoutInflater.from(this#MainActivity)
val layout = inflater.inflate(R.layout.append_list, null, false)
button.setOnClickListener{screen.addView(layout)}
Is there a bullet proof method where you can simply append elemets like with html and js? I wrote this snippet in 2 min. Is there a similar "easy" way in Android/Kotlin?
$("#next").click(function(){
$(".append_text").append("new text <img src='http://static.webshopapp.com/shops/015426/files/005031634/560x625x2/kek-amsterdam-wandtattoo-hase-forest-friends-braun.jpg'/>")
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="next">Load</button>
<span class="append_text"> </span>
In general I recive a lot of errors for choosing the wrong Layout. I tried Listview and contrainlayout and recycling Layout and Vertical Scrolling layout and so on. Is there a simple body tag where you can simply append a xml file?
I think I go the wrong way the whole time because I see everything though the eyes of a Web. Dev. while android does not have the classical DOM. Can anybody explain it to me with an minimal example on how to append a xml file to the main xml file on button click/on scroll?

I use this method for adding endless scroll functionality to a recyclerview in Kotlin:
private fun setRecyclerViewScrollListener() {
scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val totalItemCount = recyclerView!!.layoutManager.itemCount
if (totalItemCount == lastVisibleItemPosition + 1) {
Log.d("MyTAG", "Load new list")
recycler.removeOnScrollListener(scrollListener)
}
}
}
recycler.addOnScrollListener(scrollListener)
}
the variable lastVisibleItemPosition is declared as follows:
private val lastVisibleItemPosition: Int
get() = linearLayoutManager.findLastVisibleItemPosition()
private lateinit var scrollListener: RecyclerView.OnScrollListener
Just call the setRecyclerViewScrollListener() method every time you nedd to add this functionality to the recyclerView.
Hope it helps,
Leonardo

Hmm i don't now if this solve your problem but i use this to implement an recycler view that added new data to my recycler view when the scroll come to the end:
productsListActivityBinding.recyclerViewProducts.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!recyclerView.canScrollVertically(1)){
//function that add new elements to my recycler view
}
}
})

set in recycler view in scroll listener
recycler.addOnScrollListener(your listener)

Related

Kotlin Recyclerview Stack Layout Type showing wrong views

I am attempting to write a recyclerview which has some of the Viewholders inside it as stacked ontop of one another. The idea is that you can drag the topmost view above the stacked list and have drop it above where it becomes separate.
I managed to get this working using a Recyclerview with a custom RecyclerView.ItemDecoration. However, after I drop the item, i have the adapter call notifyDataSetChange to update the background code. This causes the the next item in the stack to appear to be the wrong one (though this does change sometimes if you touch the item and start scrolling, then it displays the correct one).
The custom RecyclerView.ItemDecoration class:
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
{
val itemPosition = parent.getChildAdapterPosition(view)
val adapter = parent.adapter
if (adapter is BaseRecVAdapter)
{
val item = adapter.getDataModel(itemPosition)
if (item is DragDropModel && item.mStackedPos != PMConsts.negNum)
{
if (item.mStackedPos != 0)
{
val context = view.context
val top = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 148f, context.resources.displayMetrics).toInt()
outRect.set(0, -top, 0, 0)
return
}
}
}
super.getItemOffsets(outRect, view, parent, state)
}
The drag interface I made for the Adapter and the ItemTouchHelper.Callback can be found below:
interface ItemTouchHelperListener
{
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
fun onClearView(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?)
}
The onItem move code is as follows:
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
{
var newToPosition = toPosition
if (toPosition <= mDragUpLimit)
{//Prevent items from being dragged above maximum movement.
newToPosition = mDragUpLimit + 1
}
else if (toPosition >= mDragDownLimit)
{//Cannot drag below stacked List...
newToPosition = mDragDownLimit - 1
}
if (fromPosition < newToPosition)
{
for (i in fromPosition until newToPosition)
{
swap(mDataList, i, i + 1)
}
}
else
{
for (i in fromPosition downTo newToPosition + 1)
{
swap(mDataList, i, i - 1)
}
}
notifyItemMoved(fromPosition, newToPosition)
return true
}
I have a simple viewholder which is an invisible bar which i mark as the position you need to drag above in order to make a valid change to the list order.
I have the code call notifyDataSetChanged after the onClearView() method is called as I need to update the background features so that the next item in the stack is draggable and the background data feeding into the adapter is also updated. It seems the simplest way to keep the data updating smoothly, but I wonder if it is causing my problems
If someone would be able to give me a hand with this, I would be most grateful. I am tearing my hair out somewhat. I thought I had a good system setup but it was not quite working. I hope that this is enough information to get some help with this issue.
Thank you in advance

Drag & Dropping the first item of the RecyclerView moves several random positions

Currently, I have a RecyclerView implementing the new ListAdapter, using submitList to differ elements and proceed to update the UI automatically.
Lately i had to implement drag & drop to the list using the well known ItemTouchHelper. Here is my implementation, pretty straight forward:
class DraggableItemTouchHelper(private val adapter: DestinationsAdapter) : ItemTouchHelper.Callback() {
private val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
private val swipeFlags = 0
override fun isLongPressDragEnabled() = false
override fun isItemViewSwipeEnabled() = false
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val oldPos = viewHolder.adapterPosition
val newPos = target.adapterPosition
adapter.swap(oldPos, newPos)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
this is my swap function inside the adapter:
fun swap(from: Int, to: Int) {
submitList(ArrayList(currentList).also {
it[from] = currentList[to]
it[to] = currentList[from]
})
}
Everything works well EXCEPT when moving the FIRST item of the list. Sometimes it behaves OK, but most of the time (like 90%), it snaps several positions even when moving it slightly above the second item (to move 1st item on 2nd position for example). The new position seems random and i couldn't figure out the issue.
As a guide, i used the https://github.com/material-components/material-components-android example to implement Drag&Drop and for their (simple) list&layout works well. My list is a bit complex since it's inside a viewpager, using Navigation component and having many other views constrained together in that screen, but i don't think this should be related.
At this point i don't even know how to search on the web for this issue anymore.
The closest solution I found for this might be https://issuetracker.google.com/issues/37018279 but after implementing and having the same behaviour, I am thinking it's because I use ListAdapter which differs and updates the list asynchronously, when the solution uses RecyclerView.Adapter which uses notifyItemMoved and other similar methods.
Switching to RecyclerView.Adapter is not a solution.
This seems to be a bug in AsyncListDiffer, which is used under the hood by ListAdapter. My solution lets you manually diff changes when you need to. However, it's rather hacky, uses reflection, and may not work with future appcompat versions (The version I've tested it with is 1.3.0).
Since mDiffer is private in ListAdapter and you need to work directly with it, you'll have to create your own ListAdapter implementation(you can just copy the original source). And then add the following method:
fun setListWithoutDiffing(list: List<T>) {
setOf("mList", "mReadOnlyList").forEach { fieldName ->
val field = mDiffer::class.java.getDeclaredField(fieldName)
field.isAccessible = true
field.set(mDiffer, list)
}
}
This method silently changes the current list in the underlying AsyncListDiffer without triggering any diffing, as submitList() would.
The resulting file should look like this:
package com.example.yourapp
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
abstract class ListAdapter<T, VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH> {
private val mDiffer: AsyncListDiffer<T>
private val mListener =
ListListener<T> { previousList, currentList -> onCurrentListChanged(previousList, currentList) }
protected constructor(diffCallback: DiffUtil.ItemCallback<T>) {
mDiffer = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build()
).apply {
addListListener(mListener)
}
}
protected constructor(config: AsyncDifferConfig<T>) {
mDiffer = AsyncListDiffer(AdapterListUpdateCallback(this), config).apply {
addListListener(mListener)
}
}
fun setListWithoutDiffing(list: List<T>) {
setOf("mList", "mReadOnlyList").forEach { fieldName ->
val field = mDiffer::class.java.getDeclaredField(fieldName)
field.isAccessible = true
field.set(mDiffer, list)
}
}
open fun submitList(list: List<T>?) {
mDiffer.submitList(list)
}
fun submitList(list: List<T>?, commitCallback: Runnable?) {
mDiffer.submitList(list, commitCallback)
}
protected fun getItem(position: Int): T {
return mDiffer.currentList[position]
}
override fun getItemCount(): Int {
return mDiffer.currentList.size
}
val currentList: List<T>
get() = mDiffer.currentList
open fun onCurrentListChanged(previousList: List<T>, currentList: List<T>) {}
}
Now you need to change your adapter implementation to inherit from your custom ListAdapter rather than androidx.recyclerview.widget.ListAdapter.
Finally you'll need to change your adapter's swap() method implementation to use the setListWithoutDiffing() and notifyItemMoved() methods:
fun swap(from: Int, to: Int) {
setListWithoutDiffing(ArrayList(currentList).also {
it[from] = currentList[to]
it[to] = currentList[from]
})
notifyItemMoved(from, to)
}
An alternative solution would be to create a custom AsyncListDiffer version that lets you do the same without reflection, but this way seems easier. I will also file a feature request for supporting manual diffing out of the box and update the question with a Google Issue Tracker link.
I kept a copy of the items in my adapter, modified the copy, and used notifyItemMoved to update the UI as the user was dragging. I only save the updated items/order AFTER the user finishes dragging. This works for me because 1) I had a fixed length list of 9 items; 2) I was able to use clearView to know when the drag ended.
ListAdapter - kotlin:
var myItems: MutableList<MyItem> = mutableListOf()
fun onMove(fromPosition: Int, toPosition: Int): Boolean {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(myItems, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(myItems, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
return true
}
ItemTouchHelper.Callback() - kotlin:
// my items are only ever selected during drag, so when selection clears, drag has ended
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
// clear drag style after item moved
viewHolder.itemView.requestLayout()
// trigger callback after item moved
val itemViewHolder = viewHolder as MyItemViewHolder
itemViewHolder.onItemMovedCallback(adapter.myItems)
}
MyItemViewHolder - kotlin
fun onItemMovedCallback(reorderedItems: List<MyItem>) {
// user has finished drag
// save new item order to database or submit list properly to adapter
}
I also had an itemOrder field on MyItem. I updated that field using the index of the re-ordered items when I saved it to the DB. I could probably update each items itemOrder field when I swap the items, but it seemed pointless (I just calculate the new order after the drag is finished).
I'm using LiveData from my database. I found the views "flickered" after the final database save because I changed the itemOrder on all the items and moved the items around in the adapter list. If this happens to you and you don't like it, just temporarily disable the recycler view item animator (I achieved this by setting it to null after the drag and restoring it after the list is updated in the RecyclerView/Adapter).
This worked for me and my specific case. Let me know if you need more details.

IllegalStateException: Range start point not set

I am using Paging library form Jetpack for loading data. In order to allow users to select multiple items in RecyclerView, I have used the RecyclerView Selection library.
Now, the problem is that when the user selects an item and drags down, the app gets crashed after few items are selected. I am getting the below exception:
java.lang.IllegalStateException: Range start point not set.
I don't know what I am missing here. Also, I want to disable drag and select in the SelectionTracker but can't find a solution for that. Any help will be appreciated.
Update
I am attaching the necessary code used for the multi-selection below.
Adapter
fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getPosition(): Int = adapterPosition
override fun getSelectionKey(): Long? = itemId
}
ItemDetailsLookup
class HomeItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
override fun getItemDetails(event: MotionEvent): ItemDetails<Long>? {
val view = recyclerView.findChildViewUnder(event.x, event.y)
if (view != null) {
return (recyclerView.getChildViewHolder(view) as HomeViewHolder).getItemDetails()
}
return null
}
}
Fragment
selectionTracker = SelectionTracker.Builder<Long>(
"mySelection",
rvHome,
StableIdKeyProvider(rvHome),
HomeItemDetailsLookup(rvHome),
StorageStrategy.createLongStorage()
).build()
homeAdapter.tracker = selectionTracker
While combining paging library and selection library there exists this bug.
No solutions have been found so far.
It happens when paging library calls notifyItemRangeInserted on adapter which cause DefaultSelectionTracker.endRange method trigger that set DefaultSelectionTracker.mRange to null.
Better try updating your libraries to latest alpha and try again

OnBindViewHolder does not apply after notifyitemmoved () in Android Recyclerview

The code above is the RecyclerViewAdapter, which changes color only when it is the first item, as shown below.
class TestAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val textColor1 = Color.BLACK
private val textColor2 = Color.YELLOW
private val items = ArrayList<String>()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textColor = if(position==0) textColor1 else textColor2
holder.itemView.textView.setTextColor(textColor)
holder.itemView.textView.text = items[position]
}
fun move(from:Int,to:Int){
val item = items[from]
items.remove(item)
items.add(to,item)
notifyItemMoved(from,to)
}
}
In this state I would like to move Value 3 to the first position using the move function. The results I want are shown below.
But in fact, it shows the following results
When using notifyDataSetChanged, I can not see the animation transition effect,
Running the onBindViewHolder manually using findViewHolderForAdapterPosition results in what I wanted, but it is very unstable. (Causing other parts of the error that I did not fix)
fun move(from:Int,to:Int){
val item = items[from]
val originTopHolder = recyclerView.findViewHolderForAdapterPosition(0)
val afterTopHolder = recyclerView.findViewHolderForAdapterPosition(from)
items.remove(item)
items.add(to,item)
notifyItemMoved(from,to)
if(to==0){
onBindViewHolder(originTopHolder,1)
onBindViewHolder(afterTopHolder,0)
}
}
Is there any other way to solve this?
Using the various notifyItemFoo() methods, like moved/inserted/removed, doesn't re-bind views. This is by design. You could call
if (from == 0 || to == 0) {
notifyItemChanged(from, Boolean.FALSE);
notifyItemChanged(to, Boolean.FALSE);
}
in order to re-bind the views that moved.
notifyItemMoved will not update it. According to documentation:
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter
This is a structural change event. Representations of other existing items in the data set are still considered up to date and will not be rebound, though their positions may be altered.
What you're seeing is expected.
Might want to look into using notifyItemChanged, or dig through the documentation and see what works best for you.

How to visually stay on same scroll position, upon unknown number of RecyclerView data changes (insertion, deletion, updates)?

Background
In case your RecyclerView gets new items, it is best to use notifyItemRangeInserted, together with unique, stable id for each item, so that it will animate nicely, and without changing what you see too much:
As you can see, the item "0", which is the first on the list, stays on the same spot when I add more items before of it, as if nothing has changed.
The problem
This is a great solution, which will fit for other cases too, when you insert items anywhere else.
However, it doesn't fit all cases. Sometimes, all I get from outside, is : "here's a new list of items, some are new, some are the same, some have updated/removed" .
Because of this, I can't use notifyItemRangeInserted anymore, because I don't have the knowledge of how many were added.
Problem is, if I use notifyDataSetChanged, the scrolling changes, because the amount of items before the current one have changed.
This means that the items that you look at currently will be visually shifted aside:
As you can see now, when I add more items before the first one, they push it down.
I want that the currently viewable items will stay as much as they can, with priority of the one at the top ("0" in this case).
To the user, he won't notice anything above the current items, except for some possible end cases (removed current items and those after, or updated current ones in some way). It would look as if I used notifyItemRangeInserted.
What I've tried
I tried to save the current scroll state or position, and restore it afterward, as shown here, but none of the solutions there had fixed this.
Here's the POC project I've made to try it all:
class MainActivity : AppCompatActivity() {
val listItems = ArrayList<ListItemData>()
var idGenerator = 0L
var dataGenerator = 0
class ListItemData(val data: Int, val id: Long)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val inflater = LayoutInflater.from(this#MainActivity)
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false)) {}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
val textView = holder!!.itemView as TextView
val item = listItems[position]
textView.text = "item: ${item.data}"
}
override fun getItemId(position: Int): Long = listItems[position].id
override fun getItemCount(): Int = listItems.size
}
adapter.setHasStableIds(true)
recyclerView.adapter = adapter
for (i in 1..30)
listItems.add(ListItemData(dataGenerator++, idGenerator++))
addItemsFromTopButton.setOnClickListener {
for (i in 1..5) {
listItems.add(0, ListItemData(dataGenerator++, idGenerator++))
}
//this is a good insertion, when we know how many items were added
adapter.notifyItemRangeInserted(0, 5)
//this is a bad insertion, when we don't know how many items were added
// adapter.notifyDataSetChanged()
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.user.recyclerviewadditionwithoutscrollingtest.MainActivity">
<Button
android:id="#+id/addItemsFromTopButton" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginRight="8dp"
android:text="add items to top" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="#+id/recyclerView"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp"
android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="8dp" android:orientation="vertical"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
The question
Is it possible to notify the adapter of various changes, yet let it stay on the exact same place?
Items that are viewed currently would stay if they can, or removed/updated as needed.
Of course, the items' ids will stay unique and stable, but sadly the cells size might be different from one another.
EDIT: I've found a partial solution. It works by getting which view is at the top, get its item (saved it inside the viewHolder) and tries to scroll to it. There are multiple issues with this though:
If the item was removed, I will have to somehow scroll to the next one, and so on. I think in the real app, I can manage to do it. Wonder if there is a better way though.
Currently it goes over the list to get the item, but maybe in the real app I can optimize it.
Since it just scrolls to the item, if puts it at the top edge of the RecyclerView, so if you've scrolled a bit to show it partially, it will move a bit:
Here's the new code :
class MainActivity : AppCompatActivity() {
val listItems = ArrayList<ListItemData>()
var idGenerator = 0L
var dataGenerator = 0
class ListItemData(val data: Int, val id: Long)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = object : RecyclerView.Adapter<ViewHolder>() {
val inflater = LayoutInflater.from(this#MainActivity)
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
return ViewHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val textView = holder.itemView as TextView
val item = listItems[position]
textView.text = "item: ${item.data}"
holder.listItem = item
}
override fun getItemId(position: Int): Long = listItems[position].id
override fun getItemCount(): Int = listItems.size
}
adapter.setHasStableIds(true)
recyclerView.adapter = adapter
for (i in 1..30)
listItems.add(ListItemData(dataGenerator++, idGenerator++))
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
addItemsFromTopButton.setOnClickListener {
for (i in 1..5) {
listItems.add(0, ListItemData(dataGenerator++, idGenerator++))
}
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val holder = recyclerView.findViewHolderForAdapterPosition(firstVisibleItemPosition) as ViewHolder
adapter.notifyDataSetChanged()
val listItemToGoTo = holder.listItem
for (i in 0..listItems.size) {
val cur = listItems[i]
if (listItemToGoTo === cur) {
layoutManager.scrollToPositionWithOffset(i, 0)
break
}
}
//TODO think what to do if the item wasn't found
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var listItem: ListItemData? = null
}
}
I would solve this problem using the DiffUtil api. DiffUtil is meant to take in a "before" and "after" list (that can be as similar or as different as you want) and will compute for you the various insertions, removals, etc that you would need to notify the adapter of.
The biggest, and nearly only, challenge in using DiffUtil is in defining your DiffUtil.Callback to use. For your proof-of-concept app, I think things will be quite easy. Please excuse the Java code; I know you posted originally in Kotlin but I'm not nearly as comfortable with Kotlin as I am with Java.
Here's a callback that I think works with your app:
private static class MyCallback extends DiffUtil.Callback {
private List<ListItemData> oldItems;
private List<ListItemData> newItems;
#Override
public int getOldListSize() {
return oldItems.size();
}
#Override
public int getNewListSize() {
return newItems.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).id == newItems.get(newItemPosition).id;
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).data == newItems.get(newItemPosition).data;
}
}
And here's how you'd use it in your app (in java/kotlin pseudocode):
addItemsFromTopButton.setOnClickListener {
MyCallback callback = new MyCallback();
callback.oldItems = new ArrayList<>(listItems);
// modify listItems however you want... add, delete, shuffle, etc
callback.newItems = new ArrayList<>(listItems);
DiffUtil.calculateDiff(callback).dispatchUpdatesTo(adapter);
}
I made my own little app to test this out: each button press would add 20 items, shuffle the list, and then delete 10 items. Here's what I observed:
When the first visible item in the "before" list also existed in the "after" list...
When there were enough items after it to fill the screen, it stayed in place.
When there were not, the RecyclerView scrolled to the bottom
When the first visible item in the "before" list did not also exist int he "after" list, the RecyclerView would try to keep whichever item that did exist in both "before" + "after" and was closest to the first visible position in the "before" list in the same position, following the same rules as above.

Categories

Resources