kotlinandroid-recyclerviewmultiview

kotlin recyclerview with 2 view types


after every 5th item, i want a button view to load.

    override fun getItemViewType(position: Int): Int {

    if (position % 5 == 0 && position != 0) {
        return R.layout.button10th
    } else {
        return R.layout.checkitout
    }
}

when i run it, this is taking away my 5th item. how can i achieve this without taking any of my array items? do i need to fix my getItemCount?

   override fun getItemCount(): Int {

    return lists.size
}

i want it look like below. 1. John 2. Mike 3. Chris 4. Jane 5. Sue 6. CLickable BUTTON


Solution

  • After some time working with multiple view RecyclerViews, here is my best way to do it (that prevents bugs and has decent implementation):

    Create two viewHolder classes for your two item types, each having a bind() function inside:

    class NameViewHolder(itemView: View) : 
         RecyclerView.ViewHolder(itemView) {
    
         fun bind(cell: Cell) {
              //Do your bindViewHolder logic in here
         }
    
    }
    

    Since we have multiple view types, thus multiple viewHolders, we need to create a plain java object that holds the common information. I called mine Cell(). Call it whatever suits your needs. We'll get to that.

    So now you have your two viewHolder classes: NameViewHolder() and ButtonViewHolder().

    Now lets create our Cell class and objects:

    open class Cell {
        fun identifier() = this::class.java.name.hashCode()
    }
    
    class CellName(
        val name: String
    ) : Cell()
    
    class CellButton(
        val buttonText: String
    ) : Cell()
    

    Let me explain: So I created a global Cell() object that has an identifier function inside that gives me a class hash. This function will serve us later to get our view type. In my RecyclerView I don't use Int or other things to identify my view types, but the hash of my object class itself. The hash is a unique string for every class. So if my adapter, in its list of items, stumbles upon a object that is CellName(), the recyclerView gets its hash using my identifier() function and realises that the view type for this is Name, not a Button (from your example above). The other classes extend the global Cell() class and have their custom individual logic. Give them whatever parameters you like or need.

    Now inside our adapter we will add our list of Cells as a parameter like this:

    class MyAdapter(
       var items: ArrayList<Cell> = ArrayList()
    ): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    

    Make sure you implement the RecyclerView.Adapter exactly like above, otherwise the multiple view will not work.

    Now the getItemViewType override method that chooses your viewTypes will look like this:

    override fun getItemViewType(position: Int) = items[position].identifier()
    

    As you can see, we use the identifier() function I previously talked about, here, to let the adapter know what view type to choose based on the Cell() class hash.

    Now the onCreateViewHolder where your views get inflated:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
         RecyclerView.ViewHolder {
            return when (viewType) {
               CellName::class.java.name.hashCode() -> 
                    NameViewHolder(parent.inflate(R.layout.your_name_view)
               CellButton::class.java.name.hashCode() -> 
                    ButtonViewHolder(parent.inflate(R.layout.your_button_view)
            }
         }
    

    Now, when the adapter finds a Name view type, it inflates the NameViewHolder with the desired layout, and same for the Button with ButtonViewHolder. Next, onBindViewHoder:

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            when (val item = items[holder.adapterPosition]) {
                is CellName -> (holder as NameViewHolder).bind(item)
                is CellButton -> (holder as ButtonViewHolder).bind(item)
             }}
    

    Basically for each type of cell class, you access the bind functions from your viewHolder classes.

    That's about it with the recyclerView adapter. This is the whole file so far(next we will move on to creating your list of cells):

    class MyAdapter(
        private var items: ArrayList<Cell> = ArrayList()
    ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    
        override fun getItemCount(): Int  = items.size
        override fun getItemViewType(position: Int) = items[position].identifier()
    
        @Suppress("HasPlatformType")
        fun ViewGroup.inflate(@LayoutRes resId: Int) = LayoutInflater.from(this.context)
            .inflate(resId, this, false)
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
                RecyclerView.ViewHolder {
            return when (viewType) {
                CellName::class.java.name.hashCode() ->
                    NameViewHolder(parent.inflate(R.layout.your_name_view))
    
                CellButton::class.java.name.hashCode() ->
                    ButtonViewHolder(parent.inflate(R.layout.your_button_view))
    
            }
        }
    
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            when (val item = items[holder.adapterPosition]) {
                is CellName -> (holder as NameViewHolder)
                    .bind(item)
                is CellButton -> (holder as ButtonViewHolder)
                    .bind(item)
            }
        }
    }
    
    class NameViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(cell: Cell) {
            //Do your bindViewHolder logic in here
        }
    }
    
    class ButtonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(cell: Cell) {
            //Do your bindViewHolder logic in here
        }
    }
    
    open class Cell {
        fun identifier() = this::class.java.name.hashCode()
    }
    
    class CellName(
        val name: String
    ) : Cell()
    
    class CellButton(
        val buttonText: String
    ) : Cell()
    

    Make sure you scroll the above text snipped as it's very large to fit. Also, the

    @Suppress("HasPlatformType")
    fun ViewGroup.inflate(@LayoutRes resId: Int) = LayoutInflater.from(this.context)
        .inflate(resId, this, false)
    

    is a Kotlin Extension Function that makes layout inflation faster. Use as is.

    Next, your cell list creation:

    fun createCells(
        // Whatever params.
    ): ArrayList(Cell){
        val cellList = ArrayList<Cell>()
        var firstCell = CellName("Christina")
        val secondCell = CellName("Mary")
        val thirdCell = CellButton("View More!")
        cellList.add(firstCell)
        cellList.add(secondCell)
        cellList.add(thirdCell)
        // Note that here you can do whatever you want, use forEach, for, anything to 
        create a full list of Cells with your desired information.
        return cellList
    }
    

    Use this function in any activity/fragment where you have that adapter.

    val myAdapter = MyAdapter(createCells())
    recyclerView.adapter = myAdapter
    

    That's it. Feel free to customise your cells and view types as much as you want. Remember that for each new view type you need in your recyclerView, you have to create a cell class and a view holder class for it. This is my full tutorial on multiple view types. And to answer your explicit question of how to do this:

    if (position % 5 == 0 && position != 0) {
        return R.layout.button10th
    } else {
        return R.layout.checkitout
    } //Your code from your question here
    

    ... you don't. You don't to this logic anymore inside the recyclerView. The adapter must only receive a list of Cells and nothing more. This just prevents a full load of bugs and makes your code cleaner and easier to read. You just create your cells in the createCells() function like:

    something.forEachIndexed {index, item ->
    if(index % 5 == 0 && position != 0)
        cellList.add(CellButton("button_stuff"))
    else
        cellList.add(CellName("blabla"))
    }
    

    and you won't have to worry about item numbers and positions anymore. Just use the createCells() function to do your entire logic but return a single full list and the adapter knows what to do.

    And if you're wondering what to do with the bind() function inside your bindViewHolder, you can do whatever you would do in that code block normally, inside your adapter, like setting texts in textViews and buttons, setting images with Glide or by resource linking, create your button functionality. I'll actually explain how to do your button functionality and bind():

    Remember how we already set our desired information in our cell objects.

    class ButtonViewHolder(itemView: View) : 
         RecyclerView.ViewHolder(itemView) {
    
         fun bind(cell: Cell) {
          //Do your bindViewHolder logic in here
         }
    }
    

    Here you have access to that object, so let's create a button and add a callback for it. In order to do so, you have to update your bind function with a callback variable.

    fun bind(cell: Cell, buttonCallback: (() -> Unit)) {
        //Do your bindViewHolder logic in here
    
        itemView.yourXMLTitleWhatever.text = cell.titleText //(if you have a title for example)
    
        itemView.yourXMLButton.setOnClickListener {
             buttonCallback.invoke()
        }
    }
    

    The invoke function tells your callback that the button has been pressed. Now in order to make the callback work, we need to declare the callback variable as public in your adapter. So inside your adapter add this:

    class MyAdapter(
        private var items: ArrayList<Cell> = ArrayList()
    ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    
        var buttonCallback: (() -> Unit) = {}
    
    (...)
    

    And also don't forget to add the var as parameter to your bind call here in the adapter:

    So instead of:

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            when (val item = items[holder.adapterPosition]) {
                is CellName -> (holder as NameViewHolder).bind(item)
                is CellButton -> (holder as ButtonViewHolder).bind(item)
             }}
    

    we will have:

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            when (val item = items[holder.adapterPosition]) {
                is CellName -> (holder as NameViewHolder).bind(item)
                is CellButton -> (holder as ButtonViewHolder).bind(item, buttonCallback)
             }}
    

    Basically this type of callback variable is a Kotlin shortcut for an interface that would do the same thing, you know, in Java, when you create an interface to handle your adapter clicks.

    But we are not done yet, make sure your adapter callback variable is not private, and in your ACTIVITY, do this to access it:

    myAdapter.onButtonClick = { 
         //add your button click functionality here (like activity change or anything).
     }
    

    where myAdapter = MyAdapter(cellList)

    Hope I helped.