Editing list item on list view - android

Imagine we have a simple list of items. Each item contains only a short title.
To handle the list we are using RecyclerView with ListAdapter and ViewHolders.
Each item/view is not editable unless we click it.
In this scenario I am using one view model for list and one for item under edit.
Unfortunately all my attempts failed.
I have tried to use two different view holders but the list was flickering, after all inflating view (in this case binding) is heavy.
Another shot I was giving to use the same view holder but with two various bind methods - one binding plain item, second binding with viewmodel instead of data object but it failed as well - suddenly a few rows were editable.
Has anyone solved it ?
class MistakesAdapter(private val editViewModel: MistakeEditViewModel) :
ListAdapter<Mistake, RecyclerView.ViewHolder>(MistakesDiffCallback()) {
companion object{
const val ITEM_PLAIN_VIEW_TYPE = 0
const val ITEM_EDITABLE_VIEW_TYPE = 1
}
private var itemPositionUnderEdit = -1
private val listener = object: MistakeItemListener{
override fun onClick(view: View, position: Int) {
Timber.d("OnClick : edit - $itemPositionUnderEdit, clickPos - $position")
editViewModel.onEditMistake(getItem(position))
itemPositionUnderEdit = position
notifyItemChanged(itemPositionUnderEdit)
}
}
override fun getItemViewType(position: Int) =
when (position) {
itemPositionUnderEdit -> ITEM_EDITABLE_VIEW_TYPE
else -> ITEM_PLAIN_VIEW_TYPE
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (viewType) {
ITEM_EDITABLE_VIEW_TYPE -> EditableMistakeViewHolder.from(parent)
else -> MistakeViewHolder.from(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EditableMistakeViewHolder -> holder.bind(editViewModel, listener)
is MistakeViewHolder -> holder.bind(getItem(position), listener)
else -> throw ClassCastException("Unknown view holder type")
}
}
class MistakeViewHolder private constructor(private val binding: ListItemMistakesBinding) :
RecyclerView.ViewHolder(binding.root) {
companion object {
fun from(viewGroup: ViewGroup): MistakeViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
val binding = ListItemMistakesBinding.inflate(inflater, viewGroup, false)
return MistakeViewHolder(binding)
}
}
fun bind(item: Mistake, listener: MistakeItemListener) {
binding.apply {
mistake = item
inputType = InputType.TYPE_NULL
this.listener = listener
position = adapterPosition
executePendingBindings()
}
}
}
class EditableMistakeViewHolder private constructor(private val binding: ListItemMistakesBinding)
: RecyclerView.ViewHolder(binding.root) {
companion object{
fun from(viewGroup: ViewGroup): EditableMistakeViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
val binding = ListItemMistakesBinding.inflate(inflater, viewGroup, false)
return EditableMistakeViewHolder(binding)
}
}
fun bind(viewModel: MistakeEditViewModel, listener: MistakeItemListener){
binding.apply {
this.viewModel = viewModel
inputType = InputType.TYPE_CLASS_TEXT
this.listener = listener
position = adapterPosition
root.setBackgroundColor(Color.GRAY)
}
}
}
}
class MistakeEditViewModel(private val repository: MistakesRepository) : ViewModel() {
#VisibleForTesting
var mistakeUnderEdit: Mistake? = null
//two-way binding
val mistakeName = MutableLiveData<String>()
fun onEditMistake(mistake: Mistake) {
mistakeUnderEdit = mistake
mistakeName.value = mistake.name
}
}

By changing my approach to the problem I solved it.
I make all list items editable but at the same time I am following focus.
To cut the long story short, I invoke item view model methods with help of OnFocusChangeListener and TextWatcher on my editTexts.

Related

RecyclerView items changes randomly on clicking an item

I have a RecyclerView list, I want the color of the clicked item to be changed. But whenever I am clicking an item the RecyclerView items change randomly.
I have also taken a boolean in my data class to keep track of item selection.
data class
data class OrderAvailableDaysResponseItem(
val actual_date: String,
val day_name: String,
val date_name: String,
val day_num: Int,
// For item selection
var isDateSelected: Boolean)
Inside my fragment, checking if the clicked item matches the list item and updating the isDateSelected to true, then calling notifydatasetchanged.
private var availableDaysList: OrderAvailableDaysResponse = OrderAvailableDaysResponse()
orderAvailableDaysAdapter.setDateClickListener {
for(resItem in availableDaysList){
resItem.isDateSelected = resItem.date_name == it.date_name
}
orderAvailableDaysAdapter.notifyDataSetChanged()
}
Adapter clicklistener
var onDateClickListener: ((OrderAvailableDaysResponseItem) -> Unit)? = null
fun setDateClickListener(listener: (OrderAvailableDaysResponseItem) -> Unit){
onDateClickListener = listener
}
inner class AvailableDaysViewHolder(binding: ItemAvailableDaysBinding) : RecyclerView.ViewHolder(binding.root) {
fun setBindings(itemRes: OrderAvailableDaysResponseItem){
binding.resItem = itemRes
binding.executePendingBindings()
binding.clRoot.setOnClickListener {
onDateClickListener?.let {
it(itemRes)
}
}
}
}
Please refer to the attachment for a better understanding of the situation
As you can see on clicking on an item the items are changing randomly. Please help me. Am I missing something?
Edit 1:
Complete adapter code
class OrderAvailableDaysAdapter(var orderAvailableDaysResponseList: OrderAvailableDaysResponse) : RecyclerView.Adapter<OrderAvailableDaysAdapter.AvailableDaysViewHolder>() {
private lateinit var binding: ItemAvailableDaysBinding
var onDateClickListener: ((OrderAvailableDaysResponseItem) -> Unit)? = null
fun setDateClickListener(listener: (OrderAvailableDaysResponseItem) -> Unit){
onDateClickListener = listener
}
inner class AvailableDaysViewHolder(binding: ItemAvailableDaysBinding) : RecyclerView.ViewHolder(binding.root) {
fun setBindings(itemRes: OrderAvailableDaysResponseItem){
binding.resItem = itemRes
binding.executePendingBindings()
binding.clRoot.setOnClickListener {
onDateClickListener?.let {
it(itemRes)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableDaysViewHolder {
binding = ItemAvailableDaysBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return AvailableDaysViewHolder(binding)
}
override fun onBindViewHolder(holder: AvailableDaysViewHolder, position: Int) {
holder.setBindings(orderAvailableDaysResponseList[position])
}
override fun getItemCount(): Int {
return orderAvailableDaysResponseList.size
}}
remove private lateinit var binding: ItemAvailableDaysBinding from adapter, don' keep it "globally", initialisation is made only once, in onCreateViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableDaysViewHolder {
ItemAvailableDaysBinding binding = ItemAvailableDaysBinding.inflate(
LayoutInflater.from(parent.context),parent,false)
return AvailableDaysViewHolder(binding)
}
same naming of this "global" object and in AvailableDaysViewHolder inner class may confuse it and setBindings may be called on lastly initialised (global kept) object rather that this passed in constructor

lateinit property binding has not been initialized

I know that there are similar questions to this, but I just cant find something that is similar, I've been studying new things to learn, and while converting kotlin synthetics to viewvbinding mode, I've encountered this error
kotlin.UninitializedPropertyAccessException: lateinit property binding has not been initialized
at com.codepalace.chatbot.ui.MessagingAdapter.onBindViewHolder(MessagingAdapter.kt:60)
at com.codepalace.chatbot.ui.MessagingAdapter.onBindViewHolder(MessagingAdapter.kt:17)
It says that I have to initialize the binding, but I dont know where to put it.
This is the code.
class MessagingAdapter: RecyclerView.Adapter<MessagingAdapter.MessageViewHolder>() {
var messagesList = mutableListOf<Message>()
private lateinit var binding: MessageItemBinding
inner class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.setOnClickListener {
//Remove message on the item clicked
messagesList.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
return MessageViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.message_item, parent, false)
)
}
override fun getItemCount(): Int {
return messagesList.size
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
val currentMessage = messagesList[position]
when (currentMessage.id) {
SEND_ID -> {
holder.itemView.findViewById<View>(R.id.tv_message).apply {
binding.tvMessage.text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.findViewById<View>(R.id.tv_bot_message).visibility = View.GONE
}
RECEIVE_ID -> {
holder.itemView.findViewById<View>(R.id.tv_bot_message).apply {
binding.tvBotMessage.text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.findViewById<View>(R.id.tv_message).visibility = View.GONE
}
}
}
fun insertMessage(message: Message) {
this.messagesList.add(message)
notifyItemInserted(messagesList.size)
}
private lateinit var binding: MessageItemBinding
You didn't initialize the binding object, as you defined it as lateinit, then you should define it.
This is typically in onCreateViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackedActivityHolder {
val inflater = LayoutInflater.from(parent.context)
binding = DataBindingUtil.inflate<MessageItemBinding>(inflater, R.layout.message_item, parent, false)
return MessageViewHolder(
binding
)
}
UPDATE
You need to accept MessageItemBinding type instead of View in the MessageViewHolder constructor, and use itemView.root to get the root view of the list item.
inner class MessageViewHolder(itemView: MessageItemBinding) : RecyclerView.ViewHolder(itemView.root) {
init {
itemView.root.setOnClickListener {
//Remove message on the item clicked
messagesList.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
}
}
}
The reason why You get this error is that MessageItemBinding should not be in Adapter but in ViewHolder class. You can make RecyclerView like this:
object MessageDiffCallback : DiffUtil.ItemCallback<Message>()
{
override fun areItemsTheSame(
oldItem: Message,
newItem: Message
): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: Message,
newItem: Message
): Boolean =
oldItem == newItem
}
I assume that Message is a data class. You have to create this MessageDiffCallback and override these 2 methods in a similar way that I did it.
Now Create ViewHolder class:
class MessageViewHolder private constructor(
private val binding: MessageItemBinding
) : RecyclerView.ViewHolder(binding.root)
{
companion object
{
fun create(parent: ViewGroup): MessageViewHolder
{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = MessageItemBinding.inflate(layoutInflater, parent, false)
return MessageViewHolder(
binding
)
}
}
fun bind(
message: Messaage
)
{
// Here You can do everything.
// You pass the message and based on this You can set up a view and use binding
}
}
And now Adapter class
class MessageAdapter : ListAdapter<Message, MessageViewHolder>(MessageDiffCallback)
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder =
MessageViewHolder.create(parent)
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) =
holder.bind(
message = getItem(position),
)
}
Now You should have a working adapter. To put data in it use messageAdapter.submitList(messages) in Your Fragment/Activity
Maybe not the best answer because it changes Your code and uses a little different logic but it should work better. You can check google sample code here or take codelab here. It is free after signing up

How can i change findViewById to view binding?

Here it's my adapter code ;
class ModelViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val chName: TextView = view.findViewById(R.id.chName)
private val chBirthYear: TextView = view.findViewById(R.id.chBirthYear)
private val chHeight: TextView = view.findViewById(R.id.chHeight)
private val chHomeWorld: TextView = view.findViewById(R.id.chHomeWorld)
Get the list item view binding class and change it like this:
//ItemModelBinding is the generated binding class for the list view item
class ModelViewHolder(val binding: ItemModelBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: SomeModelClass) {
binding.chName = item.chName
binding.chBirthYear = item.chBirthYear
binding.chHeight = item.chHeight
binding.chHomeWorld= item.chHomeWorld
binding.executePendingBindings()
}
override fun onBindViewHolder(holder: ModelViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
class ModelViewHolder(view: View) RecyclerView.ViewHolder(view){
val binding = LayoutYouWantToUseBinding.bind(view)
}
ListAdapter is RecyclerView.Adapter base class for presenting List data in a RecyclerView, including computing diffs between Lists on a background thread.
DiffUtil helps in computing the difference between two lists on a background thread. and only update the view for which data has been changed means less number of call of onBindViewHolder() which would have called for all the item if we still stick with notifyDataSetChanged() on any data change.
class ModelAdapter : ListAdapter<Model, ModelAdapter.ModelViewHolder>(ModelDiffCallBack()) {
class ModelViewHolder(private val binding: ModelItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(model: Model) {
//if data binding is also enabled
binding.model = model
//else set data for each view one after another
binding.txtTitle.text = model.title
//and so on for all the items
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ModelViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ModelItemLayoutBinding.inflate(layoutInflater, parent, false)
return ModelViewHolder(binding)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ModelViewHolder =
ModelViewHolder.from(parent)
override fun onBindViewHolder(holder: ModelViewHolder, position: Int) {
val model = getItem(position)
holder.bind(model)
}
class ModelDiffCallBack : DiffUtil.ItemCallback<Model>() {
override fun areItemsTheSame(oldItem: Model, newItem: Model): Boolean =
oldItem === newItem
override fun areContentsTheSame(oldItem: Model, newItem: Model): Boolean =
oldItem == newItem
}
}

(Kotlin) Position of RecyclerView returns -1 when trying to click on an item

I'm new to Android development (and Kotlin).
I'm trying to implement a RecyclerView (which works fine) and when I click on a specific row it opens a new activity (Intent).
However, whenever I've press/click on one of the rows, I'm only able to get the value "-1" returned.
I've tried a number of different approaches (you should see the number of tabs in my browser).
This seems like it should be a fairly straightforward occurrence for something as common as a RecyclerView, but for whatever reason I'm unable to get it working.
Here is my RecyclerView Adapter file:
class PNHLePlayerAdapter (val players : ArrayList<PNHLePlayer>, val context: Context) : RecyclerView.Adapter<ViewHolder>() {
var onItemClick: ((Int)->Unit) = {}
// Gets the number of items in the list
override fun getItemCount(): Int {
return players.size
}
// Inflates the item views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(context).inflate(
R.layout.pnhle_list_item,
parent,
false
)
val viewHolder = ViewHolder(itemView)
itemView.setOnClickListener {
onItemClick(viewHolder.adapterPosition)
}
return ViewHolder(itemView)
}
// Binds each item in the ArrayList to a view
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.tvPlayerName?.text = players[position].Name
holder.tvPlayerRank?.text = position.toString()
holder.tvPNHLe?.text = players[position].PNHLe.toString()
holder.tvTeam?.text = players[position].Team
holder.ivLeague?.setImageResource(leagueImageID)
}
}
class ViewHolder (view: View) : RecyclerView.ViewHolder(view) {
val linLayout = view.hor1LinearLayout
val ivTeam = view.teamImageView
val tvPlayerName = view.playerNameTextView
val tvPlayerRank = view.rankNumTextView
val tvPNHLe = view.pnhleTextView
val tvTeam = view.teamTextView
val ivLeague = view.leagueImageView
}
As you can see, there is a class property "onItemClick" which uses a lambda as the click callback.
I setOnClickListener in the onCreateViewHolder method after the view is inflated.
Next, in my Activity I add the list to my Adapter and set the call back.
However, every time I 'Toast' the position it is displayed as '-1'.
val adapter = PNHLePlayerAdapter(list, this)
adapter.onItemClick = { position ->
Toast.makeText(this, position.toString(),Toast.LENGTH_SHORT).show()
var intent = Intent(this, PlayerCardActivity::class.java)
//startActivity(intent)
}
rv_player_list.adapter = adapter
Perhaps I'm not thinking about this properly, but shouldn't the position represent the row number of the item out of the RecyclerView???
Ideally, I need to use the position so that I can obtain the correct item from the 'list' (ArrayList) so that I can pass information to my next Activity using the Intent
I found the issue.
Change this line in onCreateViewHolder:
return ViewHolder(itemView)
to this one:
return viewHolder
I would reorganize the adapter like this:
class PNHLePlayerAdapter : androidx.recyclerview.widget.RecyclerView.Adapter<Adapter.ViewHolder>() {
interface AdapterListener {
fun onItemSelected(position: Int?)
}
var players: List<Player> = listOf()
set(value) {
field = value
this.notifyDataSetChanged()
}
var listener: AdapterListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_car_selector, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
override fun getItemCount(): Int {
return brands.size
}
inner class ViewHolder(view: View): androidx.recyclerview.widget.RecyclerView.ViewHolder(view) {
private var position: Int? = null
private val baseView: LinearLayout? = view.findViewById(R.id.baseView) as LinearLayout?
...
init {
baseView?.setOnClickListener {
listener?.onManufacturerSelected(position)
}
}
fun bind(position: Int) {
this.position = position
...
}
}
}
And from your activity/fragment set the listener as adapter.listener = this, and implement the onItemSelected(position: Int?)
override fun onItemSelected(position: Int?) {
...
}

Can't assign new value to a simple Singleton using an onClickeListener for a RecyclerView

I am trying to build a Slack-like chat app following a tutorial in a course I am taking online.
In the tutorial the instructor is using a ListView and the OnItemClickListener method, but I am trying to do it with recycler view, and I am stuck with the onClickListener in the adapter.
I have tried to find answers in other questions but couldn't find one that solves my problem. The closest ones were this and this
My two problems are:
1. The app's main activity has on the top of the screen a title that states what channel is currently active. I have created a singleton that holds the "current channel" and the title's text is being pulled from that singleton.
I am having a hard time changing the value of that singleton on click.
The main activity also has all the channels in a list view in a drawer.
I am trying to close the drawer when a channel is clicked but that isn't happening either.
This is my current adapter:
class ChannelsAdapter(val context: Context, val channels: ArrayList<Channel>) :
RecyclerView.Adapter<ChannelsAdapter.Holder>() {
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val singleChannel = itemView.findViewById<TextView>(R.id.single_channel)
val mainLayout = LayoutInflater.from(context).inflate(R.layout.activity_main, null)
fun bindText(textVar: String, context: Context) {
singleChannel.text = textVar
}
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bindText(channels[position].toString(), context)
holder.itemView.setOnClickListener {
ChannelName.activeChannel = channels[position]
holder.mainLayout.drawer_layout.closeDrawer(GravityCompat.START)
}
}
override fun getItemCount(): Int {
return channels.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelsAdapter.Holder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.channel_list_layout, parent, false)
return Holder(view)
}
}
This is the singleton
object ChannelName {
var activeChannel : Channel? = null
}
You can rewrite the setter for activeChanell variable and call a listener that has been added before to notify your Activity:
object ChannelName {
private val listeners = ArrayList<(Channel?) -> Unit>()
fun addChannelNameChangedListener(listener: (Channel?) -> Unit) {
listeners.add(listener)
}
fun removeChannelNameChangedListener(listener: (Channel?) -> Unit) {
listeners.remove(listener)
}
var activeChannel: Channel? = null
set(value) {
field = value
listeners.forEach { it.invoke(value) }
}
}
And inside the Activity add a listener like this:
ChannelName.addChannelNameChangedListener {
// Do your operation
}
The alternative solution is to use Observable utils like LiveData, so you shouldn't worry about the Android life cycle any more:
object ChannelName {
val activeChannel: MutableLiveData<ChannelName> = MutableLiveData()
}
To change the value inside your adapter simply call:
ChannelName.activeChannel.value = channels[position]
And inside your activity Observe to the variable by calling:
ChannelName.activeChannel.observe(this, Observer {
// Do your operation
})
class ChannelsAdapter(val context: Context, val channels: ArrayList<Channel>) :
RecyclerView.Adapter<ChannelsAdapter.Holder>() {
private var itemClickListener: OnItemClickListener? = null
fun setItemClickListener(itemClickListener: OnItemClickListener) {
this.itemClickListener = itemClickListener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
val singleChannel = itemView.findViewById<TextView>(R.id.single_channel)
val mainLayout = LayoutInflater.from(context).inflate(R.layout.activity_main, null)
fun bindText(textVar: String, context: Context) {
singleChannel.text = textVar
}
override fun onClick(v: View?) {
val position = adapterPosition
itemClickListener?.let {
if (position != RecyclerView.NO_POSITION) {
it.onItemClick(position)
}
}
}
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bindText(channels[position].toString(), context)
}
override fun getItemCount(): Int {
return channels.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelsAdapter.Holder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.channel_list_layout, parent, false)
return Holder(view)
}
}
This way you can setItemClickListener to the adapter in your activity and get callback from your recyclerView.
You should not set listener in onBind() method since it will be called more than your items' count.

Categories

Resources