why can't I set a setOnLongClick in this recyclerview adapter? - android

The problem: I haven't found a solution that works and allows me to use onLongClickListener in my recyclerview adapter
I understand that my options seem to be either implementing an interface or using a lambda, however I've been trying everything I can find and none of them are working.
places I have tried solutions from:
Item Onclick RecyclerView Kotlin Android
RecyclerView onClick in kotlin
How to add a click listener to my recycler view (Android kotlin)
RecyclerView itemClickListener in Kotlin
Recyclerview Card Item Onclick Kotlin
and everything else I could find when googling "kotlin recyclerview onClickAdapter"
I get different errors with every solution, but the main takeaway is that none of them are working, which tells me that there is probably a problem with my adapter code to begin with.
Example of one error I get: If I try to use setOnClickListener in the bind function of TaskViewHolder, I get the error of
Wrong return type, expecting Boolean, received Unit
There is no passed in list because I use submitList from a viewmodel with a room database
The adapter code is based off of the Room with a View code
I have
Adapter Code:
class TaskRvAdapter : ListAdapter<Task, TaskRvAdapter.TaskViewHolder>(TaskComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
return TaskViewHolder.create(parent)
}
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
val current = getItem(position)
holder.bind(current.task)
}
class TaskViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val taskItemView: TextView = itemView.findViewById(R.id.task_rv_item)
fun bind(text: String?) {
taskItemView.text = text
}
companion object {
fun create(parent: ViewGroup): TaskViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.task_rv_item, parent, false)
return TaskViewHolder(view)
}
}
}
class TaskComparator : DiffUtil.ItemCallback<Task>() {
override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem.task == newItem.task
}
}
}

try with this code, properly formatted
fun bind(text: String?) {
taskItemView.text = text
itemView.setOnClickListener {
Log.i("TaskRvAdapter", "item clicked: "+text)
}
itemView.setOnLongClickListener{
Log.i("TaskRvAdapter", "item long clicked: "+text)
return#setOnLongClickListener true
}
}

Related

How to update the data passed to the adapter without recreating it android kotlin

Faced such a problem: I have a RecyclerView, the data for which I get from the ViewModel using StateFlow:
viewModel.items.collect {
setRecyclerView(items)
}
Then, let's say, somewhere inside the Fragment, I change the data for items and there are more of them. In order for my RecyclerView to see my changes, I have to call the setRecyclerView(items) function again, which, it seems to me, can lead to the most unexpected consequences (for example, items will be duplicated). The question is: is it possible to somehow change the data and update the RecyclerView (including the onBindViewHolder function in it) without yet another creation of an Adapter?
Let's start talking about the adapter implementation. Reading your question, I believe you used RecyclerView.Adapter to implement your adapter. But there is another option that is simpler and more performant than this. It's the ListAdapter:
The most interesting thing about ListAdapter is the DiffUtil, that have a performative way to check if any item on your list was updated, deleted, or included. Here's a sample of the implementation:
abstract class MyAdapter: ListAdapter<ItemModel, MyAdapter.MyViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = ItemSimplePosterBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
class MyViewHolder(
private val binding: ItemSimplePosterBinding
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ItemModel) {
// Here you can get the item values to put these values on your view
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compares like this below
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// compare the objects
return oldItem == newItem
}
}
}
}
So, when your list is updated, you just have to call the submitList from the adapter, like this:
viewModel.items.collectLatest { items ->
// You will send the items to your adapter here
adapter.submitList(items)
}
Then, your RecyclerView just has to be configured on onViewCreated for example, and your list can be defined and updated in another place, observing the items change from ViewModel.

RecyclerView scrolls to changed item location, when first item changes

I have a RecyclerView with a checkbox for every item. When checkbox pressed it is moved to the bottom of the list. When i check any item besides the first one everything is OK but if i check the first one, it auto-scroll to the new item location.
I redesigned the adapter using a recent codelab but still the same issue
class ItemsAdapter(val clickListener: ItemAdapterListener):
ListAdapter<UiQueryItem<Item>, ItemsAdapter.ViewHolder>(asyncDifferConfig) {
companion object {
private val diffCallback = object : UiQueryItemDiffCallback<Item>() {}
private val executors = AppExecutors()
private val asyncDifferConfig =
AsyncDifferConfig.Builder<UiQueryItem<Item>>(diffCallback)
.setBackgroundThreadExecutor(executors.cpuExecutorService)
.build()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item,clickListener)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(val binding: ListItemBinding): RecyclerView.ViewHolder(binding.root){
fun bind(item: UiQueryItem<Item>, clickListener: ItemAdapterListener) {
binding.item = item.item
binding.clickListener = clickListener
binding.isSelected = item.isFlag1
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
I think i found a workaround.
I noticed the next github issue solution:
Automatic scrolling when new data set #224
It did not work in my case but after tweaking a bit i found an answer. Please note that it is only a workaround and might make some bugs if you are trying to implement your own auto-scroll solution. The Workaround simply checkes if an item changed position from first place and if so, gets back to first position
Also note that #elihart is suggesting other solutions (check the link)
private val dataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
if (fromPosition == 0) {
binding.itemRv.scrollToPosition(0)
}
}
}
override fun onResume() {
super.onResume()
adapter.registerAdapterDataObserver(dataObserver)
}
override fun onPause() {
super.onPause()
adapter.unregisterAdapterDataObserver(dataObserver)
}

Items in recyclerview are not shown unless declaring adapter again in the observer methode

I'm having a problem with my adapter/recyclerview. I want the user to see an overview of appointments like this:
But i can only do this be setting the adapter for the recylerview in the observe method.
This the code:
val adapter = CalendarAdapter()
binding.appointmentList.adapter = adapter
calendarDataViewModel?.appointments?.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.data = it
binding.appointmentList.adapter = adapter
}
})
Like you see, if I set the adapter again inside the observe method, the items will be shown, but if I don't do that, nothing will be shown and it will just be an empty screen.
I also tried the following code, but this also doesnt work:
val adapter = CalendarAdapter()
binding.appointmentList.adapter = adapter
calendarDataViewModel?.appointments?.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.submitList(it)
}
})
I can't seem to figure out why this isnt working.
Appointments is of the type LiveData<List>
Does anyone know how I can fix this?
This is my CalendarAdapter:
class CalendarAdapter : ListAdapter<CalendarData, RecyclerView.ViewHolder>(AppointmentDiffCallback()) {
var data = listOf<CalendarData>()
override fun getItemCount(): Int {
return data.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return AppointmentViewHolder(CalendarItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val appointment = data[position]
(holder as AppointmentViewHolder).bind(appointment)
}
}
private class AppointmentDiffCallback : DiffUtil.ItemCallback<CalendarData>() {
override fun areItemsTheSame(oldItem: CalendarData, newItem: CalendarData): Boolean {
return oldItem.appointmentId == newItem.appointmentId
}
#SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: CalendarData, newItem: CalendarData): Boolean {
return oldItem == newItem
}
}
You do not need to override the getItemCount method. In your case, data.size returns 0 due to which the items are never shown.
Also, you do not need to keep track of your items, ListAdapter abstract class already does that. You can get your Item using getItem(Int) method. Your adapter should be something like so:
class CalendarAdapter : ListAdapter<CalendarData, RecyclerView.ViewHolder>(AppointmentDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return AppointmentViewHolder(CalendarItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val appointment = getItem(position)
(holder as AppointmentViewHolder).bind(appointment)
}
}
Then in your observer you can the submitList method like in your second code block in the question.

how to add, remove the ListAdapter Items in android?

I know The ListAdpater extends a recyclerview adapter. And for initializing items, I use submitList method. the parameter in submitList method is defined like this, typed list.
public void submitList(#Nullable List<T> list) {
mDiffer.submitList(list);
}
I can't remove or add items. So I tried using arraylist variable for adding and removing, and casted it to List. But I'm not sure this is the good way...
Because arraylist is not good to add or remove items. Is there other way?
Delete item from ListAdapter. (Kotlin)
fun removeItem(position: Int){
val currentList = adapter.currentList.toMutableList()
currentList.removeAt(position)
adapter.submitList(currentList)
}
Tested and working fine.
Just call submitList with a new list, the diffutil will take care of the old list and the new list.
If you add or remove something, add to your original list or remove an item from your list and then call submitList.
you can call use adapter.currentList to get your current list, or just use a viewModel to hold your list items.
MyAdapter
class FavoritesAdapter(
private val listeners: OnFavoriteDetailPage,
private val itemClick: (title: String, price: String) -> Unit) :ListAdapter<Favorites, FavoritesAdapter.RecyclerViewHolder>(ListComparator()) {
//bind the recycler list items
inner class RecyclerViewHolder(val binding: FavoriteListBinding) :
RecyclerView.ViewHolder(binding.root) {
#SuppressLint("SetTextI18n")
fun bind(list: Favorites) {
binding.deal.text = "Deal " + list.id.toString()
binding.price.text = "Ghc " + list.price
binding.pizzaSize.text = list.size.toString()
binding.posterBanner.load(list.imgUrl)
}
}
inflate the List
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
return RecyclerViewHolder(
FavoriteListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
//bind the model list the recycler list
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
val getItemPosition = getItem(position)
holder.bind(getItemPosition)
Glide.with(holder.itemView.context).load(getItemPosition.imgUrl)
.into(holder.binding.posterBanner)
holder.binding.addCart.setOnClickListener {
itemClick("Deal ${getItemPosition.id}", getItemPosition.price)
}
holder.itemView.setOnClickListener {
listeners.viewDetail(getItemPosition)
}
holder.binding.favoriteHeart.setOnClickListener {
listeners.onItemRemoveClick(getItemPosition.id)
}
}
class ListComparator : DiffUtil.ItemCallback<Favorites>() {
override fun areItemsTheSame(oldItem: Favorites, newItem: Favorites): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Favorites, newItem: Favorites): Boolean {
return oldItem.id == newItem.id
}
}
interface OnFavoriteDetailPage {
fun viewDetail(favorites: Favorites)
fun onItemRemoveClick(position: Int)
}
}
Inside my fragment
override fun onItemRemoveClick(position: Int) {
val currentList = recyclerAdapter.currentList.toMutableList()
currentList.removeAt(position)
recyclerAdapter.submitList(currentList)
}
Everythings works fine but mine delete from the bottom of the list instead of the current postion. What could be the problem?

Populate RecyclerView from Firestore async call

I'm able to retrieve data from Firestore (it is definetly available inside the GlobalScope of populateValletList() and I was able to populate my RecyclerView from an asnyc database call or when I simply added a Vallet to my items list manually inside populateValletList(), however, when I want to populate that View from Firestore data it doesn't work. The
onBindViewHolder in RecyclerAdapter doesn't get called anymore
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder){
is ValletViewHolder ->{
holder.bind(items[position])
}
}
}
that's how I populate my items private var items : MutableList<Vallet> = ArrayList() :
fun populateValletList() {
GlobalScope.launch {
items = getAllValletsFromDatabase.executeUseCase()
}
}
I init my RecyclerView inside onActivityCreated
private fun initRecyclerView(){
recycler_view_vallets.apply{
layoutManager = LinearLayoutManager(context)
addItemDecoration(ValletRecyclerAdapter.ValletItemDecorator(30))
valletAdapter = ValletRecyclerAdapter()
adapter = walletAdapter
}
valletAdapter.populateValletList()
}
This is my first time working with coroutines, what am I overlooking here?
Ok, I think there is some improvements that you could in your code, but the reason I believe is not working the way you want is because I you update your Items List, you have to call notifyDataSetChanged() on your adapter.
Ideally u should run ur coroutine using a scope that is not global, to avoid leaks, you can use a viewmodel for it, or you could use lifecycleScope.run { } in ur fragment, for that I believe you will need to add a depedency.
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
And regarding update your recycler, I recommend using ListAdapter, so it adds DIFF Utils and makes easier to update values.
To sum up.
Ur Recycler Adapter would be like this:
class HomePatchesAdapter : ListAdapter<Vallet, RecyclerView.ViewHolder>(REPO_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return HomePatchesViewHolder.create(parent, viewType)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val repoItem = getItem(position)
if (repoItem != null) {
(holder as HomePatchesViewHolder).bind(repoItem)
}
}
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Vallet>() {
override fun areItemsTheSame(oldItem: Vallet, newItem: Vallet): Boolean =
oldItem.name == newItem.name
override fun areContentsTheSame(oldItem: Vallet, newItem: Vallet): Boolean =
oldItem == newItem
}
}
}
and ur fragment would be like this:
private fun initRecyclerView(){
recycler_view_vallets.apply{
layoutManager = LinearLayoutManager(context)
addItemDecoration(ValletRecyclerAdapter.ValletItemDecorator(30))
}
val adapter = HomePatchesAdapter()
recycler_view_vallets?.adapter = adapter
lifecycleScope.run {
adapter.submitList(getAllValletsFromDatabase.executeUseCase())
}
}
Let me know if it makes sense.

Categories

Resources