Items keeps shuffling while scrolling with PagingDataAdapter with Paging3
Below is ViewHolder DiffUtil callback, but it still changes items while scrolling
companion object {
private val diffUtil = object : DiffUtil.ItemCallback<Booking>() {
override fun areItemsTheSame(oldItem: Booking, newItem: Booking): Boolean {
return (oldItem.orderStatus?.Id == newItem.orderStatus?.Id) && (oldItem.orderId == newItem.orderId)
}
override fun areContentsTheSame(oldItem: Booking, newItem: Booking): Boolean {
return oldItem == newItem
}
}
}
Related
data class A(
val id: String
): BaseItem() {
override fun equals(other: Any?): Boolean {
// Need to Update every time even it is the same item
return false
}
override fun hashCode(): Int {
return id.hashCode()
}
}
data class B(
val id: String
): BaseItem() {
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is A) return false
return other.id == this.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
data class C(
val id: String
): BaseItem() {
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is A) return false
return other.id == this.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
companion object {
val DIFF = object: DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem.getId() == newItem.getId()
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
}
I have 3 types of data class for the RecyclerView.
When using DiffUtil, I want to make the change every time only for the type A.
Should I return false when A.equals() called?
data class A(...) {
override fun equals(other: Any?): Boolean {
// Need to Update every time even it is the same item
return false
}
}
Would it be solved by making equals() of type A class always return false?
I have a recycler view which has checkboxes as items. I want to make sure there cannot be selected more than 3 checkboxes. If 3 checkboxes are selected other checkboxes should not be selectable, only selected checkboxes should be able to be deselected. I am not sure if I need to implement that behaviour in adapter or fragment which holds adapter. This is how my adapter looks like:
class HomeAdapter(
private val listener: OnCheckBoxClickListener
) : ListAdapter<String, HomeAdapter.SurveyViewHolder>(HOME_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SurveyViewHolder {
val bind = ItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SurveyViewHolder(bind)
}
override fun onBindViewHolder(holder: SurveyViewHolder, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
inner class SurveyViewHolder(
val binding: ItemHomeBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(answer: String) = with(binding) {
checkBox.text = answer
}
init {
binding.checkBox.setOnCheckedChangeListener { _, isChecked ->
listener.onCheckBoxClickListener(
index = bindingAdapterPosition,
isChecked = isChecked,
answer = currentList[bindingAdapterPosition]
)
}
}
}
interface OnCheckBoxClickListener {
fun onCheckBoxClickListener(index: Int, isChecked: Boolean, answer: String)
}
companion object {
private val HOME_COMPARATOR = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
}
}
}
So far, I have tried something like this in fragment:
private fun countCheckedCheckBoxes() = with(binding) {
var num = 0
for (i in 0..recyclerView.childCount) {
(recyclerView.getChildAt(i) as? CheckBox).also {
if (it?.isChecked == true) {
num += 1
}
}
}
if (num == 3) {
// TODO do something here
}
}
I create an app to fetch some data from the REST API. I had some issues with recycler view scrolling. When I scroll down and then scroll up all the items will be messed up. I searched in stack overflow and find an answer. The answer said that we should add setHasStableIds = true to our adapter and add getItemViewType to our adapter class. I did and it worked fine but the new problem came.
the image of the items should load sequentially. For instance, if the user scrolled down to item 200 he/she should wait until all previous items load it images.
This is my adapter code
class MovieAdapter : PagingDataAdapter<Movie,MovieAdapter.MovieViewHolder>(DiffUtilCallback()) {
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
try {
holder.bind(getItem(position)!!)
}catch (e : Exception){
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.movie_list_item,parent,false)
return MovieViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
return position
}
/////
class MovieViewHolder(view : View) : RecyclerView.ViewHolder(view){
private val imgMoviePoster = view.findViewById<ImageView>(R.id.imgImagePoster)
private val txtMovieTitle = view.findViewById<TextView>(R.id.txtMovieTitle)
private val txtMovieReleaseDate = view.findViewById<TextView>(R.id.txtMovieReleaseDate)
fun bind(movie : Movie){
txtMovieTitle.text = movie.title
txtMovieReleaseDate.text = movie.releaseDate
val moviePosterUrl = POSTER_BASE_URL + movie.posterPath
/*Glide.with(imgMoviePoster)
.load(moviePosterUrl)
.into(imgMoviePoster)*/
Picasso.get().load(moviePosterUrl).into(imgMoviePoster)
}
}
/////
class DiffUtilCallback : DiffUtil.ItemCallback<Movie>(){
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return newItem.id == oldItem.id
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return newItem == oldItem
}
}
}
This is my code for recycler view and view model (inside main activity)
private fun initRecyclerView(){
recyclerViewPopMovie.apply {
layoutManager = GridLayoutManager(this#MainActivity,2)
movieAdapter = MovieAdapter()
adapter = movieAdapter
}
}
private fun initViewModel(){
lifecycleScope.launchWhenCreated {
viewModel.getContent().collectLatest {
movieAdapter.submitData(it)
movieAdapter.setHasStableIds(true)
}
}
}
I know how to implement DiffUtil if the adapter receive single data type objects.
But in my case I have an adapter which get two data types from a fragment.
So how can I use DiffUtil with multiple data types?
Here is my adapter code:
class VisitorsAdapter(val listener: VisitorsViewHolder.OnVisitorClicked) :
RecyclerView.Adapter<BaseViewHolder<*>>() {
private var visitorsData = mutableListOf<Any>()
private var isOwner = true
fun setData(visitorsData: List<Any>) {
Log.e("TAG", "setData called")
this.visitorsData.clear()
var currentVisitors = mutableListOf<CurrentVisitorResponseItem>()
var leavedVisitors = mutableListOf<LeavedVisitorsResponseItem>()
for (visitor in visitorsData) {
when (visitor) {
is CurrentVisitorResponseItem -> currentVisitors.add(visitor)
is LeavedVisitorsResponseItem -> leavedVisitors.add(visitor)
}
}
currentVisitors = currentVisitors.sortedBy { it.roomNumber?.toInt() }.toMutableList()
leavedVisitors = leavedVisitors.sortedBy { it.room }.toMutableList()
this.visitorsData.addAll(currentVisitors)
this.visitorsData.addAll(leavedVisitors)
notifyDataSetChanged()
}
fun isOwner(chooser: Boolean) {
this.isOwner = chooser
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return VisitorsViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.visitor_item, parent, false)
)
}
override fun getItemCount() = visitorsData.size
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val dataPositioned = visitorsData[position]
when (holder) {
is VisitorsViewHolder -> {
holder.bind(dataPositioned, position, listener, isOwner)
}
}
}
}
Note: I have 2 types of data:
1- CurrentVisitorResponseItem
2- LeavedVisitorsResponseItem
I found the solution!
First of all, if we try to make the type of ItemCallback (Any) we will face a problem
says 'Suspicious equality check: equals() is not implemented in Object DiffUtilEquals'
The solution for this is to make an interface that override equals function
and implement that interface on the data classes that we want to pass. Like this:
interface Equatable {
override fun equals(other: Any?): Boolean
}
data class CurrentVisitorResponseItem() : Equatable
data class LeavedVisitorsResponseItem() : Equatable
Then, the adapter class will be like this:
class VisitorsAdapter(val listener: VisitorsViewHolder.OnVisitorClicked) :
RecyclerView.Adapter<BaseViewHolder<*>>() {
private var isOwner = true
private val differCallBack = object : DiffUtil.ItemCallback<Equatable>() {
override fun areItemsTheSame(oldItem: Equatable, newItem: Equatable): Boolean {
Log.e("TAG", "I'm in the areItemsTheSame")
return when {
oldItem is CurrentVisitorResponseItem && newItem is CurrentVisitorResponseItem -> {
oldItem.id == newItem.id
}
oldItem is LeavedVisitorsResponseItem && newItem is LeavedVisitorsResponseItem -> {
oldItem.id == newItem.id
}
else -> false
}
}
override fun areContentsTheSame(oldItem: Equatable, newItem: Equatable): Boolean {
return when {
oldItem is CurrentVisitorResponseItem && newItem is CurrentVisitorResponseItem -> {
oldItem == newItem
}
oldItem is LeavedVisitorsResponseItem && newItem is LeavedVisitorsResponseItem -> {
oldItem == newItem
}
else -> false
}
}
}
val differ = AsyncListDiffer(this, differCallBack)
fun isOwner(chooser: Boolean) {
this.isOwner = chooser
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return VisitorsViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.visitor_item, parent, false)
)
}
override fun getItemCount() = differ.currentList.size
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val dataPositioned = differ.currentList[position]
when (holder) {
is VisitorsViewHolder -> {
holder.bind(dataPositioned, position, listener, isOwner)
}
}
}
}
How to add new item also different ViewType and show it on recyclerView when user scroll to the last position?
I already fetch data from loadInitial and loadAfter.
But I wondered How to add new item when the API send me the last item?
For Example API send data size 50 and when user scroll to the position 49
I need to add the another one which is my custom ViewType into adapter and the size will be 51 position 50
Here is my code
It will work for you
class FollowFeedPagedListAdapter(val context: Context, val itemClicked: (FollowFeedModel) -> Unit) : PagedListAdapter<FollowFeedModel, RecyclerView.ViewHolder>(DIFF_COMPARATOR) {
val TYPE_NORMAL = 1
val TYPE_LAST_POSITION = 2
companion object {
private val DIFF_COMPARATOR = object : DiffUtil.ItemCallback<FollowFeedModel>() {
override fun areItemsTheSame(oldItem: FollowFeedModel, newItem: FollowFeedModel): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: FollowFeedModel, newItem: FollowFeedModel): Boolean {
return oldItem.id == newItem.id
}
}
private var hasMore: Boolean = true
#JvmStatic
fun getHasMoreInstance() = hasMore
#JvmStatic
fun setHasMoreInstance(boolean: Boolean) {
hasMore = boolean
}
}
override fun getItemCount(): Int {
val size = super.getItemCount()
//เพิ่มอันสุดท้าย
return size+1
}
override fun getItemViewType(position: Int): Int {
if (position == itemCount - 1) {
return TYPE_LAST_POSITION
} else {
return TYPE_NORMAL
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
TYPE_NORMAL -> {
val view = LayoutInflater.from(context).inflate(R.layout.row_summaries_layout_version2, parent, false)
return SummariesModelViewHolder(view, itemClicked)
}
TYPE_LAST_POSITION -> {
val view = LayoutInflater.from(context).inflate(R.layout.layout_no_more_follow_feed, parent, false)
return NoMoreFollowFeedViewHolder(view)
}
else -> {
val view = LayoutInflater.from(context).inflate(R.layout.layout_no_more_follow_feed, parent, false)
return SummariesModelViewHolder(view, itemClicked)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
TYPE_NORMAL -> {
val holder = holder as SummariesModelViewHolder
holder.binding(getItem(position))
}
TYPE_LAST_POSITION -> {
val holder = holder as NoMoreFollowFeedViewHolder
}
}
}
After loading 50 items from server (probably in 3 calls of 20+20+10)
Just generate/fetch items that you want to be displayed after 50th position,
Than return those generated/fetched items by calling:
'callback.onResult'