Terry
Terry

Reputation: 117

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

Upvotes: 2

Views: 7343

Answers (2)

Gil G
Gil G

Reputation: 1869

I found that the easiest way to achieve that is by associating a model with a ViewHolder what do I mean by that.

My approach works by removing the decision of when to populate each ViewHolder from the adapter into our element list.

Let's say that we want to show a list of names and every 5th item we want to show an ad (just an example it could be anything)

val elements = listOf("Name #1", "Name #2", "Name #3", "Name #4", AdModel("our ad"), "Name #5")

Now inside the adapter, we first say that we extend the generic ViewHolder class RecyclerView.Adapter<RecyclerView.ViewHolder>().

After that, we want to override 3 methods.

getItemViewType

Here we will decide what layout file is associated with each model we'll do that using instance checks and return the layout file that we want to show based on the type.

override fun getItemViewType(position: Int): Int {
    val element = elements[position] // assuming your list is called "elements"

    return when (element) {
        is String -> R.layout.name_layout_file

        is AdModel -> R.layout.ad_layout_file

        else -> throw IllegalArgumentException("Unsupported type") // in case populated with a model we don't know how to display.
    }
}

onCreateViewHolder

You will need to create a ViewHolder class for every type you want to show.

 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val inflater = LayoutInflater.from(parent.context)

    return when (viewType) {
        R.layout.name_layout_file -> NameViewHolder(inflater.inflate(viewType, parent, false))

        R.layout.ad_layout_file -> AdViewHolder(inflater.inflate(viewType, parent, false))

        else -> throw IllegalArgumentException("Unsupported layout") // in case populated with a model we don't know how to display.
    }
}

onBindViewHolder

Here we can easily cast our ViewHolder

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val element = elements[position] // assuming your list is called "elements"

    when (holder) {
        is NameViewHolder -> {
            val name = element as String
            // bind NameViewHolder
        }

        is AdViewHolder -> {
            val adModel = element as AdModel
            // bind AdViewHolder
        }

    }
}

Your full adapter should look like this:

class SampleAdapter(val elements: List<Any>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)

        return when (viewType) {
            R.layout.name_layout_file -> NameViewHolder(inflater.inflate(viewType, parent, false))

            R.layout.ad_layout_file -> AdViewHolder(inflater.inflate(viewType, parent, false))

            else -> throw IllegalArgumentException("Unsupported layout") // in case populated with a model we don't know how to display.
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val element = elements[position] // assuming your list is called "elements"

        when (holder) {
            is NameViewHolder -> {
                val name = element as String
                // bind NameViewHolder
            }

            is AdViewHolder -> {
                val adModel = element as AdModel
                // bind AdViewHolder
            }

        }
    }

    override fun getItemViewType(position: Int): Int {
        val element = elements[position] // assuming your list is called "elements"

        return when (element) {
            is String -> R.layout.name_layout_file

            is AdModel -> R.layout.ad_layout_file

            else -> throw IllegalArgumentException("Unsupported type") // in case populated with a model we don't know how to display.
        }
    }
}

When you load the adapter you load it with the multi-type list and everything works.

The biggest advantage is that you're not limited to specific types and it is very easy to add more view types.

But, if you want to save all of that work I wrote a library that generates single/multi-type adapters in compile-time using annotation processing.

The library generates everything you're going to need including findViewById and leaves you with the bare minimum code needed to write in order to implement the adapter.

You can check it out here: Gencycler

Upvotes: 4

M&#39;aiq the Coder
M&#39;aiq the Coder

Reputation: 812

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.

Upvotes: 2

Related Questions