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
Related
I want to make a custom view with two Recyclerview data. First one is header titles, second one is content details. When I click on a title, I want different XML files to be shown.
After some research, I saw that I could do it using TabLayout and ViewPager, however, I don't want to use an extra fragment for each tab, instead I want to inflate an XML file. Because I want to use this view on other screens as a custom view.
How can I do that? What should I use as a solution? Thank you.
You can use ViewPager2 :
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/topicViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
you can assign second recycler adapter to ViewPager2 for example:-
class DummyAdapter(var list:ArrayList<String>) :
RecyclerView.Adapter<DummyAdapter.ViewHolder>() {
lateinit var context: Context
var isFullScreen = false
var isDownload: Boolean = false
var doubleClick = false
inner class ViewHolder(val binding: DummyItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
position: Int,
context: Context,
adapter: DummyAdapter,
) {
with(binding) {
// add data
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder {
context = parent.context
val inflater = LayoutInflater.from(context)
val binding = DummyItemBinding.inflate(inflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) =
holder.bind(position, context, this, list[position])
override fun getItemCount(): Int {
return list.size
}
}
binding.viewPager.adapter = dummyAdapter
then on the click of TabLayout you can update the adapter of second recyclerView.
Could you please help me to bind views in view holder. I have read existing answer but not sure how to implement it in my case.
My holder looks like this and I don't have onCreateViewHolder
class MyViewHolder(
itemView: View,
private val myListener: MyListener
) : RecyclerView.ViewHolder(itemView) {
private val myLabel: AppCompatTextView = itemView.**findViewById**(R.id.myLabel)//**here need to remove findviewbyId**
fun bindView() {
itemView.background = ContextCompat.getDrawable(itemView.context, R.drawable.image)
myLabel.setOnClickListener {
myListener.myMethod()
}
}
}
Pretty close, what you are missing is the binding in the constructor
class MyViewHolder(
private val binding: YourLayoutBinding, //you can access it because is private val
...
) : RecyclerView.ViewHolder(binding.root) { //You need the view so pass the root view of the binding
//change your view to binding dot view
fun bindView() {
binding.background = ContextCompat.getDrawable(itemView.context, R.drawable.image) //in this case the background seems data binding so I'm treating it as such
binding.myLabel.setOnClickListener {
myListener.myMethod()
} //but here myLabel is a view contained inside the binding
}
}
And you are probably gonna have doubts regarding the onCreateViwHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = YourLayoutBinding.inflate(inflater, parent, false)
return MyViewHolder(binding, ...)
}
The trick is the bindings have an static method to inflate them
I want to get isChecked data from checkbox from recycler view in this I am using Binding Adapter but I am not getting how to do that. If anyone has a way to do that then please share it.
class ItemListAdapter(private val itemDeleteListener: ItemDeleteListener,
private val checkItemListener: CheckItemListener) :
ListAdapter<ListItemTable, ItemListAdapter.ViewHolder>(ListItemDiffCallBack()) {
class ViewHolder(private val binding: ShowItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listItemTable: ListItemTable, itemDeleteListener: ItemDeleteListener,
checkItemListener: CheckItemListener) {
binding.itemHistory = listItemTable
binding.itemDelete = itemDeleteListener
binding.checkItem = checkItemListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ShowItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position), itemDeleteListener, checkItemListener)
getItem(position).itemCompleted
}
class ItemDeleteListener(val clickListener: (listId: Long) -> Unit) {
fun onClick(listItemTable: ListItemTable) = clickListener(listItemTable.itemId)
}
class CheckItemListener(val clickListener: (listId: Long) -> Unit){
fun onClick(listItemTable: ListItemTable) = clickListener(listItemTable.itemId)
}
class ListItemDiffCallBack : DiffUtil.ItemCallback<ListItemTable>() {
override fun areItemsTheSame(oldItem: ListItemTable, newItem: ListItemTable): Boolean {
return oldItem.itemId == newItem.itemId
}
override fun areContentsTheSame(oldItem: ListItemTable, newItem: ListItemTable): Boolean {
return oldItem == newItem
}
}
}
See: https://developer.android.com/topic/libraries/data-binding/two-way
If you're using data binding I think you should take a look at two-way data binding. It's even introduced through this checkbox problem inside the docs, so you're in luck!
Edit: To be a bit more specific you would implement this like so:
Create a binding adapter that sets the property on the view
<layout>
<data>
<variable
name="itemHistory"
type="your.package.ListItemTable" />
<variable
name="checkItem"
type="android.widget.CompoundButton.OnCheckedChangeListener" />
<!-...->
</data>
<CheckBox
android:id="#+id/rememberMeCheckBox"
android:checked="#{itemHistory.itemCompleted}"
android:onCheckedChanged="#{checkItem}"
/>
</layout>
So you have to use OnCheckedChangeListener instead of your own listener, and pass that to your xml.
You already have 2 "listeners" on your constructor of the adapter and you are passing them to the view holder by using the bind method.
class ItemListAdapter(private val itemDeleteListener: ItemDeleteListener, private val checkItemListener: CheckItemListener)
So you have to pass the correct one to your view holder and then set the checkbox listener. Is better to use the constructor of the view holder to avoid passing the same listener on every binding
class ViewHolder(private val binding: ShowItemBinding, private val itemDeleteListener: ItemDeleteListener, private val checkItemListener: CheckItemListener) {
fun bind(listItemTable: ListItemTable) {
binding.YOUR_CHECKBOX.setOnCheckedChangeListener(null)
binding.itemHistory = listItemTable
binding.itemDelete = itemDeleteListener
binding.checkItem = checkItemListener
binding.executePendingBindings()
binding.YOUR_CHECKBOX.setOnCheckedChangeListener { -, isChecked ->
//here call your "listener" by example
checkItemListener.theMethod(isChecked)
}
}
}
We set the checkbox listener to null first so we can stop listening and then set the view values, otherwise, anything changing the view will also trigger the listener, once the view values are set, then set the checkbox listener to trigger the callbacks.
I am trying to observe fields of my class without exposing it. So far, I've tried this:
TaskItemViewModel.kt
open class TaskItemViewModel(private val navigator: ITaskNavigator) : ViewModel() {
private val taskItem: MutableLiveData<TaskItem> = MutableLiveData()
val title: LiveData<String?> = Transformations.map(taskItem) { it.title }
val content: LiveData<String?> = Transformations.map(taskItem) { it.content }
var showCheck: LiveData<Boolean> = Transformations.map(taskItem) { it.isCompleted }
fun setModel(model: TaskItem) {
this.taskItem.value = model
}
}
ItemListScreenAdapter.kt
class ItemListScreenAdapter(private val navigator: ITaskNavigator) : RecyclerView.Adapter<ItemListScreenAdapter.TaskItemViewHolder>() {
private val TAG = "ItemListScreenAdapter"
private var dataset: List<TaskItem> = listOf()
override fun onBindViewHolder(viewHolder: TaskItemViewHolder, position: Int) {
with(viewHolder.binding) {
this.viewModel?.setModel(dataset[position])
executePendingBindings()
}
}
fun updateDataset(dataset: List<TaskItem>) {
Log.d(TAG,"Updating dataset")
this.dataset = dataset
notifyDataSetChanged()
}
override fun getItemCount(): Int = dataset.size
override fun onCreateViewHolder(parent: ViewGroup, type: Int): TaskItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemTaskBinding.inflate(inflater, parent, false)
binding.viewModel = TaskItemViewModel(navigator)
return TaskItemViewHolder(binding)
}
class TaskItemViewHolder(val binding: ItemTaskBinding) : RecyclerView.ViewHolder(binding.root)
}
If I call setModel before inflating the view, everything works fine. However, after the view is inflated, the view is not updated even if taskItem 's value is updated. You can be assured that updateDataset is called everytime there is a change in dataset.
I want the view to be updated whenever I call setModel in corresponding viewmodel. What are the ways to achieve this?
For this viewmodel, I want to use ViewModel rather than BaseObservable. Therefore, please give your answers according to this.
EDIT:
I have found the solution to the problem.
in ItemListScreenAdapter's onCreateViewHolder method, after inflating, I needed to set LifeCycleOwner of the binding.
I added the following line after inflating the ItemTaskBinding.
binding.setLifecycleOwner(parent.context as MainActivity)
and the problem is solved and view is being updated.
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.