I am using paging library with recyclerView and my task is to shows empty viewHolder when I receive empty arrayList from server.
I tried to check that list is empty or not, but when I get response it calls the Observer method 2 times (1st time empty list, 2nd time real list), after when I use swipeRefresh it DataSource class sends empty list as it has same list already in cache. Also I have pagination and checking for dataset size is difficult as there are many cases.
/**
* Simple Adapter used to show list of Appeals with pagination
*/
class MyAppealsAdapter : PagedListAdapter<Appeal, RecyclerView.ViewHolder>(diffCallBack) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return AppealsViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_my_appeal, parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is AppealsViewHolder -> holder.bind(position)
}
}
inner class AppealsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
#SuppressLint("SimpleDateFormat")
fun bind(position: Int) {
getItem(position)?.let { item ->
item.fields?.get(AppealKeyObj.K_SUB_ISSUE)?.let {
itemView.mTypeOfAppeal.text = it
}
}
}
}
companion object {
open var diffCallBack: DiffUtil.ItemCallback<Appeal> = object : DiffUtil.ItemCallback<Appeal>() {
override fun areItemsTheSame(oldItem: Appeal, newItem: Appeal): Boolean {
return oldItem.id === oldItem.id
}
override fun areContentsTheSame(oldItem: Appeal, newItem: Appeal): Boolean {
return oldItem == newItem
}
}
}
}
Please help to show empty viewHolder when I receive empty arrayList
I did it like this
override fun getItemViewType(position: Int): Int {
return if (super.getItemCount() == 0) EMPTY else FILLED
}
override fun getItemCount(): Int {
return if (super.getItemCount() == 0) 1 else super.getItemCount()
}
And also changed linearlayoutmanager to custom one with overriden method
override fun onLayoutChildren(recycler: Recycler?, state: RecyclerView.State?) {
try {
super.onLayoutChildren(recycler, state)
} catch (e: IndexOutOfBoundsException) {
Timber.e(e)
}
}
Related
So, I am creating a cocktail app, based on the https://www.thecocktaildb.com/ api. Thus far, I have only created a screen to display options based on the ingredient I put in the search bar (search bar is not done yet). Yet when I run the app, only the first entry is displayed
By putting Log.e("TAG", "$position") inside of my onBindViewHolder, of the adapter, I saw that the position variable never increases from 0
class CocktailsAdapter: RecyclerView.Adapter<CocktailsAdapter.CocktailsViewHolder>() {
inner class CocktailsViewHolder(val binding: ItemCocktailPreviewBinding) :
RecyclerView.ViewHolder(binding.root)
private val differCallback = object : DiffUtil.ItemCallback<CocktailsByBaseDto>() {
override fun areItemsTheSame( oldItem: CocktailsByBaseDto, newItem: CocktailsByBaseDto): Boolean {
return oldItem.drinks[0].idDrink == newItem.drinks[0].idDrink
}
override fun areContentsTheSame(oldItem: CocktailsByBaseDto, newItem: CocktailsByBaseDto): Boolean {
return oldItem.drinks[0] == newItem.drinks[0]
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CocktailsViewHolder {
return CocktailsViewHolder(
ItemCocktailPreviewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: CocktailsViewHolder, position: Int) {
val binding = holder.binding
val cocktail = differ.currentList[position]
holder.itemView.apply {
Glide.with(this).load(cocktail.drinks[position].strDrinkThumb).into(binding.imgCocktailsMainRecyclerViewImage)
binding.tvCocktailsMainRecyclerViewTitle.text = cocktail.drinks[position].strDrink
Log.e("TAG", "$position")
setOnClickListener {
onItemClickListener?.let { it(cocktail) }
}
}
}
override fun getItemCount(): Int {
return differ.currentList.size
}
private var onItemClickListener: ((CocktailsByBaseDto) -> Unit)? = null
fun setOnItemClickListener(listener: (CocktailsByBaseDto) -> Unit) {
onItemClickListener = listener
}
I have tried both position and 0 (which makes more sense) inside val cocktail = differ.currentList[position], but neither gave me a different result
Fixed it by changing the class I had passed to DiffUtil.ItemCallback
Hey I am working in android kotlin. I am working in reyclerview. I want to do single selection in my items. I tried some code which is working fine.
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
private var selectedItemPosition: Int = 0
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
selectedItemPosition = holder.bindingAdapterPosition
notifyAdapter()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
holder.binding.root.background =
ContextCompat.getDrawable(holder.binding.root.context, drawableColor)
holder.bindItem(getItem(position), position)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(item: ProductVariant?, position: Int) {
}
}
Video for single click is working fine.
When I move onBindViewHolder code inside bindItem it not working. Can someone guide me why this is happening.
vs
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.bindItem(getItem(position), position ,::notifyAdapter)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
private var selectedItemPosition: Int = 0
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(
item: ProductVariant?,
position: Int,
onAdapterChange: () -> Unit
) {
binding.root.setOnClickListener {
selectedItemPosition = position
onAdapterChange()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
binding.root.background =
ContextCompat.getDrawable(binding.root.context, drawableColor)
}
}
The video for single click is not working.
You moved the selectedItemPosition into the ViewHolder class, so you have a separate copy of this property for every instance of the ViewHolder class so you are no longer affecting other items in the list when any one list item is clicked.
This would be much easier to implement by making the view holder class an inner class of the Adapter so it can directly modify the adapter’s selectedItemPosition property. And I would give the property a custom setter so you can automatically notify the adapter of the change instead of having to work with a separate function call. You can also make it notify the adapter specifically of which two row items changed instead of the whole data set (more efficient and by doing it in the property setter you have access to the old and new row positions in one place easily—field and value).
private var selectedItemPosition: Int = 0
set(value) {
val oldPos = field
field = value
notifyItemChanged(oldPos)
notifyItemChanged(value)
}
Even though you are passing the method reference notifyAdapter() in bindItem() function you are not calling it in the OnClickListener that is why it is not working.
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.
I use a binding for a ListAdapter with the definition of a DiffUtil.ItemCallback. When deleting items (at least 2) I have an IndexOutOfBoundsException.
The update of the list works (the number of elements is indeed N-1 after deletion) but not the position of the item, which is kept is the call. The exception's therefore thrown when calling getItem(position) (in the onBindViewHolder). NB: A log of getItemCount() just before the getItem(position) shows that the list contains N-1 elements.
I created a small repo: https://github.com/jeremy-giles/DiffListAdapterTest (with a same configuration to my project) which reproduces the problem.
ItemAdapter class
class ItemAdapter(
var listener: ListAdapterListener) : DataBindingAdapter<Item>(DiffCallback()) {
class DiffCallback : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}
override fun getItemViewType(position: Int) = R.layout.recycler_item
override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
super.onBindViewHolder(holder, position)
holder.itemView.tv_position.text = "Pos: $position"
holder.itemView.setOnLongClickListener {
Timber.d("List item count: ${itemCount}, position: $position")
listener.onLongViewClick(getItem(position), position)
}
}
interface ListAdapterListener {
fun onLongViewClick(item: Item, position: Int) : Boolean
}
}
BindingUtils classes
abstract class DataBindingAdapter<T>(diffCallback: DiffUtil.ItemCallback<T>) :
ListAdapter<T, DataBindingViewHolder<T>>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder<T> {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, viewType, parent, false)
return DataBindingViewHolder(binding)
}
override fun onBindViewHolder(holder: DataBindingViewHolder<T>, position: Int) {
holder.bind(getItem(position))
}
}
class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: T) {
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
}
And in my MainActivity class I use a LiveData to update the recyclerView
itemViewModel.getListObserver().observe(this, Observer {
Timber.d("List Observer, items count ${it.size}")
itemAdapter.submitList(it.toList())
})
In your onBindViewHolder update usage of 'position' to 'holder.getAdapterPosition()':
override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
super.onBindViewHolder(holder, position)
holder.itemView.tv_position.text = "Pos: $position"
holder.itemView.setOnLongClickListener {
Timber.d("List item count: ${itemCount}, position: $position")
listener.onLongViewClick(getItem(holder.getAdapterPosition()), holder.getAdapterPosition())
}
}
Currently holder.getAdapterPosition() is deprecated. As per documentation, you might want to use either one of the following two methods.
If you are calling this in the context of an Adapter, you probably
want to call getBindingAdapterPosition() or if you want the position
as RecyclerView sees it, you should call getAbsoluteAdapterPosition().
I'm trying to make 2 view holder one will be at position 0 always which will be a header view for selecting image, another for displaying the model,
I can do it for normal recyclerview adapter but in firestore recycler adapter i can't do it,
what I try it increase the item count by 1 but I got
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
This is my adapter class code:
val TYPE_SELECT_IMAGE = 0
val TYPE_ITEMS = 1
class UsersImageAdapter(options: FirestoreRecyclerOptions<UserImage>)
: FirestoreRecyclerAdapter<UserImage, RecyclerView.ViewHolder>(options) , AnkoLogger{
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, model: UserImage) {
when(holder){
is SelectImageViewHolder -> {
holder.bindModel()
}
is UserImageViewHolder -> {
holder.bindImages(model)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_SELECT_IMAGE -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_add_image, parent, false)
SelectImageViewHolder(view)
}
TYPE_ITEMS -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_image,parent,false)
UserImageViewHolder(view)
}
else -> throw RuntimeException()
}
}
override fun getItemViewType(position: Int): Int {
return if (isPositionHeader(position)) {
TYPE_SELECT_IMAGE
} else {
TYPE_ITEMS
}
}
fun isPositionHeader(position: Int): Boolean {
return position == 0
}
override fun getItemCount(): Int {
return super.getItemCount() + 1
}
}
and this is two view holder one for header view another for displaying model
class SelectImageViewHolder(val view: View) : RecyclerView.ViewHolder(view) , AnkoLogger{
fun bindModel() {
view.setOnClickListener {
info {
"clicked !"
}
}
}
}
class UserImageViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bindImages(userImage: UserImage) {
val imageView = view.findViewById<ImageView>(R.id.image_user_post)
Glide.with(view.context).load(userImage.imageUrl).into(imageView)
}
}
my goal is to keep the SelectImageViewHolder in position 0 always
FirebaseUI maintainer here. onBindViewHolder(RecyclerView.ViewHolder, Int, Model) makes a getItem(position) call internally to seamlessly populate your model. Since you're manually injecting an item, the RecyclerView is going to think there are items in the database and will blow up as you saw. To fix that, you'll want to override the native onBindViewHolder(RecyclerView.ViewHolder, Int) method without calling super:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder) {
is SelectImageViewHolder -> holder.bindModel()
is UserImageViewHolder -> {
// Manually get the model item
holder.bindImages(getItem(position - 1))
}
}
}