I'm trying to fetch multiple documents from my Firestore collection so I can populate my RecyclerView. However, I'm getting a mismatch error when I try to hook my categories ArrayList to the QuerySnapshot, it says it's looking for kotlin.collections.ArrayList<Category> but it found Category?. What can I do to make my RecyclerView populate my category collection in Firestore? Do I need to rewrite my val categories = ArrayList<Category>()? Thank you!
Category Collection
Category.kt
data class Category(var category: String?, val categoryImage: String?) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(category)
parcel.writeString(categoryImage)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Category> {
override fun createFromParcel(parcel: Parcel): Category {
return Category(parcel)
}
override fun newArray(size: Int): Array<Category?> {
return arrayOfNulls(size)
}
}
}
CategoryAdapter.kt
class CategoryAdapter(val category: ArrayList<Category>) : RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
var selectedCategory = Category("", "")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindCategory(category[position])
holder.itemView.setOnClickListener { v ->
val context: Context = v.context
val intent = Intent(context, CategoryServiceActivity::class.java)
selectedCategory.category = category[position].category
intent.putExtra("category", selectedCategory)
context.startActivity(intent)
}
}
override fun getItemCount(): Int {
return category.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.categoryrecyclyerview, parent, false)
return ViewHolder(view)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val categoryName = itemView.findViewById<TextView>(R.id.categoryJobNameTextView)
val categoryImage = itemView.findViewById<ImageView>(R.id.categoryImageView)
fun bindCategory(category: Category) {
categoryName?.text = category.category
Picasso.get().load(category.categoryImage).into(categoryImage)
}
}
}
HomeFragment.kt
val categories = ArrayList<Category>()
val categoriesDatabaseRef = FirebaseFirestore.getInstance().collection(REF_JOB_CATEGORIES)
categoriesDatabaseRef.orderBy("category").get().addOnSuccessListener(object: OnSuccessListener<QuerySnapshot> {
override fun onSuccess(p0: QuerySnapshot?) {
if (p0 != null) {
for (querySnapshot in p0.documents) {
categories = querySnapshot.toObject(Category::class.java)
}
}
}
})
As I see in your code, the categories object is an ArrayList. So when you're using the following line of code:
categories = querySnapshot.toObject(Category::class.java)
It means that you're trying to convert the querySnapshot object, which is actually a DocumentSnapshot object, into an object of type Category, which works perfectly fine. However, you cannot assign that value to the categories object because between the ArrayList and Category classes, there is no inheritance relationship, hence that error.
So there are two ways in which you can solve this. The first solution would be to add an object of type Category, at each iteration of the for loop to the list:
categoriesDatabaseRef.orderBy("category").get().addOnSuccessListener(object: OnSuccessListener<QuerySnapshot> {
override fun onSuccess(p0: QuerySnapshot?) {
if (p0 != null) {
for (querySnapshot in p0.documents) {
val category = querySnapshot.toObject(Category::class.java)
categories.add(category) //Add the object to the list.
}
}
}
})
The second solution, which is even simpler in my opinion, would be to convert the querySnapshot directly into a list, by removing the for loop like this:
categoriesDatabaseRef.orderBy("category").get().addOnSuccessListener(object: OnSuccessListener<QuerySnapshot> {
override fun onSuccess(p0: QuerySnapshot?) {
if (p0 != null) {
categories = p0.toObjects(Category::class.java)
}
}
})
Please see that I have used toObjects(Class clazz) method which:
Returns the contents of the documents in the QuerySnapshot, converted to the provided class, as a list.
So it's toObjects, see the s? And not toObject.
Besides that, don't forget that Firebase API is asynchronous. So you cannot simply use the value of categories outside the onSuccess() method. If you're new to asynchronous programming, I recommend you read the following resource:
How to read data from Cloud Firestore using get()?
Related
Currently, I am making a task in Android that changes the unit value of the list according to the toggle button and shows the list with the changed value.
I am observing the list using a ViewModel and LiveData.
So i use toList() to return a new list and overwrite the old list to observe the values.
However, the screen is not updated even though it has returned a new list.
I've tried debugging and I'm getting some incomprehensible results.
Obviously, the address values of the old list and the new list are different, but even the unit of the old list has changed.
What happened?
Even if the addresses of Lists are different, do the values of the old list and the new list change at the same time because the properties refer to the same place?
I'll show you the minimal code.
Fragment
// Change Unit
toggleButton.addOnButtonCheckedListener { _, checkedId, isChecked ->
if(isChecked) {
when(checkedId) {
R.id.kg -> vm.changeUnit("kg")
R.id.lb -> vm.changeUnit("lbs")
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.items.observe(viewLifecycleOwner) { newList ->
adapter.submitList(newList)
}
}
WorkoutSetInfo
#Entity(
foreignKeys = [
ForeignKey(
entity = Workout::class,
parentColumns = arrayOf("workoutId"),
childColumns = arrayOf("parentWorkoutId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class WorkoutSetInfo(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
val set: Int,
var weight: String = "",
var reps: String = "",
var unit: String = "kg",
val parentWorkoutId: Long = 0
)
Adapter
class DetailAdapter
: ListAdapter<WorkoutSetInfo, DetailAdapter.ViewHolder>(DetailDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemRoutineDetailBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(currentList[position])
}
inner class ViewHolder(val binding: ItemRoutineDetailBinding) : RecyclerView.ViewHolder(binding.root) {
private var weightTextWatcher: TextWatcher? = null
private var repTextWatcher: TextWatcher? = null
fun bind(item: WorkoutSetInfo) {
binding.set.text = item.set.toString()
binding.weight.removeTextChangedListener(weightTextWatcher)
binding.unit.text = item.unit
binding.rep.removeTextChangedListener(repTextWatcher)
weightTextWatcher = object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
override fun afterTextChanged(w: Editable?) {
if(!binding.weight.hasFocus())
return
item.weight = w.toString()
}
}
repTextWatcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(r: Editable?) {
if(!binding.rep.hasFocus())
return
item.reps = r.toString()
}
}
binding.apply {
weight.setTextIfDifferent(item.weight)
weight.addTextChangedListener(weightTextWatcher)
rep.setTextIfDifferent(item.reps)
rep.addTextChangedListener(repTextWatcher)
}
}
}
}
DiffUtil*
class DetailDiffCallback : DiffUtil.ItemCallback<WorkoutSetInfo>() {
override fun areItemsTheSame(
oldItem: WorkoutSetInfo,
newItem: WorkoutSetInfo
): Boolean {
return (oldItem.id == newItem.id)
}
override fun areContentsTheSame(
oldItem: WorkoutSetInfo,
newItem: WorkoutSetInfo
): Boolean {
return oldItem == newItem
}
}
ViewModel
class DetailViewModel(application: Application, title: String) : ViewModel() {
private val workoutDao = DetailDatabase.getDatabase(application)!!.workoutDao()
private val repository: WorkoutRepository = WorkoutRepository(workoutDao, title)
private val _items: MutableLiveData<List<WorkoutSetInfo>> = MutableLiveData()
val items = _items
fun changeUnit(unit: String) {
repository.changeUnit(unit)
_items.postValue(repository.getList())
}
fun addSet() {
viewModelScope.launch(Dispatchers.IO){
repository.add()
_items.postValue(repository.getList())
}
}
fun deleteSet() {
repository.delete()
_items.postValue(repository.getList())
}
fun save() {
viewModelScope.launch(Dispatchers.IO) {
repository.save()
}
}
}
Repository
class WorkoutRepository(private val workoutDao : WorkoutDao, title: String) {
private val workout = Workout(title = title)
private val setInfoList = ArrayList<WorkoutSetInfo>()
fun changeUnit(unit: String) {
setInfoList.map { setInfo ->
setInfo.unit = unit
}
}
fun add() {
val item = WorkoutSetInfo(set = setInfoList.size + 1)
setInfoList.add(item)
}
fun delete() {
if(setInfoList.size != 0)
setInfoList.removeLast()
return
}
fun save() {
val workoutId = workoutDao.insertWorkout(workout)
val newWorkoutSetInfoList = setInfoList.map { setInfo ->
setInfo.copy(parentWorkoutId = workoutId)
}
workoutDao.insertSetInfoList(newWorkoutSetInfoList)
}
fun getList() : List<WorkoutSetInfo> = setInfoList.toList()
}
You'd need to post your observer code for any help with why it's not updating.
As for the weird behaviour, setInfoList contains a few WorkoutSetInfo objects, right? Let's call them A, B and C. When you call setInfoList.toList() you're creating a new container, which holds the same references to objects A, B and C. Because it's a separate list, you can add and remove items without affecting the original list, but any changes to the objects that both share will be reflected in both lists - because they're both looking at the same thing.
So when you do setInfoList.map { setInfo -> setInfo.unit = unit } (which should be forEach really, map creates a new list you're discarding) you're modifying A, B and C. So every list you've made that contains those objects will see those changes, including your old list.
Basically if you want each list to be independent, when you modify the list you need to create new instances of the items, which means copying your WorkoutSetInfo objects to create new ones, instead of updating the current ones. If it's a data class then you can do that fairly easily (so long as you don't have nested objects that need copying themselves):
// var so we can replace it with a new list
private var setInfoList = listOf<WorkoutSetInfo>()
fun changeUnit(unit: String) {
// create a new list, copying each item with a change to the unit property
setInfoList = setInfoList.map { setInfo ->
setInfo.copy(unit = unit)
}
}
You don't need to do toList() on getList anymore, since you're just passing the current version of the list, and that list will never change (because you'll just create a new one). Meaning you don't need that function, you can just make setInfoList public - and because I changed it to listOf which creates an immutable List, it's safe to pass around because it can't be modified.
The WorkoutSetInfo objects inside that list could still be modified externally though (e.g. by changing one of the items' unit value), so instead of making a new copy when you call changeUnit, you might want to do it when you call getList instead:
class WorkoutRepository(private val workoutDao : WorkoutDao, title: String) {
private val workout = Workout(title = title)
private val setInfoList = ArrayList<WorkoutSetInfo>()
// store the current unit here
private var currentUnit = "kg"
fun changeUnit(unit: String) {
currentUnit = unit
}
// return new List
fun getList() : List<WorkoutSetInfo> = setInfoList.map { it.copy(unit = currentUnit) }
}
Now everything that calls getList gets a unique list with unique objects, so they're all separate from each other. And if you don't actually need to store the current unit value, you could pass it in to getList instead of having a changeUnit function:
fun getList(unit: String) = setInfoList.map { it.copy(unit = unit) }
I have an app which uses Room Database to show data in recycleview. It works fine when i load data seperately from different tables. But i want to show data from both tables in a single recycleview with multiple viewtypes, i know how to combine tables in room but it's not working. I get empty cards in recycleview when i load the data. Here is what i have tried so far.
My Adapter Class
class CategoriesAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val TYPE_CATEGORIES = 0
private const val TYPE_ARTICLES = 1
}
private val items: MutableList<Any> by lazy {
ArrayList<Any>()
}
fun setItems(list: List<Any>) {
items.addAll(list)
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
return if (items[position] is Categories) TYPE_CATEGORIES else TYPE_ARTICLES
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_CATEGORIES -> CategoriesViewHolder.create(viewGroup)
else -> ArticlesViewHolder.create(viewGroup)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CategoriesViewHolder -> {
if (items[position] is Categories)
holder.bind(items[position] as Categories)
}
is ArticlesViewHolder -> {
if (items[position] is Articles)
holder.bind(items[position] as Articles)
}
}
}
override fun getItemCount(): Int {
return items.size
}
}
class CategoriesViewHolder (parent: View) : RecyclerView.ViewHolder(parent) {
val textView: TextView = parent.findViewById(R.id.categories_textView)
fun bind(category: Categories) {
textView.text = category.categoryName
}
companion object {
fun create(parent: ViewGroup): CategoriesViewHolder {
return CategoriesViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.categories_item_layout, parent, false))
}
}
}
class ArticlesViewHolder (parent: View) : RecyclerView.ViewHolder(parent) {
val textView: TextView = parent.findViewById(R.id.titleText)
fun bind(articles : Articles) {
textView.text = articles.articleName
}
companion object {
fun create(parent: ViewGroup): ArticlesViewHolder {
return ArticlesViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.article_item_layout, parent, false))
}
}
}
this is how i set data from my activity
val db = AppDatabase.getDatabase(applicationContext)
dao = db.articleDao()
val recyclerView = findViewById<RecyclerView>(R.id.categories_recycle_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = CategoriesAdapter()
adapter.setItems(dao.getAllArticlesAndCategories())
Can anyone help.
P.s i'm new to kotlin
Instead of
adapter.setItems(dao.getAllArticlesAndCategories())
Use live data observer to avoid processing on main thread and debug in observe function of live data to confirm you are receiving correct data from DB.
calling code one line of code is missing
val db = AppDatabase.getDatabase(applicationContext)
dao = db.articleDao()
val recyclerView = findViewById<RecyclerView>(R.id.categories_recycle_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = CategoriesAdapter()
adapter.setItems(dao.getAllArticlesAndCategories())
it should be:
val db = AppDatabase.getDatabase(applicationContext)
dao = db.articleDao()
val recyclerView = findViewById<RecyclerView>(R.id.categories_recycle_view)
recyclerView.layoutManager = LinearLayoutManager(this)
adapter=CategoriesAdapter()
adapter.setItems(dao.getAllArticlesAndCategories())
recyclerView.adapter = adapter
I would like to thank for the question and the code
I am trying to use Groupie to create a recyclerview with HeaderItems. I have Group of Data like this
class Group(
val id: String = generateId(),
val name: String? = null,
val entries: List<Entry>? = null
) : Item(), Parcelable {
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.apply {
itemView.tvGroupName.text = name
}
}
override fun getLayout() = R.layout.group_single_item
constructor(source: Parcel) : this(
source.readString(),
source.readString(),
source.createTypedArrayList(Entry.CREATOR)
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
writeString(id)
writeString(name)
writeTypedList(entries)
}
companion object {
private fun generateId(): String {
return UUID.randomUUID().toString()
}
#JvmField
val CREATOR: Parcelable.Creator<Group> = object : Parcelable.Creator<Group> {
override fun createFromParcel(source: Parcel): Group = Group(source)
override fun newArray(size: Int): Array<Group?> = arrayOfNulls(size)
}
}
}
Every group has a list of entries
data class Entry(val id: Long=0, val name: String) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readLong(),
parcel.readString()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeString(name)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Entry> {
override fun createFromParcel(parcel: Parcel): Entry {
return Entry(parcel)
}
override fun newArray(size: Int): Array<Entry?> {
return arrayOfNulls(size)
}
}
}
So I am trying to show a list of Groups along with their respective Entries. So I will be showing a Group with its name and the list of entries. So I thought of using Groupie for this one.
This is what I have been trying
val linearLayoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
val groups = intent.getParcelableArrayListExtra<Group>("groups")
val groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
val section = Section(Group())
section.setHeader(Group())
section.addAll(groups)
this.add(section)
}
recyclerViewGroups.apply {
layoutManager = linearLayoutManager
adapter = groupAdapter
}
But I am not quite sure, how to add the Group along with its Entries. Any help would be appreciated. Thanks
First you need to create item classes for your groups (possibly header and entry).
Follow instructions in this section.
E.g. those could be:
class HeaderItem(private val groupName: String) : Item() {
//... to be implemented
}
and
class EntryItem(private val entryName: String) : Item() {
//... to be implemented
}
and then use them in your adapter (needs to be tested, I'm writing this off the top of my head):
val groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
groups.forEach { group ->
val section = Section()
section.setHeader(HeaderItem(group.name))
section.addAll(group.entries.map{ it -> EntryItem(it.name) })
this.add(section)
}
}
I am doing a school project.
I have a list with Doses, so I need to fetch data en set text one by one.
Right now I'm getting:
kotlin.UninitializedPropertyAccessException: lateinit property medicine has not been initialized.
So I need to wait till the first item is fetched and set before continuing to next item.
can you help me?
class ClientDoseListAdapter(private val doses: List<Dose>) : RecyclerView.Adapter<ClientDoseListAdapter.ViewHolder>() {
private lateinit var medicine : Medicine
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.client_dose_listitem, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = doses[position]
runBlocking {
displayMedicine(item.medicine)
}
holder.med_name.text = medicine.name
holder.dose_amount.text = item.amount.toString()
}
private suspend fun displayMedicine(id: Int) {
fetchMedicine(id)
}
override fun getItemCount(): Int = doses.size
inner class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
LayoutContainer
private fun fetchMedicine(id: Int) {
service.getMedicine(id, "Bearer ${ClienOverzichtFragment.auth}")
.enqueue(object : Callback<List<Medicine>> {
override fun onResponse(call: Call<List<Medicine>>, response: Response<List<Medicine>>) {
if (response.code() == 200) {
val temp = response.body()!!
medicine = temp[0]
Log.v("SHIT", medicine.name)
} else {
Log.v("SHIT", response.code().toString())
//TODO
}
}
override fun onFailure(call: Call<List<Medicine>>, t: Throwable) {
Log.v("SHIT", "FAILED : "+t.message)
}
})
}
}
Move your service call out of the Recycler (best into a ViewModel, but can call from Activity or using any other pattern - the main thing, shouldn't be part of the Recycler) and pass the data, when it's received, into the Recycler.
Your ClientDoseListAdapter to accept medicine:
class ClientDoseListAdapter(private val doses: List<Dose>, private val medicine: Medicine)
In your activity, initiate and a call for medicine and observe it - when the data arrives, pass it to the adapter. Assuming you use a view model your code in Activity would look something like this:
viewModel.getMedicine().observe(
this,
Observer<Medicine> { medicine ->
//assuming doses come from somewhere else
adapter = ClientDoseListAdapter(doses, medicine, this)
clientDoseRecyclerView.adapter = adapter
}
)
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.
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)
}
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)
}
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
One "terrible" way of doing it is to simply have 1 constructor taking an ArrayList of Objects and perform an instanceof on the objects.
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.
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>
...
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" -> {}
}
}