Can't access view in holder - android

I'm trying to create RecyclerView adapter and following this documentation.
private class Adapter(private val list: List<HashMap<String, Any>>, private val ctx: Context) : RecyclerView.Adapter<Adapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
var mTextView: TextView
return ViewHolder(ctx.UI {
relativeLayout {
mTextView = textView("1 + 1 = 2")
}
}.view)
}
override fun getItemCount() = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.mTextView.text = "1 + 1 = 3" // Should work?
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
}
How can I access mTextView in the onBindViewHolder?

If you're using kotlin extensions and have a text view with an id mTextView, then it should be:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemView.mTextView.text = "1 + 1 = 3" // Should work?
}
You can also define variables in your ViewHolder and use them directly, this is the best way in terms of performance as it won't force unecessary calls to findviewbyid:
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val myTextView = itemView.mTextView
}
And later access it like:
holder.myTextView.text = "some text"

I was also facing Unresolved reference when trying to access views in onBindViewHolder.
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
//Unresolved reference fooView
holder.itemView.fooView.text
}
In the end it turned out I was just missing the kotlinx import atop
import kotlinx.android.synthetic.main.myLayout.view.*
...And adding the extensions plugin in my app's build.gradle file.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions' //here
}
Adding the dependency and the import fixed it for me.

What you need to do here is to create a property for your TextView inside your ViewHolder. This isn't particularly easy with Anko, but here are some ways to do it.
1. Pass every View you want be able to reference from your ViewHolder as a separate constructor parameter.
For this solution, you have to modify your ViewHolder class like this, adding a property for the TextView:
class ViewHolder(view: View, val mTextView: TextView) : RecyclerView.ViewHolder(view)
You can initialize this property in this slightly convoluted way:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
lateinit var mTextView: TextView
val view = ctx.UI {
relativeLayout {
mTextView = textView("1 + 1 = 2")
}
}.view
return ViewHolder(view, mTextView)
}
While this doesn't look very nice, you're keeping the reference to your TextView from when you're initially creating it, so it's efficient.
2. Give your Anko created View instances IDs and look them up by their IDs later.
First, you have to create constants for your IDs - the ID can be any positive number, just make sure they're unique within the scope you're using them. One way is to place these inside a companion object, like so:
companion object {
private const val TEXT_VIEW_ID = 123 // Arbitrary positive number
}
Then, you need to give this ID to your TextView:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
return ViewHolder(ctx.UI {
relativeLayout {
textView("1 + 1 = 2") {
id = TEXT_VIEW_ID
}
}
}.view)
}
Finally, you can find your TextView again in your ViewHolder class with findViewById:
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val mTextView: TextView = view.findViewById(TEXT_VIEW_ID)
}
Of course this is going to be slower, because you're losing the TextView reference you already had when you're passing in the View to the ViewHolder, and then you're looking it up again.
Note that I stuck with mTextView for the answer so that it's easier to follow, but it's generally not recommended to use Hungarian notation or any other name prefixes in Kotlin.

Related

Use of binding breaks RecyclerView item layout

I am building an Android app with Kotlin and decided to replace the calls to findViewById and use binding. It all works fine but specifically, when I change an Adapter for a RecyclerView it breaks the item layout.
Original code with findViewById:
class WeightListAdapter(val weights: List<WeightWithPictures>, val onWeightItemClickListener: OnWeightItemClickListener) : RecyclerView.Adapter<WeightListAdapter.WeightHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_weight, parent, false)
return WeightHolder(view)
}
override fun onBindViewHolder(holder: WeightListAdapter.WeightHolder, position: Int) {
val weightWithPictures = weights[position]
holder.bind(weightWithPictures)
}
override fun getItemCount() = weights.size
inner class WeightHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private lateinit var weight: Weight
private val weightValueView: TextView = this.itemView.findViewById(R.id.weightValue)
private val weightDateView: TextView = this.itemView.findViewById(R.id.weightDate)
private val weightImageView: ImageView = this.itemView.findViewById(R.id.weightImage) as ImageView
And this is the layout:
But then whenever I use binding:
class WeightListAdapter(val weights: List<WeightWithPictures>, val onWeightItemClickListener: OnWeightItemClickListener) : RecyclerView.Adapter<WeightListAdapter.WeightHolder>() {
private var _binding: ListItemWeightBinding? = null
private val binding get() = _binding!!
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
_binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context))
val view = binding.root
return WeightHolder(view)
}
override fun onBindViewHolder(holder: WeightListAdapter.WeightHolder, position: Int) {
val weightWithPictures = weights[position]
holder.bind(weightWithPictures)
}
override fun getItemCount() = weights.size
inner class WeightHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private lateinit var weight: Weight
private val weightValueView: TextView = binding.weightValue
private val weightDateView: TextView = binding.weightDate
private val weightImageView: ImageView = binding.weightImage
The layout breaks:
Any ideas about what am I doing wrong here? Is it a bug?
P.S - For now, I am just adding the annotation to ignore bindings as documented here for the item view but I would really like to understand what's wrong.
Your binding needs to be inflated in the context of its parent so its root view's layout params will take effect:
binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context), parent, false)
I think it will also give you problems that you are creating a binding property for the Adapter if you try to use it long term. Each ViewHolder holds a distinct view with a distinct binding instance. It's working now because you use it only for the ViewHolder instantiation immediately after setting each instance. But if that's all your intent is, you should just pass the binding to the constructor of your ViewHolder and omit the adapter's property.
By the way, this is the sort of pattern I use for ViewHolders. Less code. Note, it doesn't have to be an inner class.
class WeightHolder(binding: ListItemWeightBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
fun bind(item: WeightWithPictures) {
with (binding) {
// set data for views here
}
}
}
I agree with #Tenfour04, using the same instance of binding is wrong but I believe the root cause of your issue is with the binding logic. with binding, the data is bound to bind with the view but not immediately. So your view gets inflated but since the binding happens at a later stage, scheduled to happen in near future, the item_view width is shrunk.
So try the following,
// oncreate view logic
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
val binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return WeightHolder(binding)
}
// onBindViewHolder logic remains the same
// this remains same as suggested by #Tenfour04 but a change in the bind function
class WeightHolder(binding: ListItemWeightBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
fun bind(item: WeightWithPictures) {
with (binding) {
// set data for views using databindig
customVariable = item
executePendingBindings() // this is important
}
}
}
// define the customvariable in your `item_list_view.xml`
<variable
name="customVariable"
type="packagename.WeightWithPictures" />
executePendingBindings() is the way we force the binding to happen right away and not to schedule it later
Edit:
This answer is from Databinding perspective and not ViewBinding

Why does my code not enter this adapter class?

I'm new to Kotlin and Android studio.
I'm trying to get a recycle view to work properly but I'm running into a problem when trying to use an adapter class.
I tried taking a look with breakpoints. but it seems to trigger on the very first line where the class gets defined. ( so class OrganisationsAdapter...etc ) and after that it skips the whole class, it doesnt even enter it.
I also don't get any exceptions.
Adapter Class
class OrganisationsAdapter(
private val myDataset: Array<String>
) : RecyclerView.Adapter<OrganisationsAdapter.MyViewHolder>() {
class MyViewHolder(
val textView: TextView
) : RecyclerView.ViewHolder(textView) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
var cell = LayoutInflater.from(parent.context).inflate(R.layout.example_item, parent, false)
val textView = cell.findViewById<TextView>(R.id.organisation_name)
return MyViewHolder(textView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.textView.text = myDataset[position]
}
override fun getItemCount() = myDataset.size
}
**The line I use to call the class **
recyclerViewOrganisationFragment.adapter = OrganisationsAdapter(Array(getTopics().size) { i -> getTopics()[i].name })
You need to pass the inflated layout to ViewHolder instead of TextView
ex:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
var cell = LayoutInflater.from(parent.context).inflate(R.layout.example_item, parent, false)
return MyViewHolder(cell)
}
class MyViewHolder(
val itemView: View
) : RecyclerView.ViewHolder(itemView) {
//TODO get textview here itemView.findViewByID
}
Update the viewholder accordingly with textview etc.,
Thank you Lakhwinder.
"You need to pass on ArrayList on the constructor of the adapter, you are passing a size"

Unresolved reference WearableRecyclerView.Adapter error in Kotlin

I'm trying to create an android wear app with a list of items. To enable circular scrolling, I'm using WearableRecyclerView by coding in kotlin. But facing this error Unresolved reference: Adapter The same code in java doesn't throw this error! I've also referred the inner and outer classes concepts in kotlin. Adapter class is an abstract class of RecyclerView which in turn is extended by WearableRecyclerView. Any guidance will be of much use, Thankyou!!
Here is my code
class CustomRecyclerAdapter(context: Context,dataSet: Array<String>): WearableRecyclerView.Adapter<CustomRecyclerAdapter.viewHolder>() {
var mDataSet: Array<String>
var mcontext: Context
init {
mDataSet = dataSet
mcontext=context
}
inner class viewHolder(view: View) : WearableRecyclerView(mcontext) {
val mTextView: TextView
init {
mTextView = view.findViewById(R.id.textView)
}
override fun toString(): String {
return mTextView.text as String
}
}
fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): viewHolder {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.recycler_row_item, viewGroup, false)
return viewHolder(view)
}
fun onBindViewHolder(viewHolder: viewHolder, position: Int) {
// Replaces content of view with correct element from data set
viewHolder.mTextView.text = mDataSet[position]
}
// Return the size of your dataset (invoked by the layout manager)
fun getItemCount(): Int {
return mDataSet.size
}
}
Rather than using a (apparently non existent) WearableRecyclerView.Adapter consider using the non-wearable adapter: RecyclerView.Adapter
Your class inner class viewHolder(view: View) : WearableRecyclerView(mcontext) must extends WearableRecyclerView.ViewHolder instead of WearableRecyclerView like :
inner class viewHolder(view: View) : WearableRecyclerView.ViewHolder(view)

Out-projected type prohibits the use of method

I can't seem to wrap my head around Kotlin generics, please help
I read here and here and I still can't determine how to make this work. I have a RecyclerView adapter that uses an abstracted parent BaseFieldVH as the ViewHolder class.
So the Adapter class looks like this:
class MyAdapter() : RecyclerView.Adapter<BaseFieldVH<*>>() {
...
override fun onBindViewHolder(holder: BaseFieldVH<*>, position: Int) {
holder.bind(data[position])
}
}
And the ViewHolder implementation looks like this:
abstract class BaseFieldVH<in FF : BaseFormField>
(itemView: View) : RecyclerView.ViewHolder(itemView) {
...
abstract fun bind(formField: FF)
}
But the actual call to holder.bind(data[position]) is displaying the error:
Out-projected type 'BaseFieldVH<*>' prohibits the use of 'public
abstract fun bind(formField: FF): Unit defined in ...
use in Nothing?
I've also tried defining the Adapter with BaseFieldVH<in Nothing> but then I get a Type Mismatch error for attempting to put Nothing into a function that requires a BaseFormField.
use BaseFormField
Defining the Adapter with BaseFieldVH<iBaseFormField> actually resolves the binding issue, but then in the onCreateViewHolder there is a type mismatch for the view holders:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseFieldVH<BaseFormField> {
val itemView: View
when (viewType) {
HEADER_TYPE -> {
itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_header, parent, false)
return HeaderVH(itemView)
}
HeaderVH is an extension of BaseFieldVH, and so there's a type mismatch
It looks like the answer to this issue is that you can't implement a ViewHolder class that specifies a generic for binding. My guess is this arises from the fact that the RecyclerView.Adapter is written in Java and any Kotlin extension is too restrictive to implement it properly.
If you can contradict this conclusion, let me know!
I found this post which explains the whole projection concept in a way that I could understand.
In the end I abandoned my attempt to control the Type for the ViewHolder:
abstract class BaseFieldVH
(itemView: View) : RecyclerView.ViewHolder(itemView) {
...
abstract fun bind(formField: Any)
}
This means each child class of BaseFieldVH has to do some a type check in its bind function. For instance:
class HeaderVH
(itemView: View) : BaseFieldVH(itemView) {
...
override fun bind(headerField: Any) {
if (headerField is HeaderField) {
// do something
}
}
}
But now at least I'm not getting any errors.
If you have multiple complex objects in data list (in order to show different items in the recycler view) you also need to specify you holder.
The idea here is that you provide Adapter, not a Holder to decide how you need to handle each object type.
class MyAdapter() : RecyclerView.Adapter<BaseFieldVH<*>>() {
...
override fun onBindViewHolder(holder: BaseFieldVH<*>, position: Int) {
val item = data[position]
holder as BaseFieldVH<DATA_ITEM_TYPE>
item as DATA_ITEM_TYPE
holder.bind(item)
}
}

Why can't I use the var mSelectedItem in inner class ViewHolder?

I define mSelectedItem as a public var in the class CustomAdapter, I think mSelectedItem=getAdapterPosition() will be Ok when I use mSelectedItem in inner class ViewHolder.
But it failed, and display "Unresolved reference: mSelectedItem" error, why?
And more, what is good way for getAdapterPosition() in Kotlin, there is hint which display "This inspection reports calls to java get and set methods that can be replaced with use of Kotlin synthetic properties", but it will cause errro when I use mSelectedItem=getAdapterPosition .
class CustomAdapter (val backupItemList: List<MSetting>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
public var mSelectedItem = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_recyclerview, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: CustomAdapter.ViewHolder, position: Int) {
holder.bindItems(backupItemList[position])
holder.itemView.radioButton.setChecked(position == mSelectedItem);
}
override fun getItemCount(): Int {
return backupItemList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(aMSetting: MSetting) {
itemView.radioButton.tag=aMSetting._id
itemView.textViewUsername.text=aMSetting.createdDate.toString()
itemView.textViewAddress.text=aMSetting.description
mSelectedItem=getAdapterPosition() //It will cause error
}
}
}
If you don't want to make the ViewHolder an inner class (which you should not), you could create a class like AdapterSelection that has a field var selectedItem:Int inside it and replace your public var mSelectedItem = -1 with private var mSelectedItem = AdapterSelection(-1). Then pass the mSelectedItem to the bind method (bindItems(aMSetting: MSetting, adapterSelection:AdapterSelection) and inside the bind, set the position adapterSelection.selectedItem = getAdapterPosition().
You could have passed the adapter itself, but it is messy, that's why I suggest making another class.
ViewHolder is the Recycler rather than the operator.if you want to get the position,you put this mSelectedItem = position in onBindViewHolder.And this method named getAdapterPosition() always works on with notifyItemsetChanged().hope this will help you.

Categories

Resources