RecyclerView delete the wrong position item - android

I have a list of item in RecylerView, with have a delete button with delete the item from the list.
When I click the delete button, the item at last got deleted not the item selected to be deleted.
What's more, it creates a new item with old item's data instead of new data.
The following below is the code written in Kotlin:
class MedicineAdapter(private val activity: Activity, private val dataSet: MutableList<MedicineModel>, private val medicineSet: List<MedicineData>): RecyclerView.Adapter<MedicineAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val deleteLayout: LinearLayout
val medicineLayout: LinearLayout
val medicineName: TextView
val timeLayout: LinearLayout
val medicineTime: TextView
init {
deleteLayout = view.findViewById(R.id.delete_layout)
medicineLayout = view.findViewById(R.id.medicine_layout)
medicineName = view.findViewById(R.id.medicine_name)
timeLayout = view.findViewById(R.id.time_layout)
medicineTime = view.findViewById(R.id.medicine_time)
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.layout_medicine_item, viewGroup, false)
return ViewHolder(view)
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
viewHolder.deleteLayout.setOnClickListener {
deleteRecord(viewHolder.adapterPosition)
}
viewHolder.medicineLayout.setOnClickListener {
val spinnerDialog = SpinnerDialog(activity, medicineSet.map { p -> p.name } as ArrayList<String>, "请选择药品", "关闭")
spinnerDialog.setCancellable(true)
spinnerDialog.setShowKeyboard(false)
spinnerDialog.bindOnSpinerListener { item, position ->
viewHolder.medicineName.text = item
}
spinnerDialog.showSpinerDialog()
}
}
override fun getItemCount(): Int {
return dataSet.count()
}
fun addRecord() {
val position = dataSet.count()
dataSet.add(position, MedicineModel())
notifyItemInserted(position)
}
#SuppressLint("NotifyDataSetChanged")
private fun deleteRecord(position: Int) {
dataSet.removeAt(position)
notifyItemRemoved(position)
notifyDataSetChanged()
}
I have called notifyDataSetChanged however it does not seems to work.

Related

Error when initialize viewHolder in RecyclerView adapter

How should I initialize viewHolder? I have this error:
What I need to do is to get selected item in recyclerView but without using onClick method. When I get this selected item I need to show Toast message. Item is data class. Is it possible to pass some value from adapter to activity? Like I need to pass actual items from Data Class.
Process: com.pors.coopreaderlast, PID: 7862
kotlin.UninitializedPropertyAccessException: lateinit property viewHolder has not been initialized
at com.pors.coopreaderlast.features.polozka.PolozkaAdapter.getViewHolder(PolozkaAdapter.kt:18)
at com.pors.coopreaderlast.features.polozka.PolozkaAdapter.getCurrentItem(PolozkaAdapter.kt:46)
at com.pors.coopreaderlast.features.polozka.PolozkaActivity.onStart(PolozkaActivity.kt:213)
this is for line where viewHolder is set in Adapter:
lateinit var viewHolder: PolozkaViewHolder
This is Adapter
class PolozkaAdapter(val chosen_item: Int, private val listener: OnItemClickListener): ListAdapter<Polozka, PolozkaAdapter.PolozkaViewHolder>(DiffCallback()){
var selectedItemPosition: Int = chosen_item
lateinit var viewHolder: PolozkaViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PolozkaViewHolder {
val binding = PolozkyItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
//return PolozkaViewHolder(binding)
viewHolder = PolozkaViewHolder(binding)
return viewHolder
}
override fun onBindViewHolder(holder: PolozkaViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
if (selectedItemPosition == position){
holder.itemView.setBackgroundColor(Color.parseColor("#DA745A"))
} else
{
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
}
}
fun getCurrentItem(): Polozka = super.getItem(viewHolder.bindingAdapterPosition)
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
inner class PolozkaViewHolder(private val binding: PolozkyItemBinding): RecyclerView.ViewHolder(binding.root){
init {
binding.root.setOnClickListener{
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION){
val item = getItem(position)
if (item != null){
listener.onItemClick(item, position)
}
}
notifyItemChanged(selectedItemPosition)
selectedItemPosition = bindingAdapterPosition
notifyItemChanged(selectedItemPosition)
}
}
fun bind(polozkaPolozka: Polozka){
binding.apply {
tvREG.text = polozkaPolozka.reg
tvVB.text = polozkaPolozka.veb.toString()
}
}
}
interface OnItemClickListener{
fun onItemClick(polozkaDoklad: Polozka, position: Int)
}
class DiffCallback: DiffUtil.ItemCallback<Polozka>(){
override fun areItemsTheSame(oldItem: Polozka, newItem: Polozka) =
oldItem.pvp06pk == newItem.pvp06pk
override fun areContentsTheSame(oldItem: Polozka, newItem: Polozka) =
oldItem == newItem
}
}
This is onCreate method but it can be in onCreate method also.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityPolozkaBinding.inflate(layoutInflater)
idPositionItem = intent.getIntExtra("positionItem",0)
val itemAdapter = PolozkaAdapter(idPositionItem, this)
binding.apply {
recyclerView.apply {
adapter = itemAdapter
layoutManager = LinearLayoutManager(this#ItemActivity)
}
itemViewModel.getall(index,idExp.toString() ).observe(this#PolozkaActivity){
itemAdapter.submitList(it)
}
}
val selectedItem = itemAdapter.getCurrentItem()
Toast.makeText(this, "Reg vybrane polozky je ${selectedItem.reg}", Toast.LENGTH_LONG).show()
I have similar question here: Similar question but here I use binding.
The reason you're getting this exception is that viewHolder has not been initalized at the moment you want to access it. And since it's a lateinit var, it expects to be initialized every time you access it. (see https://kotlinlang.org/docs/properties.html#late-initialized-properties-and-variables)
Instead of using a lateinit var for viewHolder, you can return the instance you created in the onCreateViewHolder() so no need to have an extra field in the adapter for it.
I believe you use viewHolder to find the selected item. In that case I suggest using a boolean (for instance you can name it selected) in your model object Polozka to indicate if the item is selected or not. And in you getCurrentItem() method I would write getCurrentList().find { it.selected } to find the currently selected item. In this case you need to update your list every time you select a new item and mark only that as selected.
class PolozkaAdapter(private val listener: OnItemClickListener): ListAdapter<Polozka, PolozkaAdapter.PolozkaViewHolder>(DiffCallback()){
lateinit var viewHolder: PolozkaViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PolozkaViewHolder {
val binding = PolozkyItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PolozkaViewHolder(binding)
}
override fun onBindViewHolder(holder: PolozkaViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
if (currentItem.selected){
holder.itemView.setBackgroundColor(Color.parseColor("#DA745A"))
} else
{
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
}
}
fun getCurrentItem(): Polozka = currentList.find { it.selected }
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
//Model object would look like
data class Polozka(
val selected: Boolean,
//rest of the fields
)
And in the observe you should do it like this.
itemViewModel.getall(index,idExp.toString() ).observe(this#PolozkaActivity){
// mark the item selected
val updatedList = it.mapIndexed { index, item ->
if (index == idPositionItem) {
item.copy(selected = true)
} else {
item
}
}
itemAdapter.submitList(updatedList)
}
}
val selectedItem = itemAdapter.getCurrentItem()
Toast.makeText(this, "Reg vybrane polozky je ${selectedItem.reg}", Toast.LENGTH_LONG).show()
And every time a new item gets selected you need to update your list.

How to display item info on selected item in RecyclerView using Kotlin

How to display some information from recyclerview selected item without using onClick method. When the app is started first item is selected and highlighted. I need to eg. use Toast with value of anything that is in data class. I have implemented onClick method but the question is how to do it without using this method.
This is MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val exampleList = generateDummyList(20)
val exampleAdapter = ExampleAdapter(getItem, exampleList)
exampleAdapter.onItemClick = { item, position: Int ->
Toast.makeText(this, "Position: $position", Toast.LENGTH_SHORT).show()
val intent = Intent(this, ItemActivity::class.java).apply {
putExtra("itempos", position)
putExtra("maxSize", maxS)
}
startActivity(intent)
}
}
}
This is adapter:
class ExampleAdapter(val chosen_item: Int, private val exampleList: List<ExampleItem>):
RecyclerView.Adapter<ExampleAdapter.ExampleViewHolder>()
{
var onItemClick: ((ExampleItem, Int) -> Unit)? = null
var selected_item: Int = chosen_item
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_recy, parent, false)
return ExampleViewHolder(itemView)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int){
val currentItem = exampleList[position]
holder.tv_ID.text = currentItem.id.toString()
holder.tv_NAME.text = currentItem.name
holder.tv_EMAIL.text = currentItem.email
if (position == selected_item){
holder.tv_NAME.setBackgroundColor(Color.GREEN)
} else {
holder.tv_NAME.setBackgroundColor(Color.TRANSPARENT)
}
}
override fun getItemCount(): Int {
return exampleList.size
}
inner class ExampleViewHolder(itemView:View): RecyclerView.ViewHolder(itemView) {
val tv_ID: TextView = itemView.tv_ID
val tv_NAME: TextView = itemView.tv_NAME
val tv_EMAIL: TextView = itemView.tv_EMAIL
init {
itemView.setOnClickListener{
onItemClick?.invoke(exampleList[absoluteAdapterPosition], absoluteAdapterPosition)
notifyItemChanged(selected_item)
selected_item = absoluteAdapterPosition
notifyItemChanged(selected_item)
}
itemView.isSelected
}
}
}
I have second activity - when user click on item in first activity(recyclerview) - this second activity is open - then I raise the id of item by one and open again first activity where another item is highlighted. And I need to display eg. EMAIL from ExampleItem class.
This is second activity:
class ItemActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_item)
var itempos = intent.getIntExtra("itempos",0)
val maxSize = intent.getIntExtra("maxSize",0)
button2.setOnClickListener {
if (itempos == maxSize){
itempos = itempos
} else {
itempos = itempos + 1
}
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("itemposplus", itempos)
}
startActivity(intent)
}
}
}
If I understood correctly, you want to get the selected item at any time (without a click). There are several ways to do this. I recommend to you use getAdapterPosition() method in ViewHolder
First, save your ViewHolder
class ExampleAdapter(val chosen_item: Int, private val exampleList: List<ExampleItem>):
RecyclerView.Adapter<ExampleAdapter.ExampleViewHolder>()
{
var onItemClick: ((ExampleItem, Int) -> Unit)? = null
var selected_item: Int = chosen_item
lateinit var viewHolder: ExampleViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_recy, parent, false)
viewHolder = ExampleViewHolder(itemView)
return viewHolder
}
And then write a public method into the adapter for get the current item in activity
fun getCurrentItem(): ExampleItem = exampleList.get(viewHolder.getAdapterPosition())
Finally you can get selected item in activity
val selectedItem = exampleAdapter.getCurrentItem()
Also you can check getLayoutManager().findFirstVisibleItemPosition() method in your RecyclerView

How to add multi countdown timer in Recycler view?

I'm new to Android development. I'm trying to add a Multi countdown timer in recycler view but it does not work. Editing and deleting items in the list are okay, but I have no idea how to add a countdown timer function.
When I click the play button for starting the countdown, nothing happens. I would really appreciate it if you could tell me with a simple example.
Here is Adapter.kt.
class UserAdapter(val c: Context, val userList:ArrayList<UserData>): RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
inner class UserViewHolder(val v: View) : RecyclerView.ViewHolder(v) {
var name: TextView
var DeleteButton: ImageView
var EditButton: ImageView
var PlayButton: ImageView
val start = 600_000L
var timer = start
lateinit var countDownTimer: CountDownTimer
init {
name = v.findViewById<TextView>(R.id.alarm_name)
DeleteButton = v.findViewById(R.id.alarm_button_delete)
EditButton=v.findViewById(R.id.alarm_button_edit)
PlayButton=v.findViewById(R.id.alarm_button_start)
DeleteButton.setOnClickListener { DeleteItem(it) }
EditButton.setOnClickListener { EditItem(it) }
PlayButton.setOnClickListener { PlayItem(it)}
}
private fun PlayItem(v: View) {
countDownTimer = object : CountDownTimer(timer,1000){
override fun onFinish() {
}
override fun onTick(millisUntilFinished: Long) {
}
}.start()
}
private fun DeleteItem(v: View) {
userList.removeAt(adapterPosition)
notifyDataSetChanged()
Toast.makeText(c, "Deleted this Information", Toast.LENGTH_SHORT).show()
}
private fun EditItem(v: View){
val position = userList[adapterPosition]
val v = LayoutInflater.from(c).inflate(R.layout.add_item,null)
val name = v.findViewById<EditText>(R.id.add_alarm_name)
AlertDialog.Builder(c)
.setView(v)
.setPositiveButton("Ok"){
dialog,_->
position.add_alram_name = name.text.toString()
notifyDataSetChanged()
Toast.makeText(c,"User Information is Edited",Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
.setNegativeButton("Cancel"){
dialog,_->
dialog.dismiss()
}
.create()
.show()
true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.list_item, parent, false)
return UserViewHolder(v)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val newList = userList[position]
holder.name.text = newList.add_alram_name
}
override fun getItemCount(): Int {
return userList.size
}
Seems like your adapter is not holding instance for each row items, as per recycling behavior the timer unable to hold the view position.
you can refer this

Recyclerview Edittext data loss on scroll

I am trying to implement recyclerview which have a 50-100 list of item. Each row contains of TextView and Edittext view. User type comments on each row containing edittext. But the issue is that when user types the value and try to scroll up/down, data entered in the row get lost.
Then I tried another solution. I update the value of the array at that position and notifyDataSetChanged, it prints value on all row containing edittext
My Adapter:
class MyAdapter(
val context: Context,
val itemList: MutableList<String>
) :
RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var txtLabel: TextView = view.findViewById(R.id.txtLabel)
var etValue: EditText = view.findViewById(R.id.etValue)
var watcher: TextWatcher? = null
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val view =
LayoutInflater.from(context).inflate(R.layout.list_comment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.txtLabel.text = itemList[position]
holder.watcher = holder.etValue.doAfterTextChanged { text ->
try {
updateItem(
position,
text.toString()
)
} catch (ex: Exception) {
println(ex.message)
}
}
}
override fun getItemCount(): Int {
return itemList.size
}
fun updateItem(position: Int, item: String) {
itemList.add(position, item)
notifyDataSetChanged()
}
}
When updateItem method run it updates all rows but I am passing the position to only update at that position. Kindly guide what is the issue.
holder.etValue.doAfterTextChanged will add a TextChangedListener. You don't remove it later and thus you end up with multiple listener attached to one EditText.
You could rewrite your code like this:
class MyAdapter(
val context: Context,
val itemList: MutableList<String>
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
lateinit var onTextUpdated: (String) -> Unit
var txtLabel: TextView = view.findViewById(R.id.txtLabel)
var etValue: EditText = view.findViewById(R.id.etValue)
init { // TextChanged listener added only once.
etValue.doAfterTextChanged { editable ->
val text = editable.toString()
onTextUpdated(text)
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val view =
LayoutInflater.from(context).inflate(R.layout.list_comment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.txtLabel.text = itemList[position]
holder.onTextUpdated = { text -> // each time holder is bound, new listener will be assigned
try {
updateItem(
position,
text
) // update cached value
holder.txtLabel.text = text // update label
} catch (ex: Exception) {
println(ex.message)
}
}
}
override fun getItemCount(): Int {
return itemList.size
}
fun updateItem(position: Int, item: String) {
itemList[position] = item
// this might mess up the EditTexts focus.
// notifyDataSetChanged()
}
}

Adding data from parent recyclerview adapter and removing data from child recyclerview adapter

I have a nested recyclerview. Parent recyclerview adapter can add a data to child by a button, then child recyclerview adapter can remove a data in child by a button. The preview is like below.
Adding data seems fine, but when removing data i got index out of bound. I have called notifyDataSetChanged() in add or remove function.
My parent adapter code :
class FormPlanParentAdapter :
RecyclerView.Adapter<FormPlanParentAdapter.ViewHolder>() {
private val data: MutableList<MutableList<ItemPlan>> = mutableListOf()
private val viewPool = RecyclerView.RecycledViewPool()
fun setData(newData: List<MutableList<ItemPlan>>) {
data.clear()
data.addAll(newData)
notifyDataSetChanged()
}
fun getData(): String = Gson().toJson(data)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_plan_parent, parent, false)
)
}
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
val listPlans = data[position]
val childLayoutManager = LinearLayoutManager(
holder.recyclerView.context,
LinearLayoutManager.VERTICAL,
false
)
val childAdapter = FormPlanChildAdapter(listPlans, object : PlanParentCallback {
override fun deletePlan(position: Int) {
listPlans.removeAt(position)
notifyDataSetChanged()
}
})
holder.recyclerView.apply {
layoutManager = childLayoutManager
adapter = childAdapter
setRecycledViewPool(viewPool)
}
holder.textView.text = "Hari ke ${position + 1}"
holder.imageButton.setOnClickListener {
listPlans.add(ItemPlan())
notifyDataSetChanged()
}
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val recyclerView: RecyclerView = itemView.rv_plan_child
val textView: TextView = itemView.tv_days_of
val imageButton: ImageButton = itemView.ib_add
}
interface PlanParentCallback {
fun deletePlan(position: Int)
}
}
My child adapter code :
class FormPlanChildAdapter(
private val listPlans: List<ItemPlan>,
private val callback: FormPlanParentAdapter.PlanParentCallback
) :
RecyclerView.Adapter<FormPlanChildAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_plan_child, parent, false)
)
}
override fun getItemCount(): Int = listPlans.size
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
val itemPlan = listPlans[position]
holder.etPlan.setText(itemPlan.plan)
holder.etPlan.doAfterTextChanged {
listPlans[position].plan = it.toString()
}
if (position == 0) {
holder.ibDelete.invisible()
} else {
holder.ibDelete.visible()
}
holder.ibDelete.setOnClickListener {
if (listPlans.size > 1) {
callback.deletePlan(position)
}
}
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val etPlan: EditText = itemView.et_plan
val ibDelete: ImageButton = itemView.ib_delete
}
}
Any help appreciated, thank you.
Reason:
The old TextWatchers and adapter position mess up when the dataset changed.
notifyDatasetChanged triggers onBindViewHolder, and then EditText's TextWatcher is being notified because of this line
holder.etPlan.setText(itemPlan.plan)
Since etPlan has already an active TextWatcher from previous binding, it tries to notify it with the old position of the adapter.
Quick answer:
Keep the TextWatcher in your ViewHolder, remove it from EditText on onBindViewHolder, and set it to the EditText again after setting the EditText's value.
So, update your child adapter's onBindViewHolder as
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
val itemPlan = listPlans[position]
holder.etPlan.removeTextChangedListener(holder.textWatcher)
holder.etPlan.setText(itemPlan.plan)
holder.textWatcher = holder.etPlan.doAfterTextChanged {
listPlans[position].plan = it.toString()
}
...
}
and also update it's ViewHolder like:
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val etPlan: EditText = itemView.et_plan
val ibDelete: ImageButton = itemView.ib_delete
var textWatcher: TextWatcher? = null
}
But, it's not recommended setting listeners on onBindViewHolder due to performance concerns. Please check this and this.
Here is another workaround:
Move all the TextWatcher things to Adapter via a listener, and prevent notifying the TextWatcher if it's not necessary.
interface TextChangeListener {
fun onTextChange(text: String, position: Int)
}
class FormPlanChildAdapter(...) :
RecyclerView.Adapter<FormPlanChildAdapter.ViewHolder>(), TextChangeListener {
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
val itemPlan = listPlans[position]
holder.setPlan(itemPlan.plan)
...
}
override fun onTextChanged(text: String, position: Int) {
listPlans[position].plan = text
}
inner class ViewHolder(itemView: View, textChangeListener: TextChangeListener) :
RecyclerView.ViewHolder(itemView) {
private var disableTextChangeListener = false
private val etPlan: EditText = itemView.et_plan
val ibDelete: ImageButton = itemView.ib_delete
init {
etPlan.doAfterTextChanged {
if (!disableTextChangeListener) {
textChangeListener.onTextChanged(it.toString(), adapterPosition)
}
}
}
fun setPlan(plan: String, notifyTextChangeListener: Boolean = false) {
if (notifyTextChangeListener) {
etPlan.setText(plan)
} else {
disableTextChangeListener = true
etPlan.setText(plan)
disableTextChangeListener = false
}
}
}
}
Please try doing
listPlans.removeAt(position)
notifyItemRemoved(position)
instead of
listPlans.removeAt(position)
notifyDataSetChanged()

Categories

Resources