I'm trying to set a simple collapsing item in my recyclerView.
I saw many applications in Java, but only a few in Kotlin.
Almost the onClick method is set inside onBindViewHolder like this:
val isExpandable:Boolean = data[position].expandable
holder.subItem.visibility = if(isExpandable) View.VISIBLE else View.GONE
holder.mainItem.setOnClickListener {
data[position].expandable = !data[position].expandable
notifyItemChanged(position)
}
This work very well. But, I read some developers saying that is not good idea apply onClick methods inside onBindViewHolder, and it must be done inside ViewHolder class.
As I'm using Kotlin, I applied onClick method in Koltin way, using lambda. like this:
class MyAdapter(val data:List<MyModel>,
val listener:(MyModel)->Unit):RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
class MyViewHolder(itemView:View):RecyclerView.ViewHolder(itemView){
fun bind (data:MyModel,listener: (MyModel) -> Unit) = with(itemView){
...
view.setOnClickListener {listener(data)}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder = MyViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.card_item,parent,false)
)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(data[position], listener)
}
override fun getItemCount(): Int = data.count()
}
How can I use data[position] and notifyItemChanged(position) inside onClick?
Note: with this Kotlin method, in Activity, when I set the Adapter, the data can be accessed like this:
with(my_recycler) {
layoutManager = LinearLayoutManager(this#MainActivity, RecyclerView.VERTICAL, false)
adapter = MyAdapter(it) {
//data can be accessed here according item position
}
}
If you want to access properties in the ViewHolder from your adapter, you can declare the ViewHolder as an Inner class.
inner class MyViewHolder(itemView:View):RecyclerView.ViewHolder(itemView){
fun bind (data:MyModel,listener: (MyModel) -> Unit) = with(itemView){
...
view.setOnClickListener {listener(data)}
}
}
I haven´t tried calling the notifyItemChanged(position) but I would say that if it is going to be inside a click listener, it shouldn't crash. If you call it from outside of the listener and inside of a function that is called upon notifyItemChanged then you probably will get a crash.
Let me know if it works.
Related
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 7 months ago.
Improve this question
I got crazy ı am not able find solution. Please help me. 2 days ı am looking for solution. I clearly watn that if I click any recylerView Item starting a new activity. Each item has to own its activity.How to implement codes?. Thanks in advance.
My adapter code:
class CustomAdapter(private val mList: List<ItemsViewModel>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
// create new views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// inflates the card_view_design view
// that is used to hold list item
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.card_view_design, parent, false)
return ViewHolder(view)
}
// binds the list items to a view
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val ItemsViewModel = mList[position]
// sets the image to the imageview from our itemHolder class
holder.imageView.setImageResource(ItemsViewModel.image)
// sets the text to the textview from our itemHolder class
holder.textView.text = ItemsViewModel.text
holder.itemView.setOnClickListener {
}
}
// return the number of the items in the list
override fun getItemCount(): Int {
return mList.size
}
// Holds the views for adding it to image and text
class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
val imageView: ImageView = itemView.findViewById(R.id.imageview)
val textView: TextView = itemView.findViewById(R.id.textView)
}
}
My recyclerview.kt :
class recyclerView : AppCompatActivity() {
private lateinit var binding: ActivityRecyclerViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRecyclerViewBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.hide()
// getting the recyclerview by its id
val recyclerview = findViewById<RecyclerView>(R.id.recyclerview)
// this creates a vertical layout Manager
binding.recyclerview.layoutManager = LinearLayoutManager(this)
// ArrayList of class ItemsViewModel
val data = ArrayList<ItemsViewModel>()
data.add(ItemsViewModel(R.drawable.jumping_jack, "Jumping Jack"))
data.add(ItemsViewModel(R.drawable.jumping_aquats, "Jumping Squat"))
// This will pass the ArrayList to our Adapter
val adapter = CustomAdapter(data)
// Setting the Adapter with the recyclerview
recyclerview.adapter = adapter
}
}
If you put a list object to adapter like param, you can get position of item, and use this position to start activity you want.
something like this:
add listener in your adapter
class CustomAdapter(
private val mList: List<ItemsViewModel>,
val clickListener: RecycleViewOnClickListener
) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
...
}
in your bindViewHolder:
override fun onBindViewHolder(holder:
RecyclerView.ViewHolder,position: Int) {
holder.itemView.setOnClickListener {
clickListener.onItemClick(position)
}
}
and add simple interface:
interface RecycleViewOnClickListener {
fun onItemCLick(position: Int)
}
in your activity, get model from list with your position:
class recyclerView : AppCompatActivity(), RecycleViewOnClickListener {
override fun onItemClick(pos: Int) {
val model = yourList[pos]
//start new activity
}
}
then when you init adapter:
// This will pass the ArrayList to our Adapter
val adapter = CustomAdapter(data, this)
You can do this by using high order function it will be easy
In you adapter class (pass list and high order function as callback)
class YourAdapter(private val itemList: List<Model>, private val onItemClicked: (item) -> Unit) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// your code
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onItemClicked(itemList[position])
}
In your activity/Fragment where you are assigning adapter assign in this way.
whenever item is clicked it will land here, and you can start your activity, you will get item also from adapter on which user clicked
YourAdapter(list) { item ->
//start activity
}
Note: Always write some code with question, it will be easy for us to answer you and guide you what is wrong in your code.
Happy Coding!
I'm having trouble binding my ViewHolder, and I've got two warning that I believe are related. I am trying to use Hilt to create a clickable ViewHolder, so in my SessionAdapter I am using an inner class to bind my SessionViewHolder to my RecyclerView.
First, I am struggling to understand what to return for the inner class SessionViewHolder fun bind(session: Session) { ...}. Android Studio is telling me function "bind" is never used, but I thought I used it in my onBindViewHolder?
Secondly, in my override onBindViewHolder I don't understand how I should use val session?
#AndroidEntryPoint
class SessionFragment : Fragment() {
var adapter: SessionAdapter = SessionAdapter()
private val sessionAdapter = SessionListAdapter(this::onSessionClicked)
private fun onSessionClicked(session: Session): Session {
return(session)
}
class SessionAdapter {
fun setOnClickListener() {
return(addSessionToItinerary())
}
private fun addSessionToItinerary() {
return addSessionToItinerary()
}
}
class SessionListAdapter(
private val onSessionCLicked: (Session) -> Unit,
) : ListAdapter<Session, SessionListAdapter.SessionViewHolder>(SessionItemCallback) {
inner class SessionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind() {
val textView = itemView.findViewById<TextView>(0)
fun bind(session: Session) {
textView.text = session.title
itemView.setOnClickListener {
onSessionCLicked(session)
return#setOnClickListener
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SessionViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val itemView = layoutInflater.inflate(R.layout.fragment_session_list, parent, false)
return SessionViewHolder(itemView)
}
override fun onBindViewHolder(holder: SessionViewHolder, position: Int) {
val session = getItem(position)
holder.bind()
}
}
Thank you in advance for you help. I have gotten myself confused with the recurrence of bind and session throughout my adapter.
See here in this code, you have defined two bind functions, but one is nested inside the other so it is unusable:
fun bind() {
val textView = itemView.findViewById<TextView>(0)
fun bind(session: Session) {
textView.text = session.title
itemView.setOnClickListener {
onSessionCLicked(session)
return#setOnClickListener
}
}
}
The outer bind() function is the one you are calling, and it doesn't make sense to bind nothing. This function gets a reference to a TextView, and it creates a function that is never used.
Another problem is that you are passing 0 to findViewById. There is never going to be a view with an ID of 0. You need to pass R.id.whateverYourTextViewIsNamedInYourXml.
Side note, return#onClickListener is unnecessary. If a function doesn't return anything, putting a return statement on the last line doesn't do anything.
To make it work, you should replace the above code with something like this, but replace the name of the text view with whatever ID you assigned it in your XML:
fun bind(session: Session) {
val textView = itemView.findViewById<TextView>(R.id.myTextView)
textView.text = session.title
itemView.setOnClickListener {
onSessionCLicked(session)
}
}
and then pass the session to this function when you call it.
Side note, your SessionAdapter class doesn't make any sense at all, but you're not using it for anything anyway. I would delete that.
This question already has answers here:
RecyclerView Item Click Listener the Right Way
(14 answers)
Closed 1 year ago.
I want to access a variable in an adapter class called item so that I could use it in my main activity. So far I've tried adding open before the adapter class and using object and companion object. How can I fix this problem?
class GrammarAdapter(private val context: Context, private val items: ArrayList<String>) :
RecyclerView.Adapter<GrammarAdapter.ViewHolder>() {
class ViewHolder(binding: GrammarItemRowBinding) : RecyclerView.ViewHolder(binding.root) {
val tvItem = binding.tvItemName
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
GrammarItemRowBinding.inflate(LayoutInflater.from(context), parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvItem.text = item
holder.itemView.setOnClickListener {
when (item) {
"Countable and uncountable nouns" -> {
val intent = Intent(holder.itemView.context, CountableUncountable::class.java)
intent.putExtra("clickedGrammarTopic", item)
holder.itemView.context.startActivity(intent)
}
"Singular and plural nouns" -> {
val intent = Intent(holder.itemView.context, Singular and plural nouns::class.java)
intent.putExtra("clickedGrammarTopic", item)
holder.itemView.context.startActivity(intent)
}
...
As long as items is not private you can do something like:
val item = (myRecycler.adapter as MyAdapterClass).items[position]
However thats not really the best practice and you may want to rethink how your managing your apps data perhaps you should look into ViewModels
You have to use the callback pattern via an interface
interface Callback {
fun onClickedRow(item: String)
}
In the adapter constructor
class GrammarAdapter(... , private val callback: Callback) : ...
And modify the interaction
holder.itemView.setOnClickListener {
callback.onClickedRow(item)
}
You have to implement the interface in the activity or fragment
clas MyFragment : Callback {
override fun onClickedRow(item: String) {
//do something here with your item
}
}
And instantiate your adapter with the callback as an argument
val adapter = GrammarAdapter(..., this#MyFragment) //depending on your scope it can be simple this
Now when the user clicks the method override fun onClickedRow(... is going to be triggered on the activity or fragment
I have created a simple notes app that uses some of the Android Architecture components. I am using dataBinding to set data to my recycler view. One of the functionalities is bookmarking a note and displaying it in the Bookmarks fragment. In the Bookmarks fragment a user can tap the 'unbookmark' icon to remove a bookmarked note. I have used a simple on click listener on the icon inside my Bookmarks Recycler View Adapter to achieve this. I have a boolean property in my notes entity. Inside the Bookmarks fragment I update the bookmarks boolean value to false when the user taps the icon so that it changes to a false value(meaning it's not bookmarked and does not appear in the bookmarks fragment). However, when I click the 'unbookmark' icon the recycler view still displays the notes removed from the bookmarks.
Here is my Bookmarks Adapter :
class BookmarksAdapter(private var bookmarksList: List<Note>, var context: Context):
RecyclerView.Adapter<BookmarksAdapter.BookmarkViewHolder>(), CoroutineScope {
private lateinit var job: Job
var deletedId : Int? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkViewHolder {
val layoutItemBinding: BookmarksLayoutItemBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.bookmarks_layout_item,
parent,
false)
job = Job()
return BookmarkViewHolder(layoutItemBinding.root)
}
override fun onBindViewHolder(holder: BookmarkViewHolder, position: Int) {
val bookmarkNote: Note = bookmarksList.get(position)
holder.bookmarkBinding?.setVariable(BR.bookmarkItem, bookmarkNote)
holder.bookmarkBinding?.executePendingBindings()
holder.bookmarkBinding?.imageRemoveBookmark?.setOnClickListener {
removeBookmark(bookmarkNote, position)
}
}
override fun getItemCount(): Int {
return bookmarksList.size
}
inner class BookmarkViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var bookmarkBinding: BookmarksLayoutItemBinding? = DataBindingUtil.bind(itemView)
}
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
fun deleteNote(note : Note) {
launch(Dispatchers.IO) {
note.isBookmarked = false
NotesDatabase(context).getDao().updateNote(note)
}
val pos = bookmarksList.toMutableList().indexOf(note)
bookmarksList.toMutableList().removeAt(pos)
notifyItemRemoved(pos)
}
// Remove a bookmark
fun removeBookmark(bookmarkNote: Note, removedPos: Int) {
bookmarkNote.isBookmarked = false
launch (Dispatchers.IO){
NotesDatabase(context).getDao().updateNote(bookmarkNote)
withContext(Dispatchers.Main) {
bookmarksList.toMutableList().removeAt(removedPos)
notifyItemRemoved(removedPos)
notifyItemRangeChanged(removedPos, 1)
}
}
}
}
Below is my Bookmarks View Model:
class BookmarksViewModel(application: Application) : AndroidViewModel(application) {
val myContext: Context = application.applicationContext
private var _bookmarksList = MutableLiveData<List<Note>>()
val bookmarksList : LiveData<List<Note>>
get() = _bookmarksList
init{
viewModelScope.launch {
_bookmarksList.value = getNotes()
}
}
suspend fun getNotes() : List<Note> = NotesDatabase(myContext).getDao().getBookmarkedNotes()
}
Here is my Bookmarks Fragment:
class BookmarksFragment : BaseFragment(){
private lateinit var bookmarksBinding: FragmentBookmarksBinding
private lateinit var bookmarksViewModel: BookmarksViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
bookmarksBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_bookmarks, container, false)
bookmarksViewModel = ViewModelProvider(this).get(BookmarksViewModel::class.java)
// Observe the list of bookmarks
bookmarksViewModel.bookmarksList.observe(viewLifecycleOwner, { bList ->
bookmarksBinding.listOfBookmarks = bList
})
return bookmarksBinding.root
}
}
Here is my Notes Entity:
val bookmarkSate: Boolean
get() = true
#Entity
data class Note(
val title : String,
val note : String,
var isBookmarked : Boolean = false
) : Serializable{
#PrimaryKey(autoGenerate = true)
var noteID : Int = 0
}
Here is my Notes DAO:
#Dao
interface NoteDao {
#Insert
suspend fun saveNote(note : Note)
#Query("SELECT * FROM note ORDER BY noteID DESC")
suspend fun getAllNotes() : List<Note>
#Query("SELECT * FROM note WHERE isBookmarked")
suspend fun getBookmarkedNotes() : List<Note>
// add multiple notes
#Insert
suspend fun addMultipleNotes(vararg note: Note)
#Update
suspend fun updateNote(note: Note)
#Delete
suspend fun deleteNote(note : Note)
}
I am stuck on how to achieve the desired functionality of 'unbookmarking' a note and making it 'disappear 'from the bookmarks fragment. Kindly anyone who can help out?
There are a few problems and possible solutions I see here:
1-) In your adapter, in your delete method, you have this piece of code:
val pos = bookmarksList.toMutableList().indexOf(note)
bookmarksList.toMutableList().removeAt(pos)
notifyItemRemoved(pos)
The problem here is that toMutableList() doesn't change your bookmarksList as mutable, it returns a new mutable list filled with the items in your bookmarkList, a mutable version of your list, but a new list! So you remove the item from this new list, and not from the original list. Simply correcting this could fix the problem. (You can define the bookmarksList as a mutable list from the beginning)
2-) You seem to observe the list from your viewmodel, it is livedata in your viewmodel. But it is not defined as livedata in your dao. So you are not observing the changes in the database. So an alternative solution could be to observe the changes from the database by wrapping your list within a livedata in your dao.
#Query("SELECT * FROM note WHERE isBookmarked")
suspend fun getBookmarkedNotes() : LiveData<List<Note>>
But this second one would require some additional changes in your code.
I've tried to build a recyclerview with an adapter class and cardview layout however even though the code runs I cannot see the list with cardview (I just get a blank recyclerview)?
SpeakerAdapter.kt
class SpeakerAdapter (val speakers: ArrayList<String>): RecyclerView.Adapter<SpeakerAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val speakerName: TextView = itemView.findViewById(R.id.name)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.speakerName.text = speakers[position]
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val View = LayoutInflater.from(parent.context).inflate(R.layout.speaker_list, parent, false)
return ViewHolder(View)
}
override fun getItemCount() = speakers.size
}
Speaker.kt
class Speaker : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
setContentView(R.layout.activity_speaker)
val speakers : ArrayList<String> = ArrayList()
for (i in 1..100){
speakers.add("Speaker 1 #$i")
recyclerView_speaker.layoutManager = LinearLayoutManager(this)
recyclerView_speaker.adapter = SpeakerAdapter(speakers)
}
}}
It's a bit hard to debug without the xml. But here goes nothing:
I think it'd be worth a shot modifying your view holder class. The reference for the text view is being set when you init the view holder and the layout may not be inflated yet. Try finding the reference when you bind the data to the view to avoid this issue.
Try this:
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(name: String?) {
itemView. name.text = name
}
}
and then modify your adapter to call this method
class SpeakerAdapter (val speakers: ArrayList<String>): RecyclerView.Adapter<SpeakerAdapter.ViewHolder>() {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(speakers[position])
}
}
PS.: it'd be a good idea to call your view holder something else. ViewHolder is a name that is already used by the android SDK. Ex.: SpeakerViewholder