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.
Related
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
Inside ExamAcitivy :
passExamState always returns false even though its value changes to true after the setOnClickListener, when called in the adapter I need it to return the state acordingly. How do i propely pass the examState and its value to the adapter?
class ExamActivity : AppCompatActivity(){
private var examState = false
private val questionData = QuestionData()
private var questionAdapter = QuestionAdapter(questionData)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exam)
questionViewPager.adapter = questionAdapter
endExam.setOnClickListener {
examState=true
}
}
fun passExamState() : Boolean {
return examState
}
}
Inside My ViewPager2Adapter :
class QuestionAdapter(private val questionData: QuestionData) :
RecyclerView.Adapter<QuestionAdapter.QuestionViewPagerViewHolder>() {
inner class QuestionViewPagerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
//itemviews
init {
if (!ExamActivity().passExamState()) {
getthis()
}else{
getthat()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionViewPagerViewHolder {
return QuestionViewPagerViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_question, parent, false)
)
}
override fun onBindViewHolder(holder: QuestionViewPagerViewHolder, position: Int) {
//itemviews
if (!ExamActivity().passExamState()){
dothis()
}else{
dothat()
}
}
//itemcount
}
The problem is that you're creating a new ExamActivity instance and using that instance's property to check passExamState(). In this line, which appears twice in your code:
if (!ExamActivity().passExamState()) {
you are calling the ExamActivity constructor to construct a new Activity, so you are not checking the state of the actual Activity that is on the screen.
The proper, encapsulated way to do this is to create the appropriate Boolean property in your Adapter, and allow your Activity to modify that property. This way of organizing the code reduces inter-dependence. The Adapter doesn't have to know about the existence of the Activity. For example:
//In ExamActivity:
endExam.setOnClickListener {
examState = true // If you even need to track this property in the Activity.
// Maybe you can remove the property entirely.
questionAdapter.examState = true
}
// Adapter class:
class QuestionAdapter(private val questionData: QuestionData) :
RecyclerView.Adapter<QuestionAdapter.QuestionViewPagerViewHolder>() {
var examState = false
inner class QuestionViewPagerViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
init {
if (!examState) { // and the same in onBindViewHolder
getthis()
}else{
getthat()
}
}
}
//...
}
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'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.
I'm trying to put a recyclerview which get its data from room. My function getAllHomework returns LiveData<List<Homework>>, but when I tried to set the return value to the recyclerview adapter, it will always return this error
Type Mismatch.
Required: List<Homework>
Found: List<Homework>?
Here's my HomeworkViewModel class which has the function getAllHomework looks like:
class HomeworkViewModel : ViewModel() {
private var matrixNumber: String? = null
private var schoolID: Int = 0
lateinit var listAllHomework: LiveData<List<Homework>>
lateinit var homeworkRepository: HomeworkRepository
fun init(params: Map<String, String>) {
schoolID = Integer.parseInt(params["schoolID"])
homeworkRepository = HomeworkRepository()
listAllHomework = homeworkRepository.getAllHomework(1, "2018")
}
fun getAllHomework(): LiveData<List<Homework>>{
return listAllHomework
}
}
And below is the part in my Homework activity that tries to set the value into recyclerview adapter but will always return the type mismatch error.
class Homework : AppCompatActivity(), LifecycleOwner {
lateinit var linearLayoutManager: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.homework)
linearLayoutManager = LinearLayoutManager(this)
rvHomeworks.layoutManager = linearLayoutManager
var adapter = AdapterHomework(this)
rvHomeworks.adapter = adapter
var homeworkViewModel = ViewModelProviders.of(this).get(HomeworkViewModel::class.java)
homeworkViewModel.init(params)
homeworkViewModel.getAllHomework().observe(this, Observer {
allHomework -> adapter.setHomeworkList(allHomework)
})
}
}
The line allHomework -> adapter.setHomeworkList(allHomework) above will show the Type Mismatch error I mentioned above.
Here's how my AdapterHomework looks like:
class AdapterHomework(context: Context): RecyclerView.Adapter<AdapterHomework.ViewHolder>() {
lateinit var homeworkList: List<Homework>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder{
val v = LayoutInflater.from(parent.context).inflate(R.layout.rv_homeworks, parent, false)
return ViewHolder(v);
}
#JvmName("functionToSetTheHomeworkList")
fun setHomeworkList(myHomeworkList: List<Homework>){
homeworkList = myHomeworkList
notifyDataSetChanged()
}
}
I could not find where in my code did I ever return List<Homework>? instead of List<Homework>.
This has actually nothing to do with Room, but with how LiveData was designed - specifically Observer class:
public interface Observer<T> {
void onChanged(#Nullable T t);
}
as you can see T (in your case List<Homework>) is marked as #Nullable therefore you will always get it's Kotlin equivalent List<Homework>? as a parameter of onChanged in your Observer implementation.
I would recommend changing setHomeworkList to something like this:
fun setHomeworkList(myHomeworkList: List<Homework>?){
if(myHomeworkList != null){
homeworkList = myHomeworkList
notifyDataSetChanged()
}
}
You can also use let function for that like this:
fun setHomeworkList(myHomeworkList: List<Homework>?){
myHomeworkList?.let {
homeworkList = it
notifyDataSetChanged()
}
}