Method param require nothing instead of typed param

Viewed 46

There's superclass with typed parameter:

abstract inner class CalendarViewHolder<T : CalendarItem>(view: View) :
    RecyclerView.ViewHolder(view) {

    open fun bindData(data: T) {}
}

I expect that the method bindData will accept params which are objects of CalendarItem subclasses.

But in onBindViewHolder I got the following error:

Type mismatch.
Required:
Nothing
Found:
CalendarItem

Here's the implementation of the method:

override fun onBindViewHolder(holder: CalendarViewHolder<out CalendarItem>, position: Int) {
    val item = items[position]
    holder.bindData(item)

    if (item is DayItem) {
        if (holder is OnClickStrategy) {
            holder.onClickListener = {
                selectedDayOfYear = item.date?.get(Calendar.DAY_OF_YEAR) ?: 0
                notifyItemChanged(position)
                if (previousClickedPosition != -1) {
                    notifyItemChanged(previousClickedPosition)
                }
                previousClickedPosition = holder.adapterPosition
            }
        }
    }
}

Here's the full code of adapter:

class CalendarAdapter :
    RecyclerView.Adapter<CalendarAdapter.CalendarViewHolder<out CalendarItem>>() {

    private val items = mutableListOf<CalendarItem>()
    private var selectedDayOfYear = -1
    private var previousClickedPosition = -1

    abstract inner class CalendarViewHolder<T : CalendarItem>(view: View) :
        RecyclerView.ViewHolder(view) {

        open fun bindData(data: T) {}
    }

    abstract inner class CalendarDateViewHolder(view: View) : CalendarViewHolder<DayItem>(view) {

        protected lateinit var tvDate: AppCompatTextView
        protected lateinit var tvMonth: AppCompatTextView
        protected lateinit var tvDayDescription: AppCompatTextView
        protected lateinit var clRoot: ConstraintLayout

        @CallSuper
        override fun bindData(data: DayItem) {
            tvDate = itemView.findViewById(R.id.tv_day_number)
            tvMonth = itemView.findViewById(R.id.tv_month)
            tvDayDescription = itemView.findViewById(R.id.tv_day_description)
            clRoot = itemView.findViewById(R.id.cl_root)
        }
    }

    interface OnClickStrategy {
        var onClickListener: () -> Unit
    }

    inner class CalendarEmptyViewHolder(view: View) : CalendarViewHolder<EmptyDayItem>(view)

    inner class CalendarWithDateViewHolder(view: View) :
        CalendarDateViewHolder(view), OnClickStrategy {

        override lateinit var onClickListener: () -> Unit

        override fun bindData(data: DayItem) {
            super.bindData(data)

            tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()

            clRoot.setOnClickListener {
                onClickListener.invoke()
            }

            clRoot.background = ContextCompat.getDrawable(
                itemView.context,
                when (selectedDayOfYear) {
                    data.date?.get(Calendar.DAY_OF_YEAR) -> R.drawable.item_selected_background
                    else -> R.drawable.item_default_background
                }
            )
            tvDate.setTextColor(
                ContextCompat.getColor(
                    itemView.context,
                    if (selectedDayOfYear == data.date?.get(Calendar.DAY_OF_YEAR)) R.color.white
                    else R.color.black
                )
            )
        }
    }

    inner class CalendarTodayDateViewHolder(view: View) : CalendarDateViewHolder(view) {
        override fun bindData(data: DayItem) {
            super.bindData(data)
            tvDayDescription.text = "hoy"
            tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()
            clRoot.background = ContextCompat.getDrawable(
                itemView.context,
                R.drawable.item_today_background
            )
            val monthName = when (data.date?.get(Calendar.MONTH)) {
                0 -> "January"
                1 -> "February"
                else -> "March"
            }
            tvMonth.text = monthName
            tvDate.setTextColor(ContextCompat.getColor(itemView.context, R.color.white))
            tvDayDescription.setTextColor(ContextCompat.getColor(itemView.context, R.color.white))
            tvMonth.setTextColor(ContextCompat.getColor(itemView.context, R.color.white))
        }
    }

    inner class CalendarNewMonthDateViewHolder(view: View) : CalendarDateViewHolder(view),
        OnClickStrategy {

        override lateinit var onClickListener: () -> Unit

        override fun bindData(data: DayItem) {
            super.bindData(data)
            tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()
            val monthName = when (data.date?.get(Calendar.MONTH)) {
                0 -> "January"
                1 -> "February"
                else -> "March"
            }
            tvMonth.text = monthName
            clRoot.setOnClickListener {
                onClickListener.invoke()
            }
            tvDate.setTextColor(
                ContextCompat.getColor(
                    itemView.context,
                    if (selectedDayOfYear == data.date?.get(Calendar.DAY_OF_YEAR)) R.color.white
                    else R.color.black
                )
            )
            clRoot.background = ContextCompat.getDrawable(
                itemView.context,
                when (selectedDayOfYear) {
                    data.date?.get(Calendar.DAY_OF_YEAR) -> R.drawable.item_selected_background
                    else -> R.drawable.item_default_background
                }
            )
        }
    }

    inner class CalendarDisabledDateViewHolder(view: View) : CalendarDateViewHolder(view) {

        override fun bindData(data: DayItem) {
            super.bindData(data)
            tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()
            clRoot.background = ContextCompat.getDrawable(
                itemView.context,
                R.drawable.item_disabled_background
            )
            tvDate.setTextColor(ContextCompat.getColor(itemView.context, R.color.silver))
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ) = when (viewType) {
        CALENDAR_EMPTY_ITEM_TYPE -> onCreateCalendarEmptyViewHolder(parent)
        CALENDAR_TODAY_DATE_ITEM_TYPE -> onCreateCalendarTodayDateViewHolder(parent)
        CALENDAR_NEW_MONTH_DATE_ITEM_TYPE -> onCreateCalendarNewMonthDateViewHolder(parent)
        CALENDAR_WITH_DATE_ITEM_TYPE -> onCreateCalendarWithDateViewHolder(parent)
        else -> onCreateCalendarDisabledDateViewHolder(parent)
    }

    override fun getItemViewType(position: Int) = when (val item = items[position]) {
        EmptyDayItem -> CALENDAR_EMPTY_ITEM_TYPE
        is DayItem -> when {
            item.isToday() -> CALENDAR_TODAY_DATE_ITEM_TYPE
            item.isNewMonth() -> CALENDAR_NEW_MONTH_DATE_ITEM_TYPE
            item.isDateEnabled -> CALENDAR_WITH_DATE_ITEM_TYPE
            else -> CALENDAR_DISABLED_DATE_ITEM_TYPE
        }
        else -> UNKNOWN_ITEM_TYPE
    }

    private fun onCreateCalendarWithDateViewHolder(parent: ViewGroup) = CalendarWithDateViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
    )

    private fun onCreateCalendarEmptyViewHolder(parent: ViewGroup): CalendarViewHolder<EmptyDayItem> =
        CalendarEmptyViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_empty, parent, false)
        )

    private fun onCreateCalendarTodayDateViewHolder(parent: ViewGroup) =
        CalendarTodayDateViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
        )

    private fun onCreateCalendarNewMonthDateViewHolder(parent: ViewGroup) =
        CalendarNewMonthDateViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
        )

    private fun onCreateCalendarDisabledDateViewHolder(parent: ViewGroup) =
        CalendarDisabledDateViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
        )

    override fun onBindViewHolder(holder: CalendarViewHolder<out CalendarItem>, position: Int) {
        val item = items[position]
        holder.bindData(item)

        if (item is DayItem) {
            if (holder is OnClickStrategy) {
                holder.onClickListener = {
                    selectedDayOfYear = item.date?.get(Calendar.DAY_OF_YEAR) ?: 0
                    notifyItemChanged(position)
                    if (previousClickedPosition != -1) {
                        notifyItemChanged(previousClickedPosition)
                    }
                    previousClickedPosition = holder.adapterPosition
                }
            }
        }
    }

    override fun getItemCount() = items.size

    fun setItems(items: MutableList<DayItem>) {
        val localItems: MutableList<CalendarItem> = items.toMutableList()
        if (!items.isNullOrEmpty()) {
            val firstDayNumber = items[0].date?.get(Calendar.DAY_OF_WEEK) ?: 0
            for (i in 1 until firstDayNumber) {
                localItems.add(0, EmptyDayItem)
            }
            this.items.clear()
            this.items.addAll(localItems)
        }
    }

    companion object {
        private const val CALENDAR_EMPTY_ITEM_TYPE = 0
        private const val CALENDAR_TODAY_DATE_ITEM_TYPE = 1
        private const val CALENDAR_NEW_MONTH_DATE_ITEM_TYPE = 2
        private const val CALENDAR_WITH_DATE_ITEM_TYPE = 3
        private const val CALENDAR_DISABLED_DATE_ITEM_TYPE = 4
        private const val CALENDAR_HEADER_ITEM_TYPE = 5
        private const val UNKNOWN_ITEM_TYPE = -1
    }

}

sealed class CalendarItem

class DayItem(
    val date: Calendar? = null,
    val isDateEnabled: Boolean = true
): CalendarItem() {

    fun isToday() = if (date != null) isSameDay(date, Calendar.getInstance()) else false

    fun isNewMonth() = date?.get(Calendar.DAY_OF_MONTH) == 1

    private fun isSameDay(cal1: Calendar, cal2: Calendar) =
        cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
                cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
                cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
}

object EmptyDayItem : CalendarItem()

What's the reason of the problem?

1 Answers

You specified the type as out CalendarItem, but the bind function consumes the item of type T, so it is in an in position, not out. So by marking it as out at the class declaration, you have restricted it to Nothing wherever T appears as a function parameter type.

There's no way to make generics work for you here, as I'm assuming you're trying to get it to allow you to bind items without casting. onCreateViewHolder has your ViewHolder in an out position, which means your particular ViewHolder cannot have invariant or contravariant (in) type if you want to be able to return different types of your ViewHolder subclass from onCreateViewHolder.

It's just not logically possible for generics to safely allow you to pass these items to the appropriate types, and you will have to use casting.

I suggest you eliminate the generic type from your abstract ViewHolder, and make each subclass throw an error if it gets the wrong type. Then you will quickly get exceptions when you test your app if there are any type mismatches and you will know the problem lies in either getItemViewType or onCreateViewHolder, where you've hooked up your classes and types incorrectly.

I also suggest using View Binding, because it will make these classes much more streamlined. No lateinit view properties or findViewById calls.

For example:

abstract class CalendarViewHolder(view: View): RecyclerView.ViewHolder(view) {
    abstract fun bindData(data: CalendarItem): Any
    // Any return type here allows subclasses to use "= with(binding)"
    // to reduce nesting.
}

class CalendarDateViewHolder(private val binding: ItemCalendarDayBinding): MyViewHolder(binding.root) {
    override fun bind(item: CalendarItem) = with(binding) {
        item as? DayItem ?: error("Wrong item type.")
        // item is now smart cast as DayItem
        // bind the DayItem data to the views. 
        // can access the views directly by name in with block.
    }
}

private fun onCreateCalendarWithDateViewHolder(parent: ViewGroup) = CalendarWithDateViewHolder(
    ItemCalendarDayBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)

And I suggest also getting rid of all those onCreate functions and adding constructors that use the parent ViewGroup, just to streamline things, for example:

class CalendarDateViewHolder private constructor(private val binding: ItemCalendarDayBinding): MyViewHolder(binding.root) {
    constructor(parent: ViewGroup): this(ItemCalendarDayBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    //...
}
Related