define recyclerview click NOT as a member of constructor - android

from reading many website im aware that the optimal way handling onclick on recycerview was to set clicklistener not in OnBindViewHolder, its either in onCreateViewHolder(explained in here) or pass the clickListener in "bind" method(explained in here).
but one thing that really bother me, was how clickListener handled in activity / fragment
val adapter : MyAdapter(){
//click goes here
}
it may be not much but, honestly its not so so readable
i prefer to handle click listener not in constuctor, but with on separate method, meyabe like this
adapter.onItemClickListner = {
val adapter = MyAdapter()
//click more readable, yay!
}
but im not sure, if its will affect performance or not, or do i really need to stick with click on constructor?

I myself have used this method before.. Instead of passing the listener in the constructor, I have set it explicitly from the method call as you described. When I used that approach I had to make sure in the adapter that i checked for initialization of the listener and its null safety. So, you'll have to make sure before calling that it's absolutely present and initialized before calling the listener methods in the adapter.
I gradually moved to constructor as I didn't want to have such conscious checks all over my adapter before any click. (When there were like multiple clickable views.)

create a interface which has some function to handle your onClick.
Now you need to implement this interface in your activity class like this :-
class MainActivity : InterfaceAdapterOnClick (Here InterfaceAdapterOnClick is an interface which will have some functions to handle your onClick)
Let's suppose we have one function named onClickItem in our interface.
So now when you create your adapter like this adapter = MyAdapter() you can pass your interface with that. So it should look like :-
var adapter = MyAdapter(this) (this because you have already implemented interface in your mainActivity class) and your adapter should look like :-
class MyAdapter(onClickHandler: InterfaceAdapterOnClick) {}
So with this at the end you will have a seperate interface method in your mainActivity and your mainAcitvity should look like :-
class MainActivity : InterfaceAdapterOnClick {
// This will be your seperate onClick handler for your recyclerView item
override func onClickItem(// You can take some parameter as input here like `posistion` or `adapterItem` here ) {
}
}

IMHO, If your adapter needs that itemClickListener to function properly then you should pass it through its constructor and passing it using a setter function is not good since you might forgot to pass it at some point you forget to pass the listener and waste your time to check why clicking on items does not work! (It happened to me :D). If you are concerned about readability then you can use an interface and pass the implementation to adapter or use some features of Kotlin like named parameter or pass the callable reference of a function to make it more readable. For example something like this
class MyAdapter(onItemClick: (MyData) -> Unit) {
// ...
}
And initialize it like this
val adapter = MyAdapter(
onItemClick = { data ->
// ...
}
)
or pass the function like this
class MyActivity: AppCompatActivity() {
override fun onCreateView(/**/) {
// ...
val adapter = MyAdapter(
onItemClick = ::onItemClick
)
// or
// val adapter = MyAdapter(::onItemClick)
}
private fun onItemClick(data: MyData) {
}
}

you can simply pass an interface between the activity and the adapter class but the best method is by passing a function to your adapter class, like this
adapter = BasketRVA { basketEntity: BasketEntity, minus: Boolean -> changeQuantity(basketEntity, minus) }
private fun changeQuantity(basketEntity: BasketEntity, minus: Boolean) {
///your code
}
and in the adapter type this
class BasketRVA(private val clickListener: (BasketEntity, Boolean)->Unit): RecyclerView.Adapter<BasketRVA.BasketViewHolder>()
and in your viewHolder class and bind function
inner class BasketViewHolder(binding: BasketItemBinding): RecyclerView.ViewHolder(binding.root) {
#SuppressLint("SetTextI18n")
fun bind(basketEntity: BasketEntity?, clickListener: (BasketEntity, Boolean) -> Unit){
binding.tvProductName.text = basketEntity?.productName
in your binding function create a click listener for your view and pass the function you want to proceed
binding.imMinus.setOnClickListener {
val minus = true
clickListener(basketEntity!!, minus)
}
in your bind viewHolder
override fun onBindViewHolder(holder: BasketViewHolder, position: Int) {
BasketViewHolder(binding).bind(basketList[position], clickListener)
Log.d(TAG, "onBindViewHolder: basket list position is ${basketList[position]}")
}
Ask me if you still have any question

Related

How can I access an arrayList item from another class in Kotlin

I created a recycler view and feed data to it from MyAdapter class, I wanted to get name in recyler view item when it's clicked, On a youtube video I learnt how to get index of item (when clicked). I created an araryList of containing all the names in MyAdapter class, so I can access them it in MainAcitivty class by making a function to return needed arrayList value using index
public fun getName(pos: Int) : String {
return nameList[pos]
}
Now when I tried to call this function on MainActivity class and it shows an error (Unresolved reference: getName)
var name = MyAdapter.getName(position)
I did some searching and couldn't find a solution for this,
Can someone help me with properly calling a function in another class (in Kotlin)
or a better way to do the same thing
and sorry I'm new to Kotlin, Thank you!
in your code getName function is not static So you must get the name like this :
var name = (recyclerView.adapter as MyAdapter).getName(position)
But another way and correct way is to use the callback :
class MyAdapter(val onItemClick: (item: ItemModel?) -> Unit):
RecyclerView.Adapter<YourViewHolder>{
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
holder.binding.btn.setOnClickListener {
onItemClick.invoke(yourList[position])
}
}
}
Then in your activity or fragment listen to the callback
MyAdapter(){ item ->
}

Is an interface required for recyclerview item click event in Kotlin?

I'm trying to implement an item click event of a recyclerview.
In Java, the typical method of creating an interface in an adapter for clicking an item, implementing it in an activity, and passing an anonymous object to the adapter was used.
However, Kotlin accepts lambda expressions and can pass them as arguments.
So I don't necessarily need to use an interface, am I?
Or is there some good reason to use interfaces as much as possible?
You can use high-order functions to accomplish that without using interfaces.
Here is an example:
Fragment/Activity
recycler_view_photos.adapter = PhotosAdapter {
actionAfterClickOnItem()
}
private fun actionAfterClickOnItem() {
//stuff
}
Adapter
class PhotosAdapter(val onItemClicked: () -> Unit) {
//stuff
inner class PhotoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(photo: Photo) {
//stuff
itemView.setOnClickListener {
onItemClicked.invoke()
}
}
}
}
Interfaces are good to create contracts between components, give a type to a class, and create events, but with Kotlin as you mentioned, you can use lambdas and do the same thing with fewer lines of code.
Interfaces are best for click listeners in recyclerView like you have views in each view are three buttons like delete, remove and add button so just make three methods in interface
fun onDeleteClick(position,Item) //Here item of that model which is passed in adapter
fun onAddClick(position,Item)
fun onRemoveClick(position,Item)
implement this interface with your activity and pass it in your adapter and then in your adapter create click listener like this
Holder.itemBinding.delete.setOnClickListener{ clickListener.OnDeleteClick(position, Item) }
So instead of passing lambda function for each button just use interface for all clicks and override in your activity

How to set OnClickListener on RecyclerView item in MVVM structure

I have an app structured in MVVM. I have different fragments within the same activity. Each fragment has its own ViewModel and all data are retrieved from a REST API.
In FragmentA, there is a RecyclerView that lists X class instances. I want to set OnClickListener on the RecyclerView and I want to pass related X object to FragmentB when an item clicked in the RecyclerView. How can I achieve this?
How I imagine it is the following.
The Fragment passes a listener object to the adapter, which in turn passes it to the ViewHolders
Here is a quick sketch of how it should look like
class Fragment {
val listener = object: CustomAdapter.CustomViewHolderListener() {
override fun onCustomItemClicked(x: Object) {}
}
fun onViewCreated() {
val adapter = CustomAdapter(listener)
}
}
---------------
class CustomAdapter(private val listener: CustomViewHolderListener) {
val listOfXObject = emptyList() // this is where you save your x objects
interface CustomViewHolderListener{
fun onCustomItemClicked(x : Object)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.itemView.setOnClickListener {
listener.onCustomItemClicked(listOfXObject[position])
}
}
}
Here are some articles that might help you get the general gist of the things.
They don't answer your question directly though
Hope it is helpful
link 1 link 2
if you're using data binding you need to pass your view(which is Fragment in your case) into the layout via adapter class and you need to import your view in layout file to be able to call view's method
android:onClick="#{() -> view.onXXXClick(item)}"
pass your current model class which is item into this new method and then create onXXXClick method in your view and do whatever you wish.
if you will be doing view related operations such as navigation from one fragment to another or starting a service you should create above function in your view, if you're doing network or db related operations it should be in your ViewModel
you can check out my GitHub repository to understand better.

Use ViewHolder element outside onBindViewHolder?

Is it possible to use ViewHolder element outside onBindViewHolder?
This is my adapter class
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = mValues[position]
with(holder.mView) {
tag = item
txvTitle.text = item.title
setOnClickListener(mOnClickListener)
}
}
inner class ViewHolder(val mView: View) :RecyclerView.ViewHolder(mView{})
If I have one function in adapter, how can I use txvTitle?
fun checkHashMapExists(hashMap: HashMap<Long, ABC>?, newValues: ArrayList<OT>){
for(i in newValues){
if(hashMap?.keys.toString().contains(i.id)){
txvTitle.setTextColor(Color.parseColor("#000000")) // txvTitle cannot resolved
}
}
}
FragmentA
override fun onResume() {
super.onResume()
mAdapter.checkHashMapExists(hashMap,otList)
}
Even though I do not recommend using it, you can call the following method to get the ViewHolder for data element at position i:
MyViewHolder holder = (MyViewHolder) mRecyclerView.findViewHolderForAdapterPosition(i);
if(holder != null) {
// Do whatever you want
}
Keep in mind what the documentation says about that method:
Return the ViewHolder for the item in the given position of the data set. Unlike findViewHolderForLayoutPosition(int) this method takes into account any pending adapter changes that may not be reflected to the layout yet. On the other hand, if notifyDataSetChanged() has been called but the new layout has not been calculated yet, this method will return null since the new positions of views are unknown until the layout is calculated.
Instead, what I would do if I were you, is to have an attribute in your model class that indicates which color its ViewHolder should use to display the txtView.title text.
Then, whenever you want to update the color of an element (or elements) in your RecyclerView, you can change that attribute of your model class and then call notifyDataSetChanged();
Normally you should not use ViewHolder outside onBindViewHolder. All UI-related code should be called inside onBindViewHolder. Prefer to call notifyDataSetChanged or other notifyXXX method to redraw your RecyclerView content.
The adapter has no way to know what txvTitle means, as it is specific to a single item in the RecyclerView.
Within a ViewHolder however, this property (if existent) is related to the view that is being "prepared".
Thus, updates to the view should only be done in the ViewHolder class.
Take a look at this example.
In this example, a separate class is created which extends RecyclerView.Viewholder(view).
Within this class, a method called bind is defined, which is passed the object (or model) that is used to populate the view.
The create method inflates the layout of the view to be drawn in the RecyclerView
You can create a function in your adapter class which takes Higher order function as parameter and then expose your ViewHolder object in that function to be used outside of Adapter class.
See how it can be done :
Let's say you've Adapter class as below
class Adapter {
...
// Here we create object of our Higher order function
var holderCallback: ((RecyclerView.ViewHolder?) -> Unit)? = null
//Then We provide callback like below in onBindViewHolder method
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
... Some binding stuff
holderCallback?.let {
it(holder)
}
... even some more stuff
}
}
And now we can access from outside (In your case, from Fragment class)
class Fragment {
// Here we have adapter object
...
//So, we get our callback like below from any method in here
...Inside some method where we receive callback
adapter.holderCallback { viewHolder: RecyclerView.ViewHolder? -> //Here you've got ViewHolder object
// Now do some amazing stuff here !!
}
}

ListAdapter submitList() - How to scroll to beginning

I have an issue with my RecyclerView and ListAdapter.
Through an API, I receive items in a ascending order from older to newer.
My list is then being refreshed by calling the submitList(items) method.
However, since everything is asynchronous, after receiving the first item, the RecyclerView remains on the position of the first item received and showed.
Since in the ListAdapter class there is no callback when the submitList() method completed, I cannot find a way to scroll after the updates to one of the new items that has been added.
Is there a way to intercept when ListAdapter has been updated ?
Kotlin : Very simple method to do this.
listAdapter.submitList(yourNewList) {
yourRecyclerView.scrollToPosition(0)
}
Not specific to ListAdapter, but it should work nonetheless:
Just use adapter.registerAdapterDataObserver(RecyclerView.AdapterDataObserver) and override the relevant onItemXXX method to tell your RecyclerView to scroll to whatever position.
As an example:
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
(recycler_view.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(positionStart, 0)
}
})
I have checked ListAdapter source code and I see that there is onCurrentListChanged method.
You can simply override it in your custom adapter. You can also pass a listener to the adapter to catch the event and scroll your RecyclerView to the beginning.
Your adapter:
typealias ListChangeListener = () -> Unit
class Adapter(private val listChangeListener: ListChangeListener) : ListAdapter<Item, Adapter.ViewHolder>(Callback) {
override fun onCurrentListChanged(previousList: List<Item>, currentList: List<Item>) {
super.onCurrentListChanged(previousList, currentList)
// Call your listener here
listChangeListener()
}
// Other methods and ViewHolder class
}
Create the adapter in your Activity or Fragment:
recyclerView.adapter = Adapter { recyclerView.scrollToPosition(0) }
The method to override is called:
public void onCurrentListChanged(List<T> previousList, List<T> currentList)
As the documentation reads:
Called when the current List is updated.
The inline documentation of the ListAdapter reads:
This class is a convenience wrapper around AsyncListDiffer that implements Adapter common default behavior for item access and counting.
And later on it reads:
Advanced users that wish for more control over adapter behavior, or to provide a specific base class should refer to AsyncListDiffer, which provides custom mapping from diff events to adapter positions.
This means, you'd need to extend the regular RecyclerView.Adapter, combined with a custom implementation of the AsyncListDiffer; and not the ListAdapter (which is just a convenience wrapper, with the stated basic functionality).
One can use the ListAdapter class as a template for that (not extending it is the clue), but the resulting class can then be extended and reused. else there is no chance to get control over the desired callback.
For my case, just override onCurrentListChanged in your adapter class and call notifyDataSetChanged()
override fun onCurrentListChanged(
previousList: MutableList<InboxMessage>,
currentList: MutableList<InboxMessage>
) {
super.onCurrentListChanged(previousList, currentList)
notifyDataSetChanged()
}
listAdapter.submitList(yourNewList) { yourRecyclerView.smoothScrollToPosition(0) }

Categories

Resources