I'm new in Android dev and now porting my iOS app.
Trying to make pretty complex RecyclerView, but at some moment behavoir of the specific row is duplicated on other row after notifyDataSetChanged() method.
There are three rows with same ViewType in first part of RecyclerView
Each of them has TextView and EditText widgets, that I'm populating in CustomViewHolder class.
First and second rows should work as always: when I'm click in EditText - the keyboard opens. But third row EditText's focus should initiate dialog alert. Everything works great until reload of adapter's DataSet. After DataSet reload the first row's EditText also begins to open the dialog alert instead of normal opening of the keyboard.
Looks like I'm missing something and somehow referencing to the same object when customizing my rows. Here's my adapter code (simplified):
class NewRequestsRecyclerAdapter(val context: Context, val parameters:ArrayList<NewRequestsFragment.ParameterCell>,val delegate:NewRequestProtocol?): RecyclerView.Adapter<NewRequestsRecyclerAdapter.CustomViewHolder>() {
enum class RowType {
Header,Parameter
}
override fun getItemCount(): Int {
// count logic
}
override fun onCreateViewHolder(parent : ViewGroup, viewType: Int): CustomViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val cellForRow = when (RowType.values()[viewType]) {
RowType.Header -> layoutInflater.inflate(R.layout.cell_header,parent,false)
RowType.Parameter -> layoutInflater.inflate(R.layout.cell_parameter_new_requests,parent,false)
}
return CustomViewHolder(cellForRow, RowType.values()[viewType])
}
override fun getItemViewType(position: Int): Int {
// Here's ItemViewType logic ...
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.bindMenu(position)
}
inner class CustomViewHolder(val cellView: View, val type:RowType): RecyclerView.ViewHolder(cellView) {
fun bindMenu(row:Int) {
when (type) {
RowType.Header -> {
val nameView = cellView.findViewById<TextView>(R.id.headerName)
// other logic to populate Header views
}
RowType.Parameter -> {
val nameView = cellView.findViewById<TextView>(R.id.paramName)
val editText = cellView.findViewById<EditText(R.id.paramEditText)
nameView.text = parameters[row-1].name
editText.apply {
hint = parameters[row-1].placeholder
when (parameters[row-1].type) {
NewRequestsFragment.PartsCellType.Name -> {
setText(delegate?.currentItem?.name)
}
NewRequestsFragment.PartsCellType.Number -> {
setText(delegate?.currentItem?.number)
}
NewRequestsFragment.PartsCellType.StateType-> {
setText(delegate?.currentItem?.state)
showSoftInputOnFocus = false
setOnFocusChangeListener { view, changed ->
if (changed) {
inputType = InputType.TYPE_NULL
delegate?.showStateDialog()
}
}
}
}
}
}
I know to resolve that problem I can show alert with button, but I would like to know why my code leads to this behavior.
Could you please guide me what I'm missing?
This type of problem with a RecyclerView where one item mysteriously takes on the attributes or behavior of another item is usually due to not resetting the view holder.
You are defining the behavior of your view holders when they are created, so, the first time, all view holders are created and behave appropriately. When things change, the view holders are reused and not recreated. As a result, things can get mixed up such as getting a dialog opened when the keyboard should show.
To correct this, reset the view holder when it is bound to behave the way you want.
Related
To make ViewHolder treat each radio button separately and not reuse previous radio button's state I stored the state of whether or not a radio button item is checked in a list of booleans and set the radio buttons state according to that list. but when I scroll down and up the toggled radio buttons get untoggled randomly even though the isCheckedList contains the right boolean information for each radio button(I print the list with logd() to verify if it actually stores the states)
Any Idea of what I'm missing?
class QuestionAdapter(context: Context) :
RecyclerView.Adapter<QuestionAdapter.QuestionViewHolder>() {
private val listOfQuestion: List<String>
private val listSize = 20
private val isCheckedList: MutableList<Boolean> = MutableList(listSize) { false }
//Initialize a list of 20 questions.
init {
val allQuestions = context.resources.getStringArray(R.array.questions).toList()
listOfQuestion = allQuestions
.shuffled()
.take(listSize)
}
class QuestionViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val questionText = view.findViewById<TextView>(R.id.question)
val radioButton = view.findViewById<RadioButton>(R.id.yes)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionViewHolder {
val layout = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_view, parent, false)
return QuestionViewHolder(layout)
}
override fun onBindViewHolder(holder: QuestionViewHolder, position: Int) {
val item = listOfQuestion[position]
holder.questionText.text = item
holder.radioButton.isChecked = isCheckedList[position]
holder.radioButton.setOnCheckedChangeListener { _, isChecked ->
isCheckedList[holder.adapterPosition] = isChecked
Log.d("Adapter", "list: $isCheckedList ")
}
}
override fun getItemCount(): Int {
return listOfQuestion.size
}
}
You need to clear the selection on the radio button before setting its correct value.
holder.radioGroup.clearCheck()
This way, the RadioGroup clears its selection, ready to receive an input that will trigger the correct RadioButton as checked.
But you need to be aware that this might call your onCheckedChanged listeners and could bring unexpected outcomes. But there is a work around for the resulting problem.
// Nullify listener
holder.radioGroup.setOnCheckedChangeListener(null);
// Clear selection
holder.radioGroup.clearCheck();
// Reset listener after selection has been cleared
holder.radioGroup.setOnCheckedChangeListener(checkedChangeListener);
I solved the problem by adding this line of code holder.radioGroup.clearCheck() before setting the state of radio buttons.
For more details this is a blog talking about the same issue and how he solved it.
I just saw this example class for an Adapter for Recyclerview and I'm a bit confused on how it knows to call onCreateViewHolder, onBindViewHolder, etc just from adding an Item object to a list?
Does it have something to do with the line notifyItemInserted(items.size - 1) ?
Is it that whenever this method is called the onCreateViewHolder method is recalled with for that item or?
Adapter:
class ListAdapter (
private val items: MutableList<Item>
) : RecyclerView.Adapter <ListAdapter.ListViewHolder>() {
class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.list_items, parent, false)
)
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val currItem = items[position]
holder.itemView.apply {
tv_item.text = currItem.title
cb_item.isChecked = currItem.checked
crossItem(tv_item, currItem.checked)
cb_item.setOnCheckedChangeListener { _, isChecked ->
crossItem(tv_item, isChecked)
currItem.checked = !currItem.checked
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
}
}
}
override fun getItemCount(): Int {
return items.size
}
private fun crossItem (itemText: TextView, checked: Boolean) {
if (checked){
//dk wtf paint flags is
itemText.paintFlags = itemText.paintFlags or STRIKE_THRU_TEXT_FLAG
}
//else remove
else {
itemText.paintFlags = itemText.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
fun addItem (item: Item){
items.add (item)
notifyItemInserted(items.size - 1)
}
}
Item Class:
data class Item (
val title: String,
var checked: Boolean = false
)
{
}
Whenever the Adapter needs to provide a new view for the RecyclerView to draw, it checks if it has an unused ViewHolder in its pool. If it doesn't, it calls onCreateViewHolder() so it can create one. Then it calls onBindViewHolder() for the ViewHolder that came from either source so the contained view can be prepared before being added to the layout.
If you call one of the notify methods, that triggers it to refresh whichever item rows are affected. It will return any removed rows to the ViewHolder pool and then follow the above steps to get the views it needs for new rows. If you use a notify...changed method, it will only need to use onBindViewHolder() for the applicable rows. When you use the nuclear option notifyDataSetChanged(), it returns all items to the pool.
When the RecyclerView is first displayed, or when the layout is resized, those actions will possibly trigger the need to show more rows. When you scroll the list, items that scroll off the screen are returned to the ViewHolder pool, and when new items scroll into view, ViewHolders need to be created or acquired from the pool as explained above.
By the way, this is going to look ugly because it refreshes the whole list even though only some items are removed:
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
I recommend this instead so you get a nice transition:
for (i in items.indices.reversed()) {
if (items[i].checked) {
items.removeAt(i)
notifyItemRemoved(i)
}
}
I iterate in reverse so the indices that are removed are stable as you iterate and remove items.
override fun getItemCount(): Int {
return items.size
}
This function is the key, it knows how many to create and how many to bind by knowing how many there are in total. The amount of ViewHolders created is more based on how many Views can fit on the screen at one time.
This gets more complex when you have different view types, as it will sometimes has to create more ViewHolders than what was required from the start as view types change.
The notify... functions just let the Adapter know it needs to "re-look" at the List.
I want to change background color of recycler view row(item) by clicking on it, i use kotlin.
i tried different methods,The item that is clicked changed background color correctly, but when I scroll down I see that another item has also changed background color.
In fact, for every click on the item, another item in the part of the list that is not in the user's view is also affected.
Please explain this problem with a complete example.
Please be sure to test it yourself
I really need to fix this
Thanks a lot
it's quite easy to do :) You need to just remember how the RecyclerView works:
It creates few ViewHolders
When you scroll your list, the ViewHolders are reused.
If you have for example 100 items on the list, it will create 5-6 ViewHolders and will reuse them.
So having that in mind, when onBindViewHolder() is called, the easiest thing you can do is to check some state of the item, and then choose background color of the item. In this scenario you can select/deselect multiple items in the list. If you want to have only one checked, you will need to change the items.map {} function a little bit:
Please keep in mind that I wrote below from my head, you will also need to override some more functions in adapter etc.
class MyAdapter(private val onItemClick: (YouItem) -> Unit): RecyclerView.Adapter() {
private var items: List<YourItem>
fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val payload = items[holder.adapterPosition]
holder.itemView.setOnClickListener {
onItemClick(payload)
}
holder.itemView.background = if(payload.someState) firstBackground else otherBackground
}
fun updateItems(updatedItems: List<YourItem>) {
items = updatedItems
notifyDataSetChanged()
}
}
class YourActivity: Activity() {
private lateinit var items: List<YourItem>
fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
...
items: List<YourItem> = getItemsFromSomewhere()
val adapter = MyAdapter { clickedItem ->
val updatedItems = items.map {
if(it == clickedItem) {
it.copy(someState = !it.someState)
} else {
it
}
items = updatedItems
adapter.updateItems(updatedItems)
}
}
}
data class YourItem(val someState: Boolean)
This is mostly because of RecyclerView reuses the view(item row) so that the view need not to be generated multiple times. you can solve the problem by setting the background only for a particular item by changing the value of the the object and for all others keep it default value.
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
if(position == selected_item) {
holder.view.setBackgroundColor(Color.parseColor("#00aaff"));
} else {
holder.view.setBackgroundColor(Color.parseColor("#00000")); //actually you should set to the normal text color
}
}
or
public void onItemClick() {
Model model = itemList.get(clickedPosition);
model.setBgColor = Color.parseColor("#00aaff");
}
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
Model model = itemList.get(position);
holder.view.setBackgroundColor(model.getBgColor());
}
I am building an app where user is required to fill some data in order to post something, so a fragment consists of EditText, radio buttons and Spinner along with RecyclerView which dynamically renders a number of child layout containing TextView and EditText.
So when user select category from Spinner, some properties which are related to that category are displayed in RecyclerView and user can optionally fill some of them.
I have tried to implement this functionality using callback and TextWatcher but I don't get the values I want.
CallBack
interface PropertiesCallback {
fun addProp(position: Int, title: String, value: String)
}
Adapter
class PropertiesAdapter(private val propertiesCallback: PropertiesCallback)
: RecyclerView.Adapter<PropertiesAdapter.ViewHolder>() {
private var list = listOf<CategoriesAndSubcategoriesQuery.Property>()
fun setData(listOfProps: List<CategoriesAndSubcategoriesQuery.Property>) {
this.list = listOfProps
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.z_property_input, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(list[position], position)
}
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
private val label: TextView = view.findViewById(R.id.label)
private val input: EditText = view.findViewById(R.id.input)
fun bind(prop: CategoriesAndSubcategoriesQuery.Property, position: Int) {
label.text = prop.title()
prop.hint()?.let { input.hint = prop.hint() }
input.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
propertiesCallback.addProp(position, prop.title(), input.text.toString())
}
})
}
}
}
In Fragment
private var propertiesList = mutableListOf<CategoriesAndSubcategoriesQuery.Property>()
private var propertiesInputList = mutableListOf<ProductPropertiesInput>()
private fun setUpSubcategorySpinner() {
subcategoriesAdapter = ArrayAdapter(
this#AddProductFragment.context!!,
android.R.layout.simple_spinner_item,
subcategoriesList
)
//Subcategories
subcategoriesAdapter.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line)
subcategory_spinner.adapter = subcategoriesAdapter
subcategory_spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
subcategoryId = subcategoriesList[position].id()
//Adding properties
subcategoriesList[position].properties()?.let {
//Clear previous properties data of another subcategory.
propertiesInputList.clear()
propertiesList.clear()
propertiesList.addAll(it)
propertiesAdapter.setData(propertiesList)
propertiesAdapter.notifyDataSetChanged()
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
overide
override fun addProp(position: Int, title: String, value: String) {
val prop = ProductPropertiesInput
.builder()
.title(title)
.value(value)
.build()
propertiesInputList.add(prop)
//Log.d(TAG, "prop: ${prop.title()} : ${prop.value()}")
}
submit fun
private fun submitProduct() {
//Initializing properties.
val properties: Any
//The keys needed in final list.
val propertyKeys = propertiesList.map { it.title() }
//Removing objects which keys are not needed.
propertiesInputList.removeAll { it.title() !in propertyKeys }
Log.d(TAG, "propertiesInputList: $propertiesInputList")
//Removing duplicate and assign result in properties var.
properties = propertiesInputList
.distinctBy { it.title() }
Log.d(TAG, "properties: $properties")
for (prop in properties) {
Log.d(TAG, "properties , title: ${prop.title()}, value: ${prop.value()} ")
}
}
Above codes is intended to work as. When user types a value in one of the EditText in RecyclerView the value will be taken to fragment and added to an object which takes title and value and then added to propertiesInputList.
Problem 1: propertiesInputList will have so many duplicates objects with the same title and I thought the best solution was using distinctBy.
Problem 2: When user fills a number of EditText which are related to let's say category1 and changes his mind and select another category from Spinner. The previous values which are not part of new chosen category remain in propertiesInputList list. So I thought the best solution was to clear propertiesInputList and using removeAll with the titles related to category to filter unwanted objects.
But now I get only the first letter user types. If user types shoes I get s. So it seems distinctBy returns the first object but I want to get exactly last word user typed and if the user typed and erased everything I want blank.
Is there a better solution to handle this? Like looping recyclerView only when user press submit instead of TextWatcher? Or which part should I fix to make this work?
I don't completely understand what you are trying to achieve here. EditTexts inside a RecyclerView is generally not a good idea for following reasons.
When the recyclerView is scrolled, you would want to preserve the
text added by the user for that particular field/item and show it
correctly when the user scrolls back.
When you add a TextWatcher to an EditText, you also need to remove it when the view is recycled or the view holder is bound again. Otherwise, you will end up with multiple listeners and things will go wrong.
For the other question that you have,
But now I get only the first letter user types. If user types shoes I get s
That's by design. TextWatcher would emit event every time a character is entered. So you would get s, sh, sho, shoe, shoes. So you can not take an action on this data because the user is still adding something to that field.
So,
You don't know when the user has stopped adding the text to the EditText (or whether user is done). You could use something like debounce but that is complicated. You should give a button to the user. Take the value when the user taps the button.
I am assuming you have multiple edittexts in the RecyclerView. So you would need to store the values for each edittext because the recyclerview will re-use the views and you'll lose the data. You could do that in your adapter's onViewRecycled callback. Keep a map of id -> string where you store this data and retrieve when the view holder is bound.
You could also use a TextWatcher but you would have detach it before attaching a new one or in onViewRecycled.
Update:
If I had something like this, I would use a ScrollView with a vertical LinearLayout (for simplicity) and add EditText based on the requirements. If you want to add TextWatcher, you'd need some kind of stable id.
class EditTextContainer : LinearLayout {
private val views = mutableListOf<EditText>()
private val textWatchers = hashMapOf<Int, TextWatcher>()
... constructor and bunch of stuff
fun updateViews(items: List<Item>, textCallback: (id, text) -> Unit) {
// Remove text watchers
views.forEach { view ->
view.removeTextWatcher(textWatchers[view.id])
}
// More views than required
while (views.size > items.size) {
val view = views.removeAt(views.size-1)
removeView(view)
}
// Less views than required
while (view.size < items.size) {
val view = createView()
view.id = View.generateViewId()
addView(view, createParams()) // Add this view to the container
views.add(view)
}
// Update the views
items.forEachIndexed { index, item ->
val editText = views[item]
// Update your edittext.
addTextWatcher(editText, item.id, textCallback)
}
}
private fun createView(): EditText {
// Create new view using inflater or just constructor and return
}
private fun createParams(): LayoutParams {
// Create layout params for the new view
}
private fun addTextWatcher(view, itemId, textCallback) {
val watcher = create text watcher where it invokes textCallback with itemId
view.addTextWatcher(watcher)
textWatchers[view.id] = watcher
}
}
Your inputs are less to identify the issue. I guess you are making some data collection application with the list of edit text.
There is a an issue when you were using the edit text in recycler list.
When you scroll down the bottom edit text in the recycler view will be filled with already filled edit text value, even though you user is not filled.
As a work around You can create some sparse array any data structure which will best suitable for you, that can map you position and value
like
mPropertyValue[] = new String [LIST_SIZE]. , assuming that position of ur list item matches with index of array.
Try updating the index with the value of text watcher
mPropertyValue[POSITION] = YOUR_EDIT_TEXT_VALUE
When you want to initialize your edit text use the value by mPropertyValue[POSITION]
You can always make sure that your edit text will be having the right value by this .
i face like this problem in my java code and that was the solution
for (int i = 0; i < list.size(); i++) {
(put her the getter and setter class) mylist = list.get(i);
//use the getter class to get values and save them or do what ever you want
}
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.