Android RecyclerView SelectionTracker clears selection on touch outside - android

I'm trying to implement the android library SelectionTracker which allows to select items in a recyclerView.
Everything works fine except that when I click outside of an
item (which is in a grid layout), the all selection is cleared.
I actually have found the code which calls the clearSelection(). It's on the line 78 of the class TouchInputHandler.
It then calls the line 64 of ItemDetailsLookup which returns false because the touch event didn't occurred on an item.
I was wondering if anyone have found a workaround to prevent this behavior, because I didn't found any option in the documentation.
It's a gridLayout so it is quite "normal" to have space between items and I don't want my users to clear the selection because they have touch the side of an item.

This is my solution, based on that if we have predefined ItemDetail that will be used as "this is not the view you can select".
First, inside your ItemDetailsLookup instead of returning null you can pass single item with distinguish data that will make sure there is no name/position collision with any other data you can have
class AppItemDetailsLookup(private val rv: RecyclerView) : ItemDetailsLookup<String>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
val view = rv.findChildViewUnder(e.x, e.y) ?: return EMPTY_ITEM
return (rv.getChildViewHolder(view) as AppItemViewHolder).getItemDetails()
}
object EMPTY_ITEM : ItemDetails<String>() {
override fun getSelectionKey(): String? = "empty_item_selection_key_that_should_be_unique_somehow_that_is_why_i_made_it_so_long"
override fun getPosition(): Int = Integer.MAX_VALUE
}
}
And then when you are creating SelectionTracker with builder, instead of using standard predicate (default is SelectionPredicates.createSelectAnything()) you make your own that will notify that this EMPTY_ITEM cannot be selected
.withSelectionPredicate(object : SelectionTracker.SelectionPredicate<String>() {
override fun canSelectMultiple(): Boolean = true
override fun canSetStateForKey(key: String, nextState: Boolean): Boolean =
key != AppItemDetailsLookup.EMPTY_ITEM.selectionKey
override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean =
position != AppItemDetailsLookup.EMPTY_ITEM.position
})
I tested it with LinearLayoutManger, the selection was deselecting all items once i clicked outside any of them (my items did not had spacing decoration, but there were so few of them that i was seeing empty under last item)

Related

Add Drag and Drop on RecyclerView with DiffUtil

I have a list that gets updated from a Room Database. I receive the updated data from Room as a new list and I then pass it to ListAdapter's submitList to get animations for the changes.
list.observe(viewLifecycleOwner, { updatedList ->
listAdapter.submitList(updatedList)
})
Now, I want to add a drag and drop functionality for the same RecyclerView. I tried to implement it using ItemTouchHelper. However, the notifyItemMoved() is not working as ListAdapter updates its content through the submitList().
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
val list = itemListAdapter.currentList.toMutableList()
Collections.swap(list, from, to)
// does not work for ListAdapter
// itemListAdapter.notifyItemMoved(from, to)
itemListAdapter.submitList(list)
return false
}
The drag and drop now works fine but only when dragged slowly, when the dragging gets fast enough, I get different and inconsistent results.
What could be the reason for this? What is the best way that I can achieve a drag and drop functionality for my RecyclerView which uses ListAdapter?
So I made a quick test (this whole thing doesn't fit in a comment so I'm writing an answer)
My Activity contains the adapter, RV, and observes a viewModel. When the ViewModel pushes the initial list from the repo via LiveData, I save a local copy of the list in mutable form (just for the purpose of this test) so I can quickly mutate it on the fly.
This is my "onMove" implementation:
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
list[from] = list[to].also { list[to] = list[from] }
adapter.submitList(list)
return true
I also added this log to verify something:
Log.d("###", "onMove from: $from (${list[from].id}) to: $to (${list[to].id})")
And I noticed it.. works. But because I'm returning true (you seem to be returning false).
Now... unfortunately, if you drag fast up and down, this causes the list to eventually become shuffled:
E.g.: Let's suppose there are 10 items from 0-9.
You want to grab item 0 and put it between item 1 and 2.
You start Dragging item 0 at position 0, and move it a bit down so now it's between 1 and 2, the new item position in the onMove method is 1 (so far, you're still dragging). Now if you slowly drag further (to position 2), the onMove method is from 1 to 2, NOT from 0 to 2. This is because I returned "true" so every onMove is a "finished operation". This is fine, since the operations are slow and the ListAdapter has time to update and calculate stuff.
But when you drag fast, the operations go out of sync before the adapter has time to catch up.
If you return false instead (like you do) then you get various other effects:
The RecyclerView Animations don't play (while you drag) since the viewHolders haven't been "moved" yet. (you returned false)
The onMove method is then spammed every time you move your finger over a viewHolder, since the framework wants to perform this move again... but the list is already modified...
So you'd get something like (similar example above, 10 items, moving the item 0)> let's say each item has an ID that corresponds to its position+1 (in the initial state, so item at position 0 has id 1, item at position 1 has id 2, etc.)
This is what the log shows while I slowly drag item 0 "down":
(format is `from: position(id of item from) to: position(id of item to)
onMove from: 0 (1) to: 1 (2) // Initial drag of first item down to 2nd item.
onMove from: 0 (2) to: 1 (1) // now the list is inverted, notice the IDs.
onMove from: 0 (1) to: 1 (2) // Back to square one.
onMove from: 0 (2) to: 1 (1) // and undo-again...
I just cut it there, but you can see how it's bouncing all over the place back and forth. I believe this is because you return false but modify the list behind the scenes, so it's getting confused. on one side of the equation the "data" says one thing, (and so does the diff util), but on the other, the adapter is oblivious to this change, at least "yet" until the computations are done, which, as you guessed, when you drag super fast, is not enough time.
Unfortunately, I don't have a good answer (today) as to what would the best approach be. Perhaps, not relying on the ListAdapter's behavior and implementing a normal adapter, where you have better list/source control of the data and when to call submitList and when to simply notifyItemChanged or moved between two positions may be a better alternative for this use-case.
Apologies for the useless answer.
I ended up implementing a new adapter and use it instead of ListAdapter, as mentioned on Martin Marconcini's answer. I added two separate functions. One for receiving updates from Room database (replacement for submitList from ListAdapter) and another for every position change from drag
MyListAdapter.kt
class MyListAdapter(list: ArrayList<Item>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// save instance instead of creating a new one every submit
// list to save some allocation time. Thanks to Martin Marconcini
private val diffCallback = DiffCallback(list, ArrayList())
fun submitList(updatedList: List<Item>) {
diffCallback.newList = updatedList
val diffResult = DiffUtil.calculateDiff(diffCallback)
list.clear()
list.addAll(updatedList)
diffResult.dispatchUpdatesTo(this)
}
fun itemMoved(from: Int, to: Int) {
Collections.swap(list, from, to)
notifyItemMoved(from, to)
}
}
DiffCallback.kt
class DiffCallback(
val oldList: List<Item>,
var newList: List<Item>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return compareContents(oldItem, newItem)
}
}
Call itemMoved every position change:
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
itemListAdapter.itemMoved(from, to)
// Update database as well if needed
return true
}
When receiving updates from Room database:
You may also want to check if currently dragging using onSelectedChanged if you are also updating your database every position change to prevent unnecessary calls to submitList
list.observe(viewLifecycleOwner, { updatedList ->
listAdapter.submitList(updatedList)
})
I've tried danartillaga's answer and got a ConcurrentModificationException for the list variable. I use LiveData in the code and it looks like the data was changed during invalidation of the list.
I've tried to keep the ListAdapter implementation and concluded to the following solution:
class MyListAdapter : ListAdapter<Item, RecyclerView.ViewHolder>(MyDiffUtil) {
var modifiableList = mutableListOf<Item>()
private set
fun moveItem(from: Int, to: Int) {
Collections.swap(modifiableList, to, from)
notifyItemMoved(from, to)
}
override fun submitList(list: List<CourseData>?) {
modifiableList = list.orEmpty().toMutableList()
super.submitList(modifiableList)
}
}
and the onMove code from ItemTouchHelper.SimpleCallback:
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val adapter = recyclerView.adapter as CoursesDownloadedAdapter
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
val list = adapter.modifiableList
// Change your DB here
adapter.moveItem(from, to)
return true
}
The magic here is saving the modifiableList inside the adapter. ListAdapter stores a link to the list from submitList call, so we can change it externally. During the Drag&Drop the list is changed with Collections.swap and RecyclerView is updated with notifyItemMoved with no DiffCallback calls. But the data inside ListAdapter was changed and the next submitList call will use the updated list to calculate the difference.

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

How to unselect all selected items in RecyclerView's SelectionTracker even if some items are scrolled off the screen?

I am using SelectionTracker to implement a multiple selection RecyclerView. the select/unselect feature works as expected if I do it manually (Item is on the screen and I change its state by tapping) but if I try to unselect all items, some of which are off screen, using clearSelection method of selection tracker it only unselects the items which are currently visible on the screen.
This is how I am building the SelectionTracker
tracker = SelectionTracker.Builder<Long>(
"mySelection",
recyclerView,
MyKeyProvider(recyclerView),
MyItemDetailsLookup(recyclerView),
StorageStrategy.createLongStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
recyclerAdapter.tracker = tracker
Following is bindItem and onBindViewHolder methods of ViewHolder and adapter respectively
fun bindItems(model: Model, isActivated: Boolean) {
itemView.isActivated = isActivated
if(itemView.isActivated){
/* Do something */
}
else{
/* Do something */
}
}
override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
val number = displayList[position]
tracker?.let {
holder.bindItems(number, it.isSelected(position.toLong()))
}
}
I call the clear selection method on click of a menu item
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if((selectionMode) && (item?.itemId==android.R.id.home)){
tracker.clearSelection()
}
return super.onOptionsItemSelected(item)
}
Edit: This seems to be a bug in the Library, I have encountered the same issue while using the Android Gmail app, Which I think must be using SelectionTracker
You add a listener to the tracker, which when changing the state of any element, checks if it was the last selected element and updates the entire list if this is true
tracker?.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onItemStateChanged(key: Long, selected: Boolean) {
if(!tracker!!.hasSelection())
adapter.notifyDataSetChanged()
super.onItemStateChanged(key, !selected)
}
})

Getting EditTexts Values From RecyclerView

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
}

Categories

Resources