I am trying to write a RecyclerView adapter class in Kotlin for android. I am trying to use the traditional way of creating a custom viewer class, for custom objects, and use a click listener in that. While I am able to do rest of the things like access the variables of inner class and show the RecyclerView, what I have not been able to do is add click listener to the var objects of inner class.
ie something like
var convertView : View? = itemView
convertView.setOnClickListener(this)
Following is my complete code of adapter class
public open class TestAdapter(val items: MutableList<Any>, val context: Activity) : RecyclerView.Adapter<TestAdapter.CustomViewHolder>() {
public var mItem: MutableList<Any> = items
public var mActivity: Activity = context
protected var clickListener: ExampleInterface? = null
public interface ExampleInterface {
fun click(pos: Int) {
}
}
open public fun setListener(mInterFaceListener: ExampleInterface) {
clickListener = mInterFaceListener
}
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): CustomViewHolder {
var parentLayout: View = LayoutInflater.from(mActivity).inflate(R.layout.custom_view, p0, false)
return CustomViewHolder(parentLayout)
// return CustomViewHolder(LayoutInflater.from(mActivity).inflate(R.layout.custom_view, p0, false))
}
override fun getItemCount(): Int {
return mItem.size
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onBindViewHolder(p0: CustomViewHolder, p1: Int) {
p0.dataView.text = mItem.get(p1).toString()
}
inner class CustomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
var convertView: View? = itemView
var dataView: TextView = convertView!!.findViewById(R.id.data)
var mposition = adapterPosition
override fun onClick(p0: View?) {
if (clickListener != null) {
clickListener!!.click(mposition)
}
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}
So if you see these two lines in CustomView class
var convertView: View? = itemView
var dataView: TextView = convertView!!.findViewById(R.id.data)
I cannot access these variables "convertView" and "dataView" so that I can set clicklistener to them. So how to achieve it ?
Thanks :)
I referred to this site
https://www.raywenderlich.com/367-android-recyclerview-tutorial-with-kotlin
Here I go to know my mistake , I need to use init in the class, there I am able to access it and initialize on click listener. Got it working
init {
convertView?.setOnClickListener(this)
}
Though the above answer could be acceptable as well, as I am new to Kotlin I cannot say which one is the better option, but my requirement is satisfied with the above mentioned site.
Thank you :)
In RecyclerView adapters, You could place the OnClickListeners in onCreateViewHolder in order to prevent them from being set each time onBindViewHolder is called (as onBindViewHolder is called multiple times when RecyclerView is scrolled). Use parentLayout in your onCreateViewHolder to access your views and set onClickListener to them. to determine current position in onCreateViewHolder you can do as below :
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): CustomViewHolder {
val parentLayout: View = LayoutInflater.from(mActivity).inflate(R.layout.custom_view, p0, false)
val customViewHolder = CustomViewHolder(parentLayout)
parentLayout.myExampleTextView.setOnClickListener {
// Place onClick logic here.
// If you need position, do as bellow :
val adapterPosition = customViewHolder.adapterPosition
if(adapterPosition != RecyclerView.NO_POSITION) {
// Place your position dependent logic here
}
}
return customViewHolder
}
UPDATE:
I updated the code snippet above and added the RecyclerView.NO_POSITION (which is equal to -1) check. The reason for the position being returned as -1 is explained below.
From android docs:
The other set of position related methods are in the form of
AdapterPosition. (e.g. getAdapterPosition(), findViewHolderForAdapterPosition(int)) You should use these methods
when you need to work with up-to-date adapter positions even if they
may not have been reflected to layout yet. For example, if you want to
access the item in the adapter on a ViewHolder click, you should use
getAdapterPosition(). Beware that these methods may not be able to
calculate adapter positions if notifyDataSetChanged() has been called
and new layout has not yet been calculated. For this reasons, you
should carefully handle NO_POSITION or null results from these
methods.
you can access them in the onBindViewHolder using the p0 this way p0.dataView so there you can set listeners successfully
Related
I try to make a small file manager, I want that when holding a file or folder I get a contextual menu, and I tried to use registerForContextMenu(newRecyclerView) but it does not work for me, nothing happens at all, instead if I do it with another element like a button or an ImageView, the menu comes out perfect, and I've been googling for hours, but most of the solutions I've found are in java, and I don't know how to implement them in kotlin, one thing to keep in mind is that when handling files , the recylerview is going to be constantly changing as we navigate through the directories, what I want is a menu where I can copy, cut the file and stuff, I know there are other solutions for this, but I want to implement them with a context menu , here is MyAdapter, I don't put the Main Activity,because it's a riot
import...
class MyAdapter(private val newsList:
ArrayList<News>):RecyclerView.Adapter<MyAdapter.MyViewHolder>()
{
private lateinit var mListener:OnItemClickListener
interface OnItemClickListener{
fun onItemClick(position: Int)
}
fun setOnItemClicKListener(listener:OnItemClickListener){
mListener = listener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType:
Int): MyViewHolder {
val itemView =
LayoutInflater.from(parent.context).inflate(R.layout.list_items,
parent, false)
return MyViewHolder(itemView, mListener)
}
override fun onBindViewHolder(holder: MyViewHolder, position:
Int) {
val currentItem = newsList[position]
holder.titleImage.setImageResource(currentItem.titleImage)
holder.tvHeading.text = currentItem.heading
}
override fun getItemCount(): Int {
return newsList.size
}
class MyViewHolder(itemView: View, listener:
OnItemClickListener):RecyclerView.ViewHolder(itemView){
val titleImage: ShapeableImageView =
itemView.findViewById(R.id.title_image)
val tvHeading:TextView = itemView.findViewById(R.id.tvHeading)
init{
itemView.setOnClickListener {
listener.onItemClick(adapterPosition)
}
}
}
}
Instead of registering the entire RecyclerView with a context menu, you should register each child view. This is because you are holding the child view, not the RecyclerView itself.
I have a RecyclerView which has an EditText in its items.
For each EditText I have added a textChangeListener.
Whenever the user types a character I want to do an input validation and so I can change the item, for example show red border in case of wrong input etc...
After I do the validation I call:
adapter.notifyItemChanged(itemPosition)
to update the view.
But the problem is that the focus is lost from the EditText and I have to click again inside the box in order to continue typing.
Is there a solution for this case? How can I continue typing while the view is updated after calling notifyItemChanged ?
ADAPTER
class ItemAdapter() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = LayoutItemBinding.inflate(inflater, parent, false)
binding.editText.addTextChangedListener {
// Do layout changes....
if (isInvalidInput) {
item.setError = true
....
...
}
adapter.notifyItemChanged(itemPosition)
}
return ItemViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.bind(items[position])
}
inner class ItemViewHolder(val binding: LayoutItemBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(model: ItemModel) {
// Setup view for item
}
}
}
Try to move your error setting code in the bind function of the ViewHolder and update the error status outside of the TextWatcher:
class ItemAdapter() : RecyclerView.Adapter<RecyclerView.ViewHolder>()
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = LayoutItemBinding.inflate(inflater, parent, false)
return ItemViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.bind(items[position], position)
}
inner class ItemViewHolder(val binding: LayoutItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(model: ItemModel, position: Int) {
binding.editText.addTextChangedListener {
// Update the ItemModel on text change
adapter.notifyItemChanged(position)
}
// Update the view error status everytime it is updated
val isInvalidInput = ... // Add your logic
if (isInvalidInput) {
model.setError = true
....
....
}
}
}
}
First, Please post your code.
If you called notifyDatasetChanged(), all views of recyclerview will refreshed (it means that the views could be recreated.).
So if you want remain focus in edittext, you not call notifyDatasetChanged() or set focus to your edittext after notifyDataChanged().
But setFocus is bit more difficult.
So I suggest method that update views of recyclerview without call notifyDatasetChanged().
I think you can directly update view of recyclerview without call notifyDataChanged().
You should save the id of item that contains the EditText that is editing. And in the bind() method of ViewHolder, you will check if the item is currently being edit, you will call requestFocus() method on EditText.
And please take note that when you call notifyDataChanged(), the RecycleView maybe check the data of each item to know whether it was changed or not. If the data of an item was changed, RecycleView will redraw that item.
I user MVVM and RecyclerView in this app so the recycle view show the list perfectly but when i add the view model to adapter i get an error in the logcat
Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.
i am new in this MVVM and i know is this possible or is any other way to do this
this is my adapter class with the viewHolder
class KeefAdapter : RecyclerView.Adapter<KeefViewHolder>() {
var dataOfAllKeef = listOf<String>()
init {
dataOfAllKeef = arrayListOf("Marijuwana" , "Bango" , "Weed" , "Hash")
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KeefViewHolder {
lateinit var binding: KeefSingleItemBinding
binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context) , R.layout.keef_single_item , parent , false)
val viewModel:OrderYourKeefViewModel = ViewModelProvider(OrderYourKeef()).get(OrderYourKeefViewModel::class.java)
binding.orderViewModelWithSingle = viewModel
viewModel.count.observe(OrderYourKeef(), Observer { newCountOfHash->
binding.root.theCountOfHash.text = newCountOfHash.toString()
})
return KeefViewHolder(binding.root)
}
override fun getItemCount() = dataOfAllKeef.size
override fun onBindViewHolder(holder: KeefViewHolder, position: Int) {
val item = dataOfAllKeef[position]
holder.keefName.text = item
if (item.equals("Marijuwana")) {
holder.keefImage.setImageResource(R.mipmap.marijuana)
} else if (item.equals("Bango")) {
holder.keefImage.setImageResource(R.mipmap.bango)
} else if (item.equals("Weed")) {
holder.keefImage.setImageResource(R.mipmap.weed)
} else if (item.equals("Hash")) {
holder.keefImage.setImageResource(R.mipmap.hashesh)
}
}
}
class KeefViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) {
var keefName:TextView = itemView.keefName
var keefImage: ImageView = itemView.keefImage
var increase: Button = itemView.increaseTheCount
var decrease: Button = itemView.minusTheCount
var theCountOfKeef: TextView = itemView.theCountOfHash
}
I think this is not the correct way to implement the MVVM pattern.
You have to call the viewModel = ViewModelProviders in your Activity. And after fetching the list items, pass it to your adapter and call the notifyDataSetChanged():
updateListItems(newListItems: List<YourItem>) {
currentItems = newListItems
notifyDataSetChanged()
}
Read more about it here
Adapter seems to be designed to be used rather passively than actively.
In OP's code, he would observe and get newCountOfHash in onCreateViewHolder to set it to binding.root.theCountOfHash.text. So this is a case that Adapter would actively seek and grab a value.
To avoid this 'active' Adapter, we should define Adapter behaving passively. Locally define countOfHash as Adapter's field value. The Adapter shouldn't mind countOfHash is LiveData or not. It just looks the field value.
class KeefAdapter : RecyclerView.Adapter<KeefViewHolder>() {
var countOfHash
override fun onBindViewHolder(holder: KeefViewHolder, position: Int) {
// You should not do this in onCreateViewHolder
// because that is done only once on creation time.
// (not invoked later again)
binding.root.theCountOfHash.text = countOfHash
}
}
Then outside of the Adapter, from Activity or Fragment that holds the Adapter, you may update Adapter.countOfHash with an Observer:
val viewModel:OrderYourKeefViewModel
= ViewModelProvider(OrderYourKeef()).get(OrderYourKeefViewModel::class.java)
viewModel.count.observe(OrderYourKeef(), Observer { newCountOfHash ->
Adapter.countOfHash = newCountOfHash.toString()
})
(Note: I'm not using Kotlin actively, there may be some syntax mistakes)
I am researching on how to add an onClick event on my recyclerview properly,
currently I am using the interface inside my customAdapter
class CategoryAdapter(val categoryList : List<CategoryObject>, val context: Context, val mItemClickListener: MainInterface) : RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
interface MainInterface {
fun onCategoryItemClick(categoryKey: Int)
}
override fun getItemCount(): Int {
return categoryList.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.listview_category, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder?.categoryName?.text = categoryList[position].categoryName
// holder?.categoryName.setOnClickListener{
// mItemClickListener.onCategoryItemClick(position)
// }
// holder?.categoryName?.setOnClickListener { listener(categoryList[position]) }
}
inner class ViewHolder (view: View) : RecyclerView.ViewHolder(view) {
val categoryName = view.lv_txt_category_name
init {
view.setOnClickListener {
mItemClickListener.onCategoryItemClick(adapterPosition)
}
}
}
}
viewHolder's onClick does not register on my activity ovveride function.
but putting the onClick inside onBindViewHolder works perfectly,
I'm not sure which is more efficient, if the onBindViewHolder onClick is the right answer, then I'll stick with it, but if the viewHolder is the right one, why it does not work properly?
Thanks in advance!
Update
This is the stackoverflow post I'm using to research things
RecyclerView itemClickListener in Kotlin
tried android:clickable="true" in your xml?
As the documentation suggest for:
RecyclerView.Adapter.html#onBindViewHolder
Called by RecyclerView to display the data at the specified position.
This method should update the contents of the itemView to reflect the
item at the given position.
And for the RecyclerView.Adapter.html#onCreateViewHolder
Called when RecyclerView needs a new RecyclerView.ViewHolder of the
given type to represent an item.
This new ViewHolder should be constructed with a new View that can
represent the items of the given type. You can either create a new
View manually or inflate it from an XML layout file.
As onCreateViewHolder is a method where we return the type of ViewHolder only while in onBindViewHolder we update or initialise the data to display.
It means no data is binded in onCreateViewHolder and binding anything in this method will have no effect.
I want to set an onclicklistener in the onBindViewHolder in order to navigate to a different fragment and send along some data to that fragment.
For the life of me, I can't seem to find a way to make it work. Any and all help is greatly appreciated!
The adapter class:
class ListAdapter(private val list: List<Workout>): RecyclerView.Adapter<WorkoutViewHolder>() {
override fun getItemCount(): Int{
return list.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WorkoutViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return WorkoutViewHolder(layoutInflater, parent)
}
override fun onBindViewHolder(holder: WorkoutViewHolder, position: Int) {
val workout: Workout = list[position]
holder.itemView.setOnClickListener{
Toast.makeText(holder.itemView.context, "TEST", Toast.LENGTH_LONG).show()
val id = workout.workoutId
val bundle = Bundle()
bundle.putInt("workoutId", id)
Navigation.createNavigateOnClickListener(R.id.workoutDetailsFragment)
}
holder.bind(workout)
}
}
I can get the toast to pop up, so the onclicklistener seems to be working. However, the navigation part does not work.
If I just set a button inside the fragment that is hosting the recyclerview and add button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.workoutDetailsFragment)) it can navigate just fine. So the problem seems to be calling the navigate function from inside the onclicklistener inside the onbindviewholder
Navigation.createNavigateOnClickListener() creates an OnClickListener. Creating an OnClickListener just to never set it on anything doesn't do anything.
Instead, you'll want to just trigger your navigate() call directly, doing the same one line of code that createNavigateOnClickListener does internally:
override fun onBindViewHolder(holder: WorkoutViewHolder, position: Int) {
val workout: Workout = list[position]
holder.itemView.setOnClickListener{
Toast.makeText(holder.itemView.context, "TEST", Toast.LENGTH_LONG).show()
val id = workout.workoutId
val bundle = Bundle()
bundle.putInt("workoutId", id)
// Using the Kotlin extension in the -ktx artifacts
// Alternatively, use Navigation.findNavController(holder.itemView)
holder.itemView.findNavController().navigate(
R.id.workoutDetailsFragment, bundle)
}
holder.bind(workout)
}
You need to assign your created listener rather than using it inside of a lambda. When you use a lambda with setOnClickListener(), the lambda literally is your listener. So in your example, you're creating a listener, but it's never assigned anywhere.
So to instead assign the created listener from Navigation.createNavigateOnClickListener(), your code should look like holder.itemView.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.workoutDetailsFragment))