1 button incrementing multiple TextViews - android

so my problem may sound a little bit weird but let me explain. I have custom recycle view adapter and custom layout. In custom layout I have buttons which increment or decrement TextView Value. But when I press 1 button it increments like 4 others.
As you can see here I only clicked delay and defendant but house and item incremented too.
Here's my code
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
var pName : TextView = holder.itemView.findViewById(R.id.ingredientName)
val ingrCount : TextView = holder.itemView.findViewById(R.id.ingredientCount)
val decrementIngr : Button = holder.itemView.findViewById(R.id.deleteIngeredient)
val incrementIngr : Button = holder.itemView.findViewById(R.id.addIngredient)
pName.text = ingredients[position]
incrementIngr.setOnClickListener{
val count = Integer.parseInt(ingrCount.text.toString()) + 1
ingrCount.text = "$count"
}
decrementIngr.setOnClickListener{
if(Integer.parseInt(ingrCount.text.toString()) - 1 > 0)
{
val count = Integer.parseInt(ingrCount.text.toString()) - 1
ingrCount.text = "$count"
}
}
}

The problem is that you are saving your ingrCount state for each item directly on the TextView.text property. Since this is inside a RecyclerView the Views (your custom layouts) get reused (recycled). There are only enough of them created to cover a bit more than the height of your RecyclerView, but as you scroll further the same views are reused. That is why you see your other numbers repeat.
Your click handlers have never incremented or decremented more than a single TextView per click.
Inside override fun onBindViewHolder(holder: ViewHolder, position: Int) you always have to (re)set all parts of the UI that you want to be bound to your underlying data. In your current code you only set the pName.text, this is why that one works correctly. Do the same for the ingrCount.text and it will work as you expect.
You can place a breakpoint or some logging output (log the position parameter for example) inside onBindViewHolder and you will see when different positions are rebound as you scroll up and down the list. This will give you a better understanding on how the RecyclerView works.
EDIT: You need to keep the ingredients count information the same as you do with ingredient names.
How you do it is up to you, you could store (data) classes instead of just ingredient names inside your ingredients Array/ArrayList.
So something like this
class Ingredient(val name: String, var count: Int = 0) {}
Or you could keep the ingredient counts in a separate Array/ArrayList/Map.
Let's say that you would use the class Ingredient to model your data, then your code inside onBindViewHolder would change to
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
var pName : TextView = holder.itemView.findViewById(R.id.ingredientName)
val ingrCount : TextView = holder.itemView.findViewById(R.id.ingredientCount)
val decrementIngr : Button = holder.itemView.findViewById(R.id.deleteIngeredient)
val incrementIngr : Button = holder.itemView.findViewById(R.id.addIngredient)
val ingredient = ingredients[position]
pName.text = ingredient.name
ingrCount.text = ingredient.count.toString()
incrementIngr.setOnClickListener{
ingredient.count += 1
ingrCount.text = ingredient.count.toString()
}
decrementIngr.setOnClickListener{
if(ingredient.count > 0)
{
ingredient.count -= 1
ingrCount.text = ingredient.count.toString()
}
}
}

Related

Moving item in RecyclerView doubles item in view

I'm building a contact list (ArrayList) in a messaging app using RecyclerView.
When a contact updates, I want to move that user to the top of the list, while all other items in the list move one step down.
I'm using Firestore to get the list of users.
DocumentChange.Type.MODIFIED -> {
val matchThatChanged = dc.newIndex
matchesArrayList[matchThatChanged] = ChatMatchListMatch(
matchUserID)
adapter.notifyItemChanged(matchThatChanged) //Ensures change is visible immediately
val fromPosition = matchesArrayList.indexOfFirst {
it!!.matchUserID == matchUserID
}
Log.d(TAG, "From position A: $matchThatChanged")
if (fromPosition != 0) {
adapter.moveMatchToTop(
fromPosition, ChatMatchListMatch(
matchUserID
)
)
}
}
In the Log here it correctly outputs 1 when I make the first move. However, thereafter it outputs 0 when I try to update the other user? This is not correct. The user that gets pushed down should no longer be at 0, it should be at 1. Because it is now wrongly at 0, it is not running the code (this is not the issue. The issue is that is should not be 0 in the first place).
Here's the code in my adapter:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.chat_matchlist_item, parent, false)
return CustomViewHolder(view)
}
// Passes the ContactListMatch object to a ViewHolder so that the contents can be bound to UI.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val contactListMatch = mMatchesArrayList[position]
(holder as CustomViewHolder).bind(contactListMatch)
}
fun moveMatchToTop(fromPosition: Int, chatMatchListMatch: ChatMatchListMatch) {
mMatchesArrayList.removeAt(fromPosition)
notifyItemChanged(fromPosition)
notifyItemRemoved(fromPosition)
mMatchesArrayList.add(0, chatMatchListMatch)
notifyItemInserted(0)
notifyItemChanged(0)
notifyItemChanged(1)
}
The list I have to begin with is:
When I update only the bottom user (2), it displays correctly (moving user two to the top and the other user down):
I now try to update original user 1 again, and I expect it to go on top again, like this:
But instead I get this:
I figured out the solution. I had to make several changes, but I'll try to make this helpful to others who may struggle with similar issues:
I used an index in Firestore and used two "orderby" queries to get the list in the order I wanted:
myCollectionRef
.orderBy("unread", Query.Direction.DESCENDING)
.orderBy("timeStamp", Query.Direction.DESCENDING)
.addSnapshotListener
Then under case MODIFIED:
DocumentChange.Type.MODIFIED -> {
I had to get the old index and the new index of the item changed. When I get the old index (by iterating through my list to look for it with a for loop), I ensure to remove the item from the list and notify my adapter that I removed the item:
val newIndex = dc.newIndex
var oldIndex = -1
for (i in myList.indices.reversed()) {
if (myList[i]!!.IDnumber == userID) {
oldIndex = i
myList.removeAt(oldIndex)
adapter.notifyItemRemoved(oldIndex)
}
}
Then I have to add the changed item back into my list in the correct position. Since I have the new index, this is easy. I also notify my adapter of the change:
myList.add(
newIndex, MySpecialItem(
userID!!,
userImageOrWhatever!!
)
)
adapter.notifyItemInserted(newIndex)
and that's it.

How can I implement a comment section using Recycler View

I'm trying to implement a comment section that allows the user to post and reply to another comment.
I have a list of comments, each comment has a list of replies this list may be null if there are no replies.
At first, I thought about using two recycler views, then I saw this post that says that using 2 RecyclerViews is not the best approach.
The comment and reply share the same layout, but the reply has a margin-left of 24dp.
Problem
My problem begins on onBindViewHolder the position will go from 0 to comments + replies
For example:
A list containing 2 objects with 5 replies each, in onBindViewHolder
position = 3 would be the reply[2] of comments[0] = comments[0].reply[2]
position = 6 would be comments[1]
How can determine the comment index and reply index from the position? I feel lost here
var comments = listOf<CommentModel>()
set(value) {
field = value
notifyDataSetChanged()
}
class ViewHolder(var binding: ItemForumCommentBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: CommentModel){
binding.commentModel = item
}
fun bind(item: ReplyModel){
binding.commentModel = item
(binding.commentGuideline.layoutParams as ConstraintLayout.LayoutParams).guideBegin = 24.px
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int){
//Bind comment or reply according to the position
//holder.bind(comments[x].replys[y]) or holder.bind(comments[x])
}
You can create some list that contains both comment and replies, and use it as single data provider for the list, e.g.:
var commentsWithReplies = mutableListOf<Any>()
var comments = listOf<CommentModel>()
set(value) {
field = value
commentsWithReplies.clear()
value.forEach {
commentsWithReplies.add(it)
commentsWithReplies.addAll(it.replies)
}
notifyDataSetChanged()
}
Then in getItemCount:
override fun getItemCount(): Int = commentsWithReplies.size
and in onBindViewHolder:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = commentsWithReplies[position]
if (item is CommentModel) {
holder.bind(commentsWithReplies[position] as CommentModel)
} else if (item is ReplyModel) {
holder.bind(commentsWithReplies[position] as ReplyModel)
}
}
P.S.
Of course, it's the very simple solution. You can refactor it (at least, use custom interface, not Any for the generic list)

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
}

Kotlin Firebase Groupie RecyclerView Scrolls to Top when like Button Pressed

I am an iPhone developer and currently porting my app to Android with Kotlin language and this is my first android app so I do not know about the Android studio or Kotlin much so bare my question.
I have an app where I show all the images stored into the firebase and each image has like node. So when the users pressed the like button the user Id will be added to the node and pressing the button again will remove the user Id from the firebase node. I use Groupie RecyclerView for the images to for rows. The only problem is when I click the like button the RecyclerView scrolls to the top which is kind of irritating and not a good user interface. How can I stop the like button to stop scrolling to the top, I believe its due scrolling due to firbase database refreshing the RecyclerView.
Below is the function I have used for fetching the data from the database
private fun fetchFactsFromFirebase(){
val factsDb = FirebaseDatabase.getInstance().getReference("/Facts").orderByChild("/timestamp")
factsDb.addValueEventListener(object : ValueEventListener{
override fun onDataChange(p0: DataSnapshot) {
val adapter = GroupAdapter<ViewHolder>()
p0.children.forEach {
val facts = it.getValue(Facts::class.java)
if (facts != null) {
adapter.add(FactsItems(facts))
}
}
recycleview_facts.adapter = adapter
}
Below is the facts class I have used to bind my database to the Row
class FactsItems(val facts: Facts) : Item<ViewHolder>(){
val factsId : String = ""
val currentUser = FirebaseAuth.getInstance().currentUser?.uid
override fun bind(viewHolder: ViewHolder, position: Int) {
viewHolder.itemView.textview_facts.text = facts.captionText
Picasso.get().load(facts.factsLink).placeholder(R.drawable.progress_image).into(viewHolder.itemView.imageview_facts)
viewHolder.itemView.likeview_facts.text = facts.likes.count().toString()
val likeButon = viewHolder.itemView.like_facts_image_button
if (facts.likes.contains(currentUser!!)){
likeButon.setImageResource(R.drawable.like)
likeButon.isSelected = true
}else{
likeButon.setImageResource(R.drawable.nolike)
likeButon.isSelected = false
}
viewHolder.itemView.imageview_facts
viewHolder.itemView.like_facts_image_button.isSelected
viewHolder.itemView.like_facts_image_button.setOnClickListener {
if (likeButon.isSelected == true){
likeButon.isSelected = false
likeButon.setImageResource(R.drawable.nolike)
addSubtractLikes(false,position, viewHolder)
}else {
likeButon.isSelected = true
likeButon.setImageResource(R.drawable.like)
addSubtractLikes(true, position, viewHolder)
}
}
}
Below is the addSubtract function to add or subtract the likes of the user ignore the postion and view holder I just tried to get the postion in this function but it did not work.
fun addSubtractLikes(addlike: Boolean, position: Int, viewHolder: ViewHolder){
val currentUsers = FirebaseAuth.getInstance().currentUser?.uid
if (addlike){
facts.likes.add(currentUsers!!)
Log.d("Like","User Liked ${currentUsers}")
}else {
facts.likes.remove(currentUsers!!)
Log.d("Like","User DisLiked ${currentUsers}")
}
val likeRef = FirebaseDatabase.getInstance().getReference("/Facts").child(facts.factsId).child("/likes")
likeRef.setValue(facts.likes)
Log.d("Like","liked Users ${facts.likes}")
}
What all I have tried.
When I get the instance of the RecyclerView into the factsItem class it gives me an error nullPointerException
so i cannot use RecyclerView.scrollToPostion or RecyclerView any function
Thank you any help will be appreciated.

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.

Categories

Resources