0

I want to have generic RecyclerView to be able to reuse it. In my case I have 2 models: CategoryImages and Category. While trying to add constructor() it brings the following errors. I know the second one is because it understands like both primary and secondary constructor are same.

Is it possible to do such kind of thing? If yes, then how? if no - thank you.

enter image description here

Here is CategoryImage:

class CategoryImage {
   @SerializedName("url")
   private var url: String? = null
       fun getUrl(): String? {
       return url
   }
}

And here is Category:

class Category {
    @SerializedName("_id")
    var id: String? = null
    @SerializedName("name")
    var name: String? = null
    @SerializedName("__v")
    var v: Int? = null
    @SerializedName("thumbnail")
    var thumbnail: String? = null
}

Here is the part of RecyclerViewAdapter's constructor:

class RecyclerViewAdapter(var arrayList: ArrayList<CategoryImage>?, var fragment: Int): RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
     constructor(arrayList: ArrayList<Category>, fragment: Int): this(arrayList, fragment)
}
azizbekian
  • 60,783
  • 13
  • 169
  • 249
Armen Hovhannisian
  • 932
  • 1
  • 11
  • 27
  • 2
    use a single super class for all model classes you are going to use inside your adapters. Then check the exact type in `onCreateViewHolder` – Droidman Jan 27 '18 at 00:23

6 Answers6

2

I want to have generic RecyclerView to be able to reuse it.

That's nice intention, then why you haven't made your adapter generic?

I think you can adopt the approach outlined by Arman Chatikyan in this blog post. After applying some Kotlin magic you'll only need following lines of code in order to setup your RecyclerView:

recyclerView.setUp(users, R.layout.item_layout, {
    nameText.text = it.name
    surNameText.text = it.surname
})

And if you need to handle clicks on RecyclerView items:

recyclerView.setUp(users, R.layout.item_layout, {
    nameText.text = it.name
    surNameText.text = it.surname
}, {
    toast("Clicked $name")
})

Now the adapter of the RecyclerView is generic and you are able to pass list of any models inside setup() method's first argument.


In this section I will copy-paste sources from the blog post, in order to be evade from external sources deprecation.

fun <ITEM> RecyclerView.setUp(items: List<ITEM>,
                              layoutResId: Int,
                              bindHolder: View.(ITEM) -> Unit,
                              itemClick: ITEM.() -> Unit = {},
                              manager: RecyclerView.LayoutManager = LinearLayoutManager(this.context)): Kadapter<ITEM> {
  return Kadapter(items, layoutResId, {
    bindHolder(it)
  }, {
    itemClick()
  }).apply {
    layoutManager = manager
    adapter = this
  }
}

class Kadapter<ITEM>(items: List<ITEM>,
                     layoutResId: Int,
                     private val bindHolder: View.(ITEM) -> Unit)
  : AbstractAdapter<ITEM>(items, layoutResId) {

  private var itemClick: ITEM.() -> Unit = {}

  constructor(items: List<ITEM>,
              layoutResId: Int,
              bindHolder: View.(ITEM) -> Unit,
              itemClick: ITEM.() -> Unit = {}) : this(items, layoutResId, bindHolder) {
    this.itemClick = itemClick
  }

  override fun onBindViewHolder(holder: Holder, position: Int) {
    holder.itemView.bindHolder(itemList[position])
  }

  override fun onItemClick(itemView: View, position: Int) {
    itemList[position].itemClick()
  }
}

abstract class AbstractAdapter<ITEM> constructor(
    protected var itemList: List<ITEM>,
    private val layoutResId: Int)
  : RecyclerView.Adapter<AbstractAdapter.Holder>() {

  override fun getItemCount() = itemList.size

  override fun onCreateViewHolder(parent: ViewGroup,
                                  viewType: Int): Holder {
    val view = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
    return Holder(view)
  }

  override fun onBindViewHolder(holder: Holder, position: Int) {
    val item = itemList[position]
    holder.itemView.bind(item)
  }

  protected abstract fun onItemClick(itemView: View, position: Int)

  protected open fun View.bind(item: ITEM) {
  }

  class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
azizbekian
  • 60,783
  • 13
  • 169
  • 249
1

Assuming CategoryImage means a Category with image.

You can express this relationship with inheritance:

open class Category(
        val name: String
)
class CategoryImage(
        name: String,
        val image: String
) : Category(name)

class RecyclerViewAdapter(
        val arr: List<Category>,
        val fragment: Int
) {
    fun bind(i: Int) {
        val item = arr[i]
        val name: String = item.name
        val image: String? = (item as? CategoryImage)?.image
    }
}

Another options it to have a common interface (which removes that ugly cast):

interface CategoryLike {
    val name: String
    val image: String?
}

class Category(
        override val name: String
) : CategoryLike {
    override val image: String? = null
}

class CategoryImage(
        override val name: String,
        override val image: String
) : CategoryLike

class RecyclerViewAdapter(private var arr: List<CategoryLike>, var fragment: Int) {
    fun bind(i: Int) {
        val item = arr[i]
        val name: String = item.name
        val image: String? = item.image
    }
}

In both cases the following works (just to see that it can be compiled):

fun testCreation() {
    val cats: List<Category> = listOf()
    val catImages: List<CategoryImage> = listOf()
    RecyclerViewAdapter(cats, 0)
    RecyclerViewAdapter(catImages, 0)
}

Tip: don't use ArrayList, List (listOf(...)) or MutableList (mutableListOf(...)) should be enough for all your needs.

Tip: try to use val as much as you can, it helps prevent mistakes.

Wish: Next time please also include some relevant parts of your code in a copy-able form (not screenshot), so we don't have to re-type it and have more context. See https://stackoverflow.com/help/mcve

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
  • Thank you for answering to my question with so much details. So I edited my post and added the copyable code parts. I will later on pay attention to all my mutable variables and will also change ArrayList to listof(). But firstly I would like to have 2 constructors which gets ArrayList s of different types. I understood the common interface solution. i will try it now it will let you know. – Armen Hovhannisian Jan 27 '18 at 10:03
  • Btw, the local variable types in `fun`s I only added for clarifying, those are inferred so you can omit them in the final code. – TWiStErRob Jan 27 '18 at 10:12
  • I made an interface like you showed above but while I am calling RecyclerViewAdapter from other class in both for Category and for CategoryImage it shows type mismatch required CategoryLike> So how to solve this problem ? – Armen Hovhannisian Jan 27 '18 at 14:00
  • That's because you're using `ArrayList` and not `List`. Use either: `ArrayList` or `List` in the constructor declaration of your adapter. See definition of those classes in Kotlin source code to see the difference. – TWiStErRob Jan 27 '18 at 16:05
  • Is it possible to share my full code with you if you do not mind ? – Armen Hovhannisian Jan 27 '18 at 16:10
0

One "terrible" way of doing it is to simply have 1 constructor taking an ArrayList of Objects and perform an instanceof on the objects.

ACVM
  • 1,497
  • 8
  • 14
0

Both methods have the same signature, because type parameters are not considered as different types (for Java Virtual Machine both are just ArrayLists). You also need to be aware of type erasure.

Przemysław Moskal
  • 3,551
  • 2
  • 12
  • 21
0

Check this repository https://github.com/shashank1800/RecyclerGenericAdapter

lateinit var adapter: RecyclerGenericAdapter<AdapterItemBinding, TestModel>
...

val clickListener = ArrayList<CallBackModel<AdapterItemBinding, TestModel>>()
clickListener.add(CallBackModel(R.id.show) { model, position, binding ->
    Toast.makeText(context, "Show button clicked at $position", Toast.LENGTH_SHORT)
        .show()
})

adapter = RecyclerGenericAdapter(
    R.layout.adapter_item, // layout for adapter
    BR.testModel,          // model variable name which is in xml
    clickListener          // adding click listeners is optional
)

binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)

adapter.submitList(viewModel.testModelList)

Recycler adapter item R.layout.adapter_item XML.


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="testModel"
            type="com.packagename.model.TestModel" />

    </data>
    ...
-1

VERY IMPORTANT NOTE: I'm using same layout for all my screens.

    //********Adapter*********
    
    // include a template parameter T which allows Any datatype
    class MainAdapter<T : Any>(var data: List<T>) : RecyclerView.Adapter<MainViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
            val view = parent.inflateLayout()
            return MainViewHolder(view)
        }
    
        override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
            val item = data[position]
            holder.bind(item)
        }
    
        override fun getItemCount(): Int = data.size
    
    
        class MainViewHolder(private val binding: MainItemsListBinding) :
            RecyclerView.ViewHolder(binding.root) {
            // do the same for for bind function on Viewholder
            fun <T : Any> bind(item: T) {
                // Extension function see code below
                binding.appInfo.mySpannedString(item)
            }
        }
    }
    
        //Cast Item to type
    fun <T : Any> TextView.mySpannedString(item: T) {
        when (item.javaClass.simpleName) {
            "AaProgram" -> {
                item as AaProgram
                this.text = buildSpannedString {
                    appInfo(item.numero, item.principio)
                }
            }
            "AppContent" -> {
                item as AppContent
                this.text = buildSpannedString {
                    appInfo(item.title, item.coment, item.footnote)
                }
            }
            "AutoDiagnostic" -> {
                item as AppContent
                this.text = buildSpannedString {
                    appInfo(item.title, item.coment, item.footnote)
                }
            }
            "GroupDirectory" -> {}
            "ReflexionsBook" -> {}
            "County" -> {}
            "States" -> {}
            "Towns" -> {}
        }
    }
Dav L
  • 1
  • 1
  • I guess that this should be added to your question and not posted as an answer. In fact this does not seem to answer your question. – homer Jan 15 '22 at 12:49