I am getting the all views but when I click the button and scroll down at that time the it will be unchecked. I added the removeall views on the onBindViewHolder. because the radiobuttons are generated infinite times. Here I shared the code please check it.
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.radioGrp.setOrientation(RadioGroup.VERTICAL)
holder.radioGrp.removeAllViews()
holder.bindView(position)
}
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val radioGrp = itemView.findViewById<RadioGroup>(R.id.radio_group)
fun bindView(position: Int) {
itemView.tv_question.text = feedback[position].questions
var newAnswer = feedback[position].answser as ArrayList<String>
if (newAnswer.isEmpty()) {
itemView.linear2.visibility = View.VISIBLE
} else {
newAnswer.forEach {
itemView.linear2.visibility = View.GONE
val rb = RadioButton(context)
rb.text = it
rb.id = position
radioGrp.addView(rb)
rb?.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener {
buttonView, isChecked ->
if (isChecked) {
rb.isChecked = true
examinationListener.addAnswer(names)
} else {
rb.isChecked = false
examinationListener.removeAnswer(names as String)
}
})
}
}
}
}
You need to track the checked state of your answers externally to the views and apply the state when binding the views. Assuming the answers for each list item are all unique Strings, you could use a Map to store the states.
// In your adapter:
val answerStates = mutableMapOf<Int, MutableMap<String, Boolean>>()
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val radioGrp = itemView.findViewById<RadioGroup>(R.id.radio_group)
fun bindView(position: Int) {
itemView.tv_question.text = feedback[position].questions
val newAnswer = feedback[position].answser as ArrayList<String>
if (newAnswer.isEmpty()) {
itemView.linear2.visibility = View.VISIBLE
} else {
itemView.linear2.visibility = View.GONE
// Lazily create answer states map for this list item
val answerStates = answerStates[position]
?: mutableMapOf<String, Boolean>().also { answerStates[position] = this }
newAnswer.forEach {
val rb = RadioButton(context)
rb.text = it
rb.id = position
rb.checked = answerStates[it] ?: false // Bind last known state, default false
radioGrp.addView(rb)
rb?.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener {
buttonView, isChecked ->
answerStates[it] = isChecked // Save state
if (isChecked) {
rb.isChecked = true
examinationListener.addAnswer(names)
} else {
rb.isChecked = false
examinationListener.removeAnswer(names as String)
}
})
}
}
}
}
If your data can change, this gets more complicated. You would need to store this Boolean in your actual Feedback class so it can be restored when there is fresh data.
Related
I created a scrollView programmaticaly that contains 20 views each with an image and a text.
I have two questions :
1 - is the id assignment correct and is my setOnClickListener correct?
2 - By which method onClick can I know which view of the scrollView the user has clicked?
See my code below
private var integerList: MutableList<Int>? = mutableListOf()
private var cellNo: MutableList<String>? = mutableListOf()
private var sv_mvmtChoosed = ""
private fun showSpinner() {
/* SCROllL VIEW */
var linearLayout: LinearLayout? = null
linearLayout = findViewById(R.id.linear1)
val layoutInflater = LayoutInflater.from(this)
var randIndex = 0
for (posIndex in 0..19) {
val rand = Random()
randIndex = rand.nextInt(20)
while (integerList!!.contains(randIndex)) {
randIndex = rand.nextInt(20)
}
integerList!!.add(randIndex)
// Create the view...
val view: View = layoutInflater.inflate(R.layout.scroll_bckgrnd, linearLayout, false)
// give it an id
view.id = generateViewId()
view.setOnClickListener(this)
cellNo!!.add(view.id.toString())
println(cellNo)
//... then populate it with image and text
val iv = view.findViewById<ImageView>(R.id.iv)
iv.setImageResource(sv_photoImage[randIndex])
val tv = view.findViewById<TextView>(R.id.tv)
tv.text = sv_photoName[randIndex]
linearLayout?.addView(view)
}
// which view the user did select?
fun onClick(view: View?) {
when (view!!.id) {
??? -> doSomething
}
}
}
Any idea to get me back on track will be welcome.
Its probably better to make a new OnClickListener for every view.
view.setOnClickListener(this)
needs to be this
view.setOnClickListener {
// do something
}
or
view.setOnClickListener(createOnClickListner())
fun createOnClickListner():View.OnClickListener{
return object :View.OnClickListener{
override fun onClick(view : View) {
//do something with the view that was clicked
}
}
}
Thanks a lot avalerio.
I finally found a solution as follow :
I replaced :
// give it an id
view.id = generateViewId()
view.setOnClickListener(this)
cellNo!!.add(view.id.toString())
println(cellNo)
with :
// give it an id
view.id = posIndex
view.setOnClickListener(this)
then I did this :
// the onClickListener for my 20 images/text
override fun onClick(view: View?) {
when (view!!.id) {
// Now de position clicked on the ScrollView
in 0..19 -> didHeSucceeded(view!!.id)
}
}
And use the result:
private fun didHeSucceeded(scrllPos: Int) {
// TODO: close de scrollView, how ? :-)
spinnerChoice = nameOfTechScrollVw[scrllPos]
succes = (!allreadyChoosedArray.contains(spinnerChoice)) && (currentArray.contains(spinnerChoice
))
if (succes) {
...
...
}
It works perfectly
apologies for my limited knowledge of programming and any sloppiness. I have a reyclerview with alarm objects that I can add and it creates them. When I add say 4 alarms, and delete three of them. The last alarms checkbox is checked by itself. I can not in anyway use the checkbox.setChecked() method for some reason. android studio is not recognizing it, if anyone could please let me know why that is. Also if you know of a solution to the auto check on the last alarm object please.
package com.example.alarmclock
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Checkable
import android.widget.EditText
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doBeforeTextChanged
import androidx.recyclerview.widget.RecyclerView
import java.security.Key
class AlarmAdapter (private val alarmList: MutableList<Alarm>) : RecyclerView.Adapter<AlarmAdapter.ViewHolder>() {
//start viewholder
inner class ViewHolder(alarm: View) : RecyclerView.ViewHolder(alarm) {
val alarmLabel = itemView.findViewById<EditText>(R.id.alarmLabel)
val editTextTime = itemView.findViewById<EditText>(R.id.editTextTime)
val textView1 = itemView.findViewById<TextView>(R.id.textView1)
val deleteCheckBox = itemView.findViewById<Button>(R.id.deleteAlarmCheckBox)
//val deleteButton = itemView.findViewById<Button>(R.id.deleteAlarmButton)
//val addButton = itemView.findViewById<Button>(R.id.addAlarmButton)
val mondayCheckBox = itemView.findViewById<Button>(R.id.mondayCheckBox)
val tuesdayCheckBox = itemView.findViewById<Button>(R.id.tuesdayCheckBox)
val wednesdayCheckBox = itemView.findViewById<Button>(R.id.wednesdayCheckBox)
val thursdayCheckBox = itemView.findViewById<Button>(R.id.thursdayCheckBox)
val fridayCheckBox = itemView.findViewById<Button>(R.id.fridayCheckBox)
val saturdayCheckBox = itemView.findViewById<Button>(R.id.saturdayCheckBox)
val sundayCheckBox = itemView.findViewById<Button>(R.id.sundayCheckBox)
val amCheckBox = itemView.findViewById<Button>(R.id.amCheckBox)
val pmCheckBox = itemView.findViewById<Button>(R.id.pmCheckBox)
}//end viewholder
fun addAlarm (alarm: Alarm) {
alarmList.add(alarm)
notifyItemInserted(alarmList.size - 1)
}
fun returnAlarmList (): MutableList<Alarm> {
return alarmList
}
fun removeAlarms() {
alarmList.removeAll {
alarm -> alarm.deleteCheck == true
}
//notifyDataSetChanged()
}
fun deleteAlarm (deletedAlarmList: List<Int> ) {
val deletedListIterator = deletedAlarmList.iterator()
val alarmListIterator = alarmList.iterator()
while (deletedListIterator.hasNext()){
while (alarmListIterator.hasNext()){
if (deletedListIterator.next() == alarmListIterator.next().alarmId){
alarmList.remove(alarmListIterator.next())
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val alarmView = inflater.inflate(R.layout.alarms, parent, false)
return ViewHolder(alarmView)
}
override fun getItemCount(): Int {
return alarmList.size
}
override fun onBindViewHolder(holder: AlarmAdapter.ViewHolder, position: Int) {
val alarm: Alarm = alarmList[position]
val alarmLabel = holder.alarmLabel
var textView1 = holder.textView1
var editTextTime = holder.editTextTime
var mondayCheckBox = holder.mondayCheckBox
var tuesdayCheckBox = holder.tuesdayCheckBox
var wednesdayCheckBox = holder.wednesdayCheckBox
var thursdayCheckBox = holder.thursdayCheckBox
var fridayCheckBox = holder.fridayCheckBox
var saturdayCheckBox = holder.saturdayCheckBox
var sundayCheckBox = holder.sundayCheckBox
var amCheckBox = holder.amCheckBox
var pmCheckBox = holder.pmCheckBox
var deleteAlarmCheckBox = holder.deleteCheckBox
var lastCharacter = ""
var secondLastCharacter = ""
deleteAlarmCheckBox.setOnClickListener {
alarm.deleteCheck = !alarm.deleteCheck
}
alarmLabel.doAfterTextChanged {
alarm.alarmLabel = alarmLabel.text.toString()
textView1.text = alarm.alarmLabel
}
editTextTime.doAfterTextChanged {
//lastCharacter = editTextTime.text.get(editTextTime.text.length-1).toString()
textView1.text = lastCharacter
if (editTextTime.text.length == 2 && secondLastCharacter != ":"){
//if (lastCharacter != ":") {
editTextTime.setText(editTextTime.text.toString().plus(":"))
editTextTime.setSelection(editTextTime.text.length)
//}
}
editTextTime.doBeforeTextChanged { _, _, _, _ ->
if (editTextTime.length() != 0) {
secondLastCharacter = editTextTime.text.get(editTextTime.text.length - 1).toString()
}
}
if (editTextTime.text.length == 5 ){
alarm.hour = editTextTime.text.get(0).toString().plus(editTextTime.text.get(1).toString())
if (alarm.hour.toInt() < 10) alarm.hour = "0".plus(alarm.hour)
///////
var inputedTimeList = editTextTime.text.toList()
val timeIterator = inputedTimeList.iterator()
}
}
mondayCheckBox.setOnClickListener {
alarm.monday = !alarm.monday
textView1.text = alarm.monday.toString()
}
tuesdayCheckBox.setOnClickListener {
alarm.tuesday = !alarm.tuesday
}
wednesdayCheckBox.setOnClickListener {
alarm.wednesday = !alarm.wednesday
}
thursdayCheckBox.setOnClickListener {
alarm.thursday = !alarm.thursday
}
fridayCheckBox.setOnClickListener {
alarm.friday = !alarm.friday
}
saturdayCheckBox.setOnClickListener {
alarm.saturday = !alarm.saturday
}
sundayCheckBox.setOnClickListener {
alarm.sunday = !alarm.sunday
}
amCheckBox.setOnClickListener {
alarm.amPm = !alarm.amPm
}
}
}
The answer is quite simple, RecyclerView items are reused, so be sure that you set the all the values onBindViewHolder, because after your item is deleted, the actual view is not, so previously set values might be preset although they are not correct according to your data.
The easiest way would be to have isChecked Boolean value store in the Alarm object, onBindViewHolder always set the isChecked param on the Checkbox according to the value returned from the Alarm and when you change the isChecked inside the Checkbox listener - make sure you also update the value inside the Alarm object.
Another solution would be calling notifyDatasetChanged() on the RecyclerView, but it's definitely not the best solution especially if you have dynamic row deletion (and possibly a neat animation).
P.S. consider using viewBinding in your project, it will save you time writing all that ugly findViewById code :)))
I have an issue with the implementation of a basic functionality of a quiz based application. Briefly, I have "questions" and "answers" in an ArrayList<Question> (each Question has its own ArrayList<Answer>, 3 for this example) that I used to populate an adapter of a RecyclerView in a fragment managed with MVVM.
This is my onBindViewHolder function:
...
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
...
val curQuest = myDataset[position]
val shuffledAnswers = curQuest.answers
holder.ans1.text = shuffledAnswers[0].answer_text
holder.ans1.setOnClickListener {
it.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.selected_ans_quiz)
holder.ans2.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
holder.ans3.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
}
holder.ans2.text = shuffledAnswers[1].answer_text
holder.ans2.setOnClickListener {
it.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.selected_ans_quiz)
holder.ans1.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
holder.ans3.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
}
holder.ans3.text = shuffledAnswers[2].answer_text
holder.ans3.setOnClickListener {
it.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.selected_ans_quiz)
holder.ans2.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
holder.ans1.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
}
}
Everything works well but the selection of the answer: if I select an answer of the first question (myDataset[0]), the view changes also for the N+0 element in the list, where N is the max chunck of items loaded by the RecyclerView
How can I set the right attributes in a RecyclerView for each sub-element without propagation after the Nth element loaded by onBindViewHolder?
Can it be fixed or should I have to change the implementation way?
EDIT: I just tried an alternative but with the same result, using an interface:
QuizAdapter.kt
...
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
...
val curQuest = myDataset[position]
val shuffledAnswers = curQuest.answers
holder.ans1.text = shuffledAnswers[0].answer_text
holder.ans1.setOnClickListener {
mCallback.onClick(shuffledAnswers[0],position,it,holder.ans2,holder.ans3)
}
holder.ans2.text = shuffledAnswers[1].answer_text
holder.ans2.setOnClickListener {
mCallback.onClick(shuffledAnswers[1],position,it,holder.ans1,holder.ans3)
}
holder.ans3.text = shuffledAnswers[2].answer_text
holder.ans3.setOnClickListener {
mCallback.onClick(shuffledAnswers[2],position,it,holder.ans2,holder.ans1)
}
interface OnItemClickListener{
fun onClick(answerSelected: Answer?, questPosition: Int, selectedItemView: View, firstItemView: View, secondItemView: View)
}
QuizFragment.kt
...
mAdapter = QuizAdapter(this)
listQuestions.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(activity)
adapter = mAdapter
itemAnimator = DefaultItemAnimator()
}
mAdapter.setOnItemClickListener(object : QuizAdapter.OnItemClickListener{
override fun onClick(answerSelected: Answer?,
questPosition: Int,
selectedItemView: View,
firstItemView: View,
secondItemView: View) {
Snackbar.make(selectedItemView, "Your answer for $questPosition is ${answerSelected!!.isCorrect}.",
Snackbar.LENGTH_SHORT)
.setAction("Action", null).show()
selectedItemView.background = ContextCompat.getDrawable(requireContext(),R.drawable.selected_ans_quiz)
firstItemView.background = ContextCompat.getDrawable(requireContext(),R.drawable.default_ans_quiz)
secondItemView.background = ContextCompat.getDrawable(requireContext(),R.drawable.default_ans_quiz)
quizViewModel.chosenAnswers[questPosition] = answerSelected
}
})
fab_endQuiz.setOnClickListener { view ->
Snackbar.make(view, quizViewModel.chosenAnswers.toString(), Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
//parentFragment?.findNavController()?.navigate(R.id.action_quizFragment_to_nav_home)
}
As you can see, with a FAB I can test the correctness of the selected answers and it seems ok, the map is filled with the user choices, even if there's a change in selection (map position, representing the question, is modified correctly).
But background colors remain mixed in different positions (the current ones visible in the RecyclerView plus others ahead), as said before.
If it can be useful, I found a solution.
I added a boolean value to Answer class so that the RecyclerView has to check that when builds its list in xml, mixing to what I've done before
QuizAdapter.kt
...
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
...
val curQuest = myDataset[position]
val shuffledAnswers = curQuest.answers
holder.ans1.text = shuffledAnswers[0].answer_text
if(shuffledAnswers[0].isSelected)
holder.ans1.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.selected_ans_quiz)
else
holder.ans1.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
holder.ans1.setOnClickListener {
shuffledAnswers[0].isSelected = true
shuffledAnswers[1].isSelected = false
shuffledAnswers[2].isSelected = false
mCallback.onClick(shuffledAnswers[0],position,it,holder.ans2,holder.ans3)
}
holder.ans2.text = shuffledAnswers[1].answer_text
if(shuffledAnswers[1].isSelected)
holder.ans2.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.selected_ans_quiz)
else
holder.ans2.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
holder.ans2.setOnClickListener {
shuffledAnswers[0].isSelected = false
shuffledAnswers[1].isSelected = true
shuffledAnswers[2].isSelected = false
mCallback.onClick(shuffledAnswers[1],position,it,holder.ans1,holder.ans3)
}
holder.ans3.text = shuffledAnswers[2].answer_text
if(shuffledAnswers[2].isSelected)
holder.ans3.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.selected_ans_quiz)
else
holder.ans3.background = ContextCompat.getDrawable(parentFragment.requireContext(),R.drawable.default_ans_quiz)
holder.ans3.setOnClickListener {
shuffledAnswers[0].isSelected = false
shuffledAnswers[1].isSelected = false
shuffledAnswers[2].isSelected = true
mCallback.onClick(shuffledAnswers[2],position,it,holder.ans1,holder.ans2)
}
}
interface OnItemClickListener{
fun onClick(answerSelected: Answer?, questPosition: Int, selectedItemView: View, firstItemView: View, secondItemView: View)
}
QuizFragment.kt
mAdapter.setOnItemClickListener(object : QuizAdapter.OnItemClickListener{
override fun onClick(answerSelected: Answer?,
questPosition: Int,
selectedItemView: View,
firstItemView: View,
secondItemView: View) {
selectedItemView.background = ContextCompat.getDrawable(requireContext(),R.drawable.selected_ans_quiz)
firstItemView.background = ContextCompat.getDrawable(requireContext(),R.drawable.default_ans_quiz)
secondItemView.background = ContextCompat.getDrawable(requireContext(),R.drawable.default_ans_quiz)
quizViewModel.chosenAnswers[questPosition] = answerSelected
}
})
I want to display information that RecyclerView have no items, but I can't check if Firestore collection is empty. How to set some kind of listener which check if RecyclerView have items or not?
I'm assuming you're using Firebase UI (otherwise you would already have a query callback to hook into). In your FirestoreRecyclerAdapter, you can override onDataChanged & onError:
typealias DataChangedListener = (count: Int) -> Unit
typealias ErrorListener = (error: FirebaseFirestoreException) -> Unit
class MyAdapter(
options: FirestoreRecyclerOptions<MyModel>,
private val onDataChangedListener: DataChangedListener = {},
private val onErrorListener: ErrorListener = {}
) : FirestoreRecyclerAdapter<MyModel, MyViewHolder>(options) {
...
// Notify Activity/Fragment/ViewModel
override fun onDataChanged() =
onDataChangedListener.invoke(itemCount)
// Notify Activity/Fragment/ViewModel
override fun onError(e: FirebaseFirestoreException) =
onErrorListener.invoke(e)
}
You can use it like this:
recyclerView.adapter = MyAdapter(
options,
{ count -> showHideNoData(count > 0) },
{ error -> showError(error) }
)
...
fun showHideNoData(haveData: Boolean) {
recyclerView.isVisible = haveData
noDataView.isVisible = !haveData
errorView.isVisible = false
}
fun showError(error: FirebaseFirestoreException) {
recyclerView.isVisible = false
noDataView.isVisible = false
errorView.isVisible = true
// Logging & other UI changes
}
If it will be useful here is my solution. I simply called this function in the fragment where RecyclerView lives:
private fun setUpRecyclerView() {
val viewManagerPortrait = LinearLayoutManager(activity)
val viewManagerLandscape = GridLayoutManager(activity, 3)
val query = docRef.orderBy("title", Query.Direction.ASCENDING)
query.addSnapshotListener { p0, _ ->
if (p0 != null) {
if(p0.size() > 0) {
emptyAds.visibility = View.GONE;
listItems.visibility = View.VISIBLE
}else {
emptyAds.visibility = View.VISIBLE;
listItems.visibility = View.GONE
}
}
}
val options = FirestoreRecyclerOptions.Builder<Item>()
.setQuery(query,Item::class.java)
.setLifecycleOwner(this)
.build()
mAdapter = ItemCardsAdapter(this,options)
listItems.apply {
setHasFixedSize(true)
// use a linear layout manager if portrait, grid one else
layoutManager = if(activity!!.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
viewManagerLandscape
else
viewManagerPortrait
adapter = mAdapter
}
}
As you can see the if statement (inside the SnapShotListener) on size checks whether the database at that reference is empty, showing a message in the layout instead of the RecyclerView.
How does one properly send data to child adapter in a fragment?
I'm basically trying to implement an Instagram like comments-section, e.g. a bunch of comments that can each have more comments (replies).
To do that, I use one main recyclerView + main adapter, which instances are retained in my fragment, and within the main adapter I bind the children comments (recyclerView + adapter).
Adding comments to the main adapter is easy since the object is always available in the fragment, so I just call mainAdapter.addComments(newComments):
MainAdapter
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
notifyItemRangeInserted(0, newComments.size)
}
But how to call addComments of one particular nested-rV? I read I should not save the adapter instances and only use positions.
I'm trying to do that in my Fragment as follows:
val item = rVComments.findViewHolderForItemId(mAdapter.itemId)!!.itemView
val adapt = item.rVReplies.adapter as ChildCommentsAdapter
adapt.addComment(it.data.comment)
But that doesn't work very well: since we have only RecyclerViews, that particular ViewHolder is often already recycled if the user scrolled after posting or fetching items, which leads to a NullPointerException.
Hence the initial question: how does one properly interact with nested recyclerviews and their adapter? If the answer is via Interface, please provide an example as I've tried it without success since I shouldn't save adapter objects.
You can achieve that using a single multi-view type adapter by placing the comments
as part of the parent item, with that, you add the child items below the parent item and call notifyItemRangeInserted.
That way you don't have to deal with most of the recycling issues.
When you want to update a comment you just update the comment inside the parent item and call notifyItemChanged.
If you want I created a library that can generate that code for you in compile time.
It supports the exact case you wanted and much more.
Using #Gil Goldzweig's suggestion, here is what I did: in case of an Instagram like comments' system with replies, I did use a nested recyclerView system. It just makes it easier to add and remove items. However, as for the question How does one properly send data to child adapter in a fragment? You don't. It gets super messy. From my fragment, I sent the data to my mainAdapter, which in turn sent the data to the relevant childAdapter. The key to make it smooth is using notifyItemRangeInserted when adding a comment to the mainAdapter and then notifyItemChanged when adding replies to a comment. The second event will allow sending data to the child adapter using the payload. Here's the code in case other people are interested:
Fragment
class CommentsFragment : androidx.fragment.app.Fragment(), Injectable,
SendCommentButton.OnSendClickListener, CommentsAdapter.Listener {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(CommentsViewModel::class.java)
}
private val searchViewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(SearchViewModel::class.java)
}
private val mAdapter = CommentsAdapter(this)
private var contentid: Int = 0 //store the contentid to process further posts or requests for more comments
private var isLoadingMoreComments: Boolean = false //used to check if we should fetch more comments
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_comments, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//hide the action bar
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.GONE
contentid = arguments!!.getInt("contentid") //argument is mandatory, since comment is only available on content
ivBackArrow.setOnClickListener{ activity!!.onBackPressed() }
viewModel.initComments(contentid) //fetch comments
val layoutManager = LinearLayoutManager(this.context)
layoutManager.stackFromEnd = true
rVComments.layoutManager = layoutManager
mAdapter.setHasStableIds(true)
rVComments.adapter = mAdapter
setupObserver() //observe initial comments response
setupSendCommentButton()
post_comment_text.setSearchViewModel(searchViewModel)
setupScrollListener(layoutManager) //scroll listener to load more comments
iVCancelReplyTo.setOnClickListener{
//reset ReplyTo function
resetReplyLayout()
}
}
private fun loadMoreComments(){
viewModel.fetchMoreComments(contentid, mAdapter.itemCount)
setupObserver()
}
/*
1.check if not already loading
2.check scroll position 0
3.check total visible items != total recycle items
4.check itemcount to make sure we can still make request
*/
private fun setupScrollListener(layoutManager: LinearLayoutManager){
rVComments.addOnScrollListener(object: RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = rVComments.childCount
val totalItemCount = layoutManager.itemCount
val pos = layoutManager.findFirstCompletelyVisibleItemPosition()
if(!isLoadingMoreComments && pos==0 && visibleItemCount!=totalItemCount && mAdapter.itemCount%10==0){
//fetch more comments
isLoadingMoreComments = true
loadMoreComments()
}
}
})
}
private fun setupSendCommentButton() {
btnSendComment.setOnSendClickListener(this)
}
override fun onSendClickListener(v: View?) {
if(isInputValid(post_comment_text.text.toString())) {
val isReply = mAdapter.commentid!=null
viewModel.postComment(post_comment_text.text.toString(), mAdapter.commentid?: contentid, isReply) //get reply ID, otherwise contentID
observePost()
post_comment_text.setText("")
btnSendComment.setCurrentState(SendCommentButton.STATE_DONE)
}
}
override fun postCommentAsReply(username: String) {
//main adapter method to post a reply
val replyText = "${getString(R.string.replyingTo)} $username"
tVReplyTo.text = replyText
layoutReplyTo.visibility=View.VISIBLE
post_comment_text.requestFocus()
}
override fun fetchReplies(commentid: Int, commentsCount: Int) {
//main adapter method to fetch replies
if(!isLoadingMoreComments){ //load one series at a time
isLoadingMoreComments = true
viewModel.fetchReplies(commentid, commentsCount)
viewModel.replies.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
isLoadingMoreComments = false
}
Resource.Status.SUCCESS -> {
isLoadingMoreComments = false
mAdapter.addReplies(mAdapter.replyCommentPosition!!, it.data)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}
}
})
}
}
private fun isInputValid(text: String): Boolean = text.isNotEmpty()
private fun observePost(){
viewModel.postComment.observe(this, Observer<Resource<PostCommentResponse>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
if(it.data.asReply){
//dispatch comment to child adapter via main adapter
mAdapter.addReply(mAdapter.replyCommentPosition!!, it.data.comment)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}else{
mAdapter.addComment(it.data.comment)
}
resetReplyLayout()
//showProgressBar(false)
}
}
})
}
private fun setupObserver(){
viewModel.comments.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
isLoadingMoreComments = false
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
mAdapter.addComments(it.data)
isLoadingMoreComments = false
//showProgressBar(false)
}
}
})
}
private fun resetReplyLayout(){
layoutReplyTo.visibility=View.GONE
mAdapter.replyCommentPosition = null
mAdapter.commentid = null
}
override fun onStop() {
super.onStop()
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.VISIBLE
}
}
MainAdapter
class CommentsAdapter(private val listener: Listener) : RecyclerView.Adapter<CommentsAdapter.ViewHolder>(), ChildCommentsAdapter.ChildListener {
//method from child adapter
override fun postChildReply(replyid: Int, username: String, position: Int) {
commentid = replyid
replyCommentPosition = position
listener.postCommentAsReply(username)
}
interface Listener {
fun postCommentAsReply(username: String)
fun fetchReplies(commentid: Int, commentsCount: Int=0)
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
private var comments = mutableListOf<Comment>()
private var repliesVisibility = mutableListOf<Boolean>() //used to store visibility state for replies
var replyCommentPosition: Int? = null //store the main comment's position
var commentid: Int? = null //used to indicate which comment is replied to
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_comment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val comment = comments[position]
with(holder.view) {
//reset visibilities (rebinding purpose)
rVReplies.visibility = View.GONE
iVMoreReplies.visibility = View.GONE
tVReplies.visibility = View.GONE
content.loadUserPhoto(comment.avatarThumbnailURL)
text.setCaptionText(comment.username!!, comment.comment)
tvTimestamp.setTimeStamp(comment.timestamp!!)
val child = ChildCommentsAdapter(
//we pass parent commentid and position to child to be able to pass it again on click
this#CommentsAdapter, comments[holder.adapterPosition].id!!, holder.adapterPosition
)
val layoutManager = LinearLayoutManager(this.context)
rVReplies.layoutManager = layoutManager
rVReplies.adapter = child
//initial visibility block when binding the viewHolder
val txtMore = this.resources.getString(R.string.show_more_replies)
if(comment.repliesCount>0) {
tVReplies.visibility = View.VISIBLE
if (repliesVisibility[position]) {
//replies are to be shown directly
rVReplies.visibility = View.VISIBLE
child.addComments(comment.replies!!)
tVReplies.text = resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
} else {
//replies all hidden
val txt = txtMore + " (${comment.repliesCount})"
tVReplies.text = txt
}
}
//second visibility block when toggling with the show more/hide textView
tVReplies.setOnClickListener{
//toggle child recyclerView visibility and change textView text
if(holder.view.rVReplies.visibility == View.GONE){
//show stuff
if(comment.replies!!.isEmpty()){
Timber.d(holder.adapterPosition.toString())
//fetch replies if none were fetched yet
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!)
}else{
//load comments into adapter if not already
if(comment.replies!!.size>child.comments.size){child.addComments(comment.replies!!)}
}
repliesVisibility[position] = true
holder.view.rVReplies.visibility = View.VISIBLE
holder.view.tVReplies.text = holder.view.resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size && comment.replies!!.isNotEmpty()) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
}else{
//hide replies and change text
repliesVisibility[position] = false
holder.view.rVReplies.visibility = View.GONE
holder.view.iVMoreReplies.visibility = View.GONE
val txt = txtMore + " (${comment.repliesCount})"
holder.view.tVReplies.text = txt
}
}
tvReply.setOnClickListener{
replyCommentPosition = holder.adapterPosition
commentid = comments[holder.adapterPosition].id!!
listener.postCommentAsReply(comments[holder.adapterPosition].username!!)
}
iVMoreReplies.setOnClickListener{
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!, layoutManager.itemCount) //pass amount of replies too
}
}
}
#Suppress("UNCHECKED_CAST")
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if(payloads.isNotEmpty()){
//add reply to child adapter
with(holder.view){
Timber.d(payloads.toString())
val adapter = rVReplies.adapter as ChildCommentsAdapter
if(payloads[0] is Comment){
adapter.addComment(payloads[0] as Comment)
}else{
//will be of type List<Comment>
adapter.addComments(payloads[0] as List<Comment>)
val comment = comments[position]
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}else{
iVMoreReplies.visibility = View.GONE
}
}
}
}else{
super.onBindViewHolder(holder,position, payloads) //delegate to normal binding process
}
}
override fun getItemCount(): Int = comments.size
//add multiple replies to child adapter at pos 0
fun addReplies(position: Int, newComments: List<Comment>){
comments[position].replies!!.addAll(0, newComments)
notifyItemChanged(position, newComments)
}
//add a single reply to child adapter at last position
fun addReply(position: Int, newComment: Comment){
comments[position].replies!!.add(newComment)
comments[position].repliesCount += 1 //update replies count in case viewHolder gets rebinded
notifyItemChanged(position, newComment)
}
//add a new comment to main adapter at last position
fun addComment(comment: Comment){
comments.add(comment) //new comment just made goes to the end
repliesVisibility.add(false)
notifyItemInserted(itemCount-1)
}
//add multiple new comments to main adapter at pos 0
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
repliesVisibility.addAll(0, List(newComments.size) { false })
notifyItemRangeInserted(0, newComments.size)
}
}
The childAdapter is very basic and has nearly 0 logic.