I'm using paging 3 to paginate my items and then show them in recycler view. but there's a time that the first item change. for example, consider the situation below:
item A
item B
item C
after a while, for some reason, data gets update and list order changes to like bellow:
item B
item A
item C
my problem is:
recycler view stays exactly in its last position. I mean when update apply, item B will go to the first position but recycler view won't scroll to the top, it just states pointing to item A, and the user won't notify that item B has changed,
how can I set that recycler view scroll automatically just when the first item changed?
here is part of my adapter:
class TicketListAdapter : PagingDataAdapter<TicketListModel, TicketListAdapter.ViewHolder>(
diffCallback = object : DiffUtil.ItemCallback<TicketListModel>() {
override fun areItemsTheSame(oldItem: TicketListModel, newItem: TicketListModel): Boolean {
return oldItem.ticket_id == newItem.ticket_id
}
override fun areContentsTheSame(
oldItem: TicketListModel,
newItem: TicketListModel
): Boolean {
return oldItem == newItem
}
}
){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.ticket_list_item_model, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int)= holder.bind(getItem(position))
and here shows how I use my recycler view and data:
binding.recyclerView.apply {
this.adapter = adapter
this.layoutManager = LinearLayoutManager(requireContext())
}
viewModel.tickets.observe(viewLifecycleOwner, {
viewLifecycleOwner.lifecycleScope.launch {
adapter.submitData(it)
}
})
I also try that set observer but it didn't work:
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
if (adapter.getItemId(0) != firstItemId) {
binding.recyclerView.smoothScrollToPosition(0)
firstItemId = adapter.getItemId(0)
}
}
the firstItemId is initialized with value -1;
Calling adapter.notifyDataSetChanged() will make the recyclerview to reload, then your items should adjust.
Regarding scrolling (dublicate), have a look here: Scroll RecyclerView to show selected item on top
Related
I have a RecyclerView showing movie categories fetched with retrofit from an api.
Based on it's preferences the user should be able to show (checkbox is checked) or hide (checkbox is unchecked) categories.
Those setting should proceed in Fragment A. After clicking on a save button (or something similar) the check-state of the items/categries should be saved (for next app starts) and the checked items should be passed in a new list that it's showed in a "new" Recyclerview in Fragment B.
I read about Recyclerview-Selection, but it's hard to find a clear step by step tutorial, that's following a similar process as I need (and most of them are written in Java, as I am a completely programming beginner it's even harder to understand it).
So I hope that someone here can help me to get on the right way, be able to implement this also for tv-rv and series-rv.
This is my current adapter, nothing special (in my item-layout there is also a checkbox, id = rvCheckBox) but I am not sure on how to implent it here.
class MovieCategoryAdapter : ListAdapter<Data, MovieCategoryAdapter.ViewHolder>(
MOVIE_CATEGORY_COMPERATOR) {
inner class ViewHolder(val binding: RvItemMoviecategoryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(category: Data) {
binding.apply {
rvItemMoviecategory.text = category.title
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
RvItemMoviecategoryBinding.inflate(
LayoutInflater.from(
parent.context
),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val moviegenre = getItem(position)!!
holder.bind(moviegenre)
holder.binding
}
companion object {
private val MOVIE_CATEGORY_COMPERATOR = object : DiffUtil.ItemCallback<Data>() {
override fun areItemsTheSame(oldItem: Data, newItem: Data) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Data, newItem: Data) =
oldItem == newItem
}
}
}
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.
I'm trying to scroll to a recycler view item if the item is expanded and/or clicked. I know that binding.recyclerview.layoutManager.scrollToPosition() can achieve this but I can't call the recycler view in the adapter class. Can I pass a boolean and position then say its true when the card item is clicked, then pass that to the main activity somehow..? I'm still very much a beginner so steps to do achieve this with best practices would really help me out. :)
EDIT*
So we got the above issue figured out. Now I am having a new issue where the item is scrolled to when clicked, but the expanded description text that appears after clicking is not scrolled into view. Lol ugh. Would really appreciate any help with this.
Hope you are having a great day!
class MainAdapter: RecyclerView.Adapter<MainViewHolder>() {
var movies = mutableListOf<Model>()
#SuppressLint("NotifyDataSetChanged")
fun setMovieList(movies: List<Model>) {
this.movies = movies.toMutableList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = AdapterMovieBinding.inflate(inflater, parent, false)
return MainViewHolder(binding)
}
#SuppressLint("NotifyDataSetChanged")
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val movie = movies[position]
holder.binding.name.text = movie.name
holder.binding.category.text = movie.category
holder.binding.description.text = movie.desc
Glide.with(holder.itemView.context).load(movie.imageUrl).into(holder.binding.imageview)
holder.binding.expandedView.visibility = if (movie.expand) View.VISIBLE else View.GONE
holder.binding.cardLayout.setOnClickListener {
movie.expand = !movie.expand
// scroll to expanded item
notifyDataSetChanged()
}
}
override fun getItemCount(): Int {
return movies.size
}
}
class MainViewHolder(val binding: AdapterMovieBinding) :
RecyclerView.ViewHolder(binding.root)
I have a recyclerview with a list of items , when i check an item and scroll until the checked checkbox disppear , and scroll again to it , the checkbox become unchecked
this is my adapter code
class ContactsAdapter(var list:ArrayList<contact>, private val listener: (contact) -> Unit):RecyclerView.Adapter<ContactsAdapter.viewHolder>() {
inner class viewHolder(view:View):RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): viewHolder {
var view = LayoutInflater.from(parent.context).inflate(R.layout.item_ui, parent, false)
return viewHolder(view)
}
override fun onBindViewHolder(holder: viewHolder, position: Int) {
var itemPosition = list[position]
holder.itemView.apply {
contact_name.text=itemPosition.contactName
contact_number.text=itemPosition.contactNumber[0]
check_number.setOnClickListener {
listener(itemPosition)
}
check_number.isChecked = itemPosition.checked
}
}
override fun getItemCount(): Int {
return list.size
}
}
A RecyclerView recycles its rows. Your setOnClickListener() needs to do something to hold onto which items are checked and unchecked, and your onBindViewHolder() needs to update the CheckBox when it binds an item. It seems like the second part is implemented, but perhaps not the first part. You may need to update itemPosition.checked in your lambda for setOnClickListener().
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?