BoyUnderTheMoon
BoyUnderTheMoon

Reputation: 771

Converting a generic RecyclerView Adapter to Kotlin

Currently, I have a list I want to populate, but the list items can have different data. I have two data classes and to accommodate this for the RecyclerView, I have two different ViewHolders extending a basic base ViewHolder. The different ViewHolders are required as different layouts are used for the different data classes.

I have converted the view holders to Kotlin, however I have an issue with the adapter.

The base ViewHolder in Kotlin:

abstract class BaseViewHolder<T> internal constructor(view: View) : RecyclerView.ViewHolder(view) {
    abstract fun bind(item: T)
}

A ViewHolder that implements the base ViewHolder in Kotlin:

class StandardViewHolder(view: View): BaseViewHolder<Standard>(view) {

    private val _eventView      : TextView = view.findViewById(R.id.eventTextView)
    private val _dateView       : TextView = view.findViewById(R.id.dateTextView)

    override fun bind(item: Standard) {
        _eventView.text     = item.event
        _dateView.text      = item.date.toString()
    }
}

In Java, I can create an adapter that uses these ViewHolders:

public class ListAdapter extends RecyclerView.Adapter<BaseViewHolder> {

    private List<Object> _items;
    private Context _context;

    public ListAdapter(List<Object> items, Context context){
        _items      = items;
        _context    = context;
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if(viewType == R.layout.item_standard){
            return new StandardViewHolder(LayoutInflater.from(_context).inflate(
                    R.layout.item_standard, parent, false));
        }

        return new AdvancedViewHolder(LayoutInflater.from(_context).inflate(
                R.layout.item_advanced, parent, false));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
        holder.bind(_items.get(position));
    }

    @Override
    public int getItemCount() {
        return _items.size();
    }

    @Override
    public int getItemViewType(int position) {
        if(_items.get(position) instanceof Standard){
            return R.layout.item_standard;
        }

        return R.layout.item_advanced;
    }
}

However, if I convert this to Kotlin, I get an error on the holder.bind, being:

Out-projected type 'BaseViewHolder<*>' prohibits the use of 'public abstract fun bind(item: T)'

class ListAdapter(private val _items: List<Any>, private val _context: Context) : RecyclerView.Adapter<BaseViewHolder<*>>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
        return if (viewType == R.layout.item_standard) {
            StandardViewHolder(LayoutInflater.from(_context).inflate(
                    R.layout.item_standard, parent, false))
        } else {
            AdvancedViewHolder(LayoutInflater.from(_context).inflate(
                    R.layout.item_advanced, parent, false))
        }
    }

    override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) = holder.bind(_items[position])

    override fun getItemCount() = _items.size

    override fun getItemViewType(position: Int): Int {
        return if (_items[position] is Standard) {
            R.layout.item_standard
        } else {
            R.layout.item_advanced
        }
    }
}

How do I use these generic ViewHolders in Kotlin?

Upvotes: 2

Views: 6095

Answers (3)

Arihant Jain
Arihant Jain

Reputation: 131

For Generic Recycler view use the following code:

Step 1: Extend Adapter with RecyclerView.Adapter() e.g.

class MyAdapter(private val recentList: ArrayList<CollectionModel>) :
        RecyclerView.Adapter<**RecyclerView.ViewHolder**>() {
}

Step 2: set View type for all views e.g.

override fun getItemViewType(position: Int): Int {
        return if (recentList[position].type == 0) CollectionModel.COLLECTION_HEADER else CollectionModel.COLLECTION_DETAIL
    }

Step 3: inside onCreateViewHolder define proper view and ViewHolder e.g.

if (viewType == CollectionModel.COLLECTION_HEADER) {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.signle_row_header, parent, false)
            return HeaderCollectionHolder(view)
        } else {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.single_row_collection_history, parent, false)
            return DetailHolder(view)
        }

Step 4: Inside onBindViewHolder bind data based on view type and type cast the view holder using as Keyword e.g.

val collectionModel = recentList[position]

        if (recentList[position].type == CollectionModel.COLLECTION_HEADER) {
            (holder as HeaderCollectionHolder).apply {
                header.text = collectionModel.date
            }
        } else {
            (holder as DetailHolder).apply {
                name.text = collectionModel.name
                phone.text = collectionModel.phone
                collectedAmount.text = collectionModel.lastCollectAmount.toInt().toString()
                balanceAmount.text = collectionModel.pendingBalance.toInt().toString()
                Utils.setImage(collectionModel.picUrl, imageView)
            }
        }

Step 5: Create two view holder class as below:

inner class DetailHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    var name: TextView = itemView.findViewById(R.id.name)
    var phone: TextView = itemView.findViewById(R.id.phone)
    var collectedAmount: TextView = itemView.findViewById(R.id.total_amount)
    var balanceAmount: TextView = item`enter code here`View.findViewById(R.id.balance_amount)
    var imageView: CircularImageView = itemView.findViewById(R.id.user_image)

}

inner class HeaderCollectionHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val header: TextView = itemView.findViewById(R.id.text)
}

Thats it.

Upvotes: 0

Sandip Savaliya
Sandip Savaliya

Reputation: 784

Nice one… Actually i had some rework on that and see what i’ve done.. I’ve made it the most dynamic adapter one can ever make.

https://medium.com/@sandipsavaliya/tired-with-adapters-4240e5f45c24

Upvotes: 0

Leo Aso
Leo Aso

Reputation: 12513

The problem is that, even though you know that the item being bound matches the type of the ViewHolder that was created for it, you can't prove it at compile-time, so the compiler yells at you. You can't call bind on BaseViewHolder<*> because then the argument would have to be of type * which can't happen. What you need there BaseViewHolder<Any> but you can't make the adapter RecyclerView.Adapter<BaseViewHolder<Any>> because that will break onCreateViewHolder. And I've tried BaseViewHolder<out Any>, that doesn't work either.

So here is what you do: Use BaseViewHolder<*>, but then inside onBindViewHolder cast it to BaseViewHolder<Any>. The compiler will complain "Hey! That's an unchecked cast! You shouldn't do that!", so tell it to shut up with @Suppress("UNCHECKED_CAST").

class ListAdapter(
        private val _items: List<Any>,
        private val _context: Context
) : RecyclerView.Adapter<BaseViewHolder<*>>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
        return if (viewType == R.layout.item_standard) {
            StandardViewHolder(LayoutInflater.from(_context).inflate(
                    R.layout.item_standard, parent, false))
        } else {
            AdvancedViewHolder(LayoutInflater.from(_context).inflate(
                    R.layout.item_advanced, parent, false))
        }
    }

    @Suppress("UNCHECKED_CAST")
    override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
        (holder as BaseViewHolder<Any>).bind(_items[position])
    }

    override fun getItemCount() = _items.size

    override fun getItemViewType(position: Int): Int {
        return if (_items[position] is Standard) {
            R.layout.item_standard
        } else {
            R.layout.item_advanced
        }
    }
}

Upvotes: 6

Related Questions