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
Related
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
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.
My structure is like this, in the Fragment I call the ViewModel which instantiate the Adapter
For each item of my RecyclerView, I have a button which I listen
binding.addItem.setOnClickListener(onAddItemClicked(product))
private fun onAddToQuoteClicked(product: Product): View.OnClickListener {
return View.OnClickListener {
// TODO add item
}
}
I don't know how to send this item data to the fragment, is there a way to do that?
Should I use interface between Adapter and ViewModel and between ViewModel and Fragment ?
Or Can I use RxEvent & RxBus ?
First for navigation between ViewModel and Fragment -
you can use RxJava PublishSubject make it public and subscribe to it in your Fragment which can use public methods of ViewModel.
or you can use an interface, better in my opinion as using interface you are using simpler logic, much less errors in that case, and also it is more clean code as you can name methods in interface as you want - easier to read.
Second for communication between Adapter and Fragment - you can use same strategy as described above. In that case I recommend to use RxJava, as usually you would just need to handle one or few click listeners (so it is for example for observable which emits some data object).
If you need some code examples text me in comments.
UPDATE - it is better practice to create your Adapter instance in View, as I assume you are using MVVM design pattern (where all UI related code must be located in View component).
Example Fragment - Adapter communication Rx - PublishSubject
In your Adapter
private val publisher = PublishSubject.create<Product>()
fun getObservableProductItem(): Observable<Product> = publisher.hide()
binding.addItem.setOnClickListener(onAddItemClicked(product))
private fun onAddToQuoteClicked(product: Product): View.OnClickListener {
return View.OnClickListener {
publisherRideItem.onNext(items[adapterPosition])
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
publisherRideItem.onComplete()
}
And in your Fragment
compositeDisposable.add(adapter.getObservableProductItem().subscribe {
//TODO do whatever you want with it
})
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
RxJava PublishSubject is the best option, I would prefer it and it is very easy.
I tried with recyclerView - Fragment - ViewModel. Some will have listadapter -> recyclerVire -> Fragment -> ViewModel. I have explained that below.
List Adapter
specify your PublishSubject here
class myapadter : ListAdapter
{
object RxBus {
val itemClickStream: PublishSubject<View> = PublishSubject.create()
}
}
ViewHolder
in your viewHolder trigger that PublishSubject.
class myViewholder : RecyclerView.ViewHolder(itemView)
{
itemView.myButton.setOnClickListener {
v -> RxBus.itemClickStream.onNext(v) }
}
Fragment
subscribe to PublishSubject here in the fragment, this will automatically get notified.
myapadter.RxBus.itemClickStream.subscribe(){ v ->
if (v.id == R.id.myButton) {
viewModel.callyourfunction()
}
You can directly add item through ViewModel reference and subscribe to RxJava EventBus or LiveData change in your Fragment or Activity
I want to create a custom onClickListener to use for databinding. This custom click listener prevents the user from spamming the button and triggering the event twice(like showing two dialogues at the same time). I made a custom listener below that works in normal code, but I don't know how to implement it for databinding like the android:onClick="" in xml.
abstract class OneClickListener(var delay: Long) : View.OnClickListener {
private var hasClicked: Boolean = true
constructor() : this(1000)
override fun onClick(it: View) {
if (!hasClicked) {
return
} else {
hasClicked = false
onClicked(it)
GlobalScope.launch {
delay(delay)
hasClicked = true
}
}
}
abstract fun onClicked(it: View)
}
Is it possible to use this listner in databinding like for example
app:OneClickListener="#{viewModel::MyMethod}" in XML? and if yes, could you please tell me how?
Using data binding you can specify which listener to call when an event is fired just by calling it in a lambda. For example, let's say you have a method in your viewmodel, called myOnClick(). You can use it with data binding this way:
android:onClick="#{() -> viewModel.myOnClick()}"
Defining a custom binding adapter called OneClickListener is something different and it would not be called when the click event is fired, unless you use a trick: registering a click listener inside the custom binding adapter. This means you would have to call a method that register an other method: not really the cleanest way to add a listener.
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) }