I tried many ways but couldn't get RecyclerView display new rows.
This is my adapter:
private inner class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
override fun getItemCount(): Int {
LogDog.i(TAG, "getItemCount=" + _to.size)
return _to.size
}
override fun onBindViewHolder(holder: MyViewHolder?, position: Int) {
LogDog.i(TAG, "Binding row $position of size=" + _to.size)
val contactId = _to[position]
holder?._tv_name?.text = DB.getString(TblContact, TblContact._display, contactId)
holder?._tv_address?.text = DB.getString(TblContact, TblContact._address, contactId)
holder?._position = position
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): MyViewHolder {
LogDog.i(TAG, "onCreateViewHolder, size=" + _to.size)
return MyViewHolder(activity.layoutInflater.inflate(R.layout.r_to, parent, false))
}
}
When contact button clicked, contact dialog displays to select and calls _adListener to add row:
private val _to = ArrayList<Long>()
private var _adapter = MyAdapter()
private val _addListener : (Long) -> Unit = { id ->
_to.add(id)
Log.i(TAG, "_addListener size=" + _to.size)
//_adapter.notifyItemInserted(_to.size - 1)
_adapter.notifyDataSetChanged()
}
private val _addClicker = View.OnClickListener { _ ->
DlgContactEdit(activity, null, null, _addListener).show()
}
private val _lookupClicker = View.OnClickListener { _ ->
DlgContactSelector(activity, _addListener).show()
}
From log, I do see _addListener is called, but new rows not displaying! Only when layout refreshes (e.g., keyboard displays and hides), new rows will display to the real content in _to list. What could be the reason?
I also tried to use Handler to call notifyItemInserted later, or change to notifyDataSetChanged, but no luck.
Contrary to many tutorials, notifyChange does not work by itself, most importantly you need to refresh the recycler view .invalidate().
Here is an example of a function i have in one of my classes that can call from different places to refresh my RV.
/**
* Sets up the recycler view and refreshes it after data changes
*/
private void setUpView() {
Schedule_AddTimes_Adapter adapter = new Schedule_AddTimes_Adapter(mTimesList);
RecyclerView.ItemDecoration divider = new DividerItemDecoration(ContextCompat.getDrawable(mContext, R.drawable.divider));
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(mContext);
mDoseTimesList.setLayoutManager(linearLayoutManager);
mDoseTimesList.setAdapter(adapter);
mDoseTimesList.addItemDecoration(divider);
mDoseTimesList.invalidate();
}
Sorry for my stupid error. Just noticed I didn't use _adapter at all, I was writing this
rv_to.adapter = MyAdapter()
which is completely wrong.
And it works without calling invalidate.
Thanks all for the help!
Related
Good day. So I currently have data in my recycler view. It is for now only static data. I still have to do the code where I import. My problem however is I have a button that changes the background of a text view. This happens in my adapter. And when I scroll through my list the bg color change gets reverted back to what it was before the button click. I have read a lot of similar problems but could not really find one that explains clearly or work for me. From what I read the data gets reset to the static data because it is currently happening in my onBindViewHolder and I think this changes the data on every new data read(scrolling). I read that I should create a link or a listener and then call it. But It does not make sense to me because if a link is called the same amount of times as the code is executed then it will be the same will it not. Maybe having a condition listener but not sure if this is the way to go.
I am somewhat new to android and kotlin. Have been working with it for a month now. I dont know everything I am doing but I got given a deadline. So sadly there was no time to go and learn the basics. Thank you for any and all help. Please let me know if you need any additional code/information
my adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.table_list_item, parent, false)
return RowViewHolder(itemView)
}
private fun setHeaderBg(view: View) {
view.setBackgroundResource(R.drawable.table_header_cell_bg)
}
private fun setContentBg(view: View) {
view.setBackgroundResource(R.drawable.table_content_cell_bg)
}
override fun onBindViewHolder(holder: RowViewHolder, position: Int) {
// (TableViewAdapter.DataviewHolder) .bind()
val rowPos = holder.adapterPosition
if (rowPos == 0) {
// Header Cells. Main Headings appear here
holder.itemView.apply {
setHeaderBg(txtWOrder)
setHeaderBg(txtDElNote)
setHeaderBg(txtCompany)
// setHeaderBg(txtAddress)
setHeaderBg(txtWeight)
setHeaderBg(txtbutton1)
setHeaderBg(txtbutton2)
setHeaderBg(txttvdone)
txtWOrder.text = "WOrder"
txtDElNote.text = "DElNote"
txtCompany.text = "Company"
// txtAddress.text = "Address"
txtWeight.text = "Weight"
txtbutton1.text = "Delivered"
txtbutton2.text = "Exception"
txttvdone.text = ""
}
} else {
val modal = Tripsheetlist[rowPos - 1]
holder.itemView.apply {
setContentBg(txtWOrder)
setContentBg(txtDElNote)
setContentBg(txtCompany)
// setContentBg(txtAddress)
setContentBg(txtWeight)
setContentBg(txtbutton1)
setContentBg(txtbutton2)
setContentBg(txttvdone)
txtWOrder.text = modal.WOrder.toString()
txtDElNote.text = modal.DElNote.toString()
txtCompany.text = modal.Company.toString()
// txtAddress.text = modal.Address.toString()
txtWeight.text = modal.Weight.toString()
txtbutton1.text = modal.Button1.toString()
txtbutton2.text = modal.Button2.toString()
txttvdone.text = modal.tvdone.toString()
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
txttvdone.setBackgroundResource(R.color.green)
txttvdone.setText("✓")
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
txttvdone.setBackgroundResource(R.color.orange)
txttvdone.setText("x")
}
}
}
class RowViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val txttvdone:TextView = itemView.findViewById<TextView>(R.id.txttvdone)
val txtbutton1:Button = itemView.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
} class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view){
var txtbutton1 = view.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
var txttvdone = view.findViewById<TextView>(R.id.txttvdone)
}
I tried (TableViewAdapter.DataviewHolder) .bind() doing this and creating another class as I saw that was done in another thread(Why do values disappear after scrolling in Recycler View?) Its a lot like my problem. I just can't seem to implement his solution to make mine work. ( don't understand his solution fully)
//I am also aware that I am using android extensions which will expire at the end of the year. But for now it works and once I have the code up and running I will start to move over to the newer versions of kotlin.
A RecyclerView, as its name implies, will recycle the views when they go off screen. This means that when the view for an item comes into view, it gets recreated and the onBindViewHolder() is called to fill in the details.
Your onClickListener inside your adapter changes the background of one of the subviews for your cell view. However, that cell will be redrawn if it leaves the screen and comes back.
To get around this, your onClickListener should be changing a property on the data item, and your onBindViewHolder should check that property to determine what background color to display for the subview:
enum class DataState {
Unselected,
Success,
Failure
}
data class DataItem(var state: DataState = DataState.Unselected)
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
var dataItems: List<DataItem> = emptyList()
fun updateData(data: List<DataItem>) {
dataItems = data
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val dataItem = dataItems[position]
holder.txttvdone.apply {
setBackgroundResource(when (dataItem.state) {
DataState.Unselected -> android.R.color.transparent
DataState.Success -> R.color.green
DataState.Failure -> R.color.orange
})
text = when (dataItem.state) {
DataState.Unselected -> ""
DataState.Success -> "✓"
DataState.Failure -> "x"
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
dataItem.state = DataState.Success
notifyDataSetChanged()
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
dataItem.state = DataState.Failure
notifyDataSetChanged()
}
}
}
}
I checked all the examples, but they don't work after all. As far as I know, even if payload is 'List', String or Int value can go into.
class RecordListAdapter (val context: Context, val layoutManager: LinearLayoutManager, private val canBeEdited: Boolean)
: RecyclerView.Adapter<RecordListAdapter.RecordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var records: ArrayList<Record> = arrayListOf()
// Update ALL VIEW holder
override fun onBindViewHolder(holder: RecordViewHolder, position: Int) {
val current = records[position]
holder.autoCompleteTextView.text = SpannableStringBuilder(current.name)
holder.weightPicker.value = current.weight
holder.setPicker.value = current.set
holder.repsPicker.value = current.reps
if(position == itemCount - 1) holder.addBtn.visibility = View.VISIBLE
else holder.addBtn.visibility = View.GONE
if(canBeEdited) {
if(itemCount == 1) {
holder.deleteBtn.visibility = View.GONE
} else {
holder.deleteBtn.visibility = View.VISIBLE
holder.deleteBtn.setOnClickListener {
records.remove(current)
notifyItemRemoved(position)
}
}
} else
holder.deleteBtn.visibility = View.GONE
}
// Update only part of ViewHolder that you are interested in
override fun onBindViewHolder(holder: RecordViewHolder, position: Int, payloads: MutableList<Any>) {
Log.e("payload", "::$payloads")
if(payloads.isNotEmpty()) {
} else
super.onBindViewHolder(holder,position, payloads)
}
private fun addRecordDefault() {
this.records.add(Record("", 28, 5, 10))
notifyItemInserted(itemCount)
notifyItemRangeChanged(itemCount-1, 2, "PAYLOAD_ADD_BTN")
}
override fun getItemCount() = records.size
}
As above code, I set the Log.e to know whether the value is empty or not. The payload Log.e always say it's null.
E/payload: ::[]
Firstly, it seems you are just adding the item and want to change something in it with payloads right away.
Obviously, when you just add a new one, the whole item has to be drawn, thus no payloads needed.
Only then, when it is already drawn and you want to change some elements, you may use payloads with notifyItemChanged (if one item was changed) or notifyItemRangeChanged (if several items were changed).
Secondly, I am not sure regarding the range you use.
The first argument of notifyItemRangeChanged is the start index.
The second one is how many items you want to change.
Thirdly, it's not clear where do you call the addRecordDefault
So make sure you called notifyItemChanged or notifyItemRangeChanged with payloads.
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.
I'm using a RecyclerView to display messages from a user and a bot, but after about 10 messages, upon scrolling, the RecyclerView's items display out of order.
I removed setStackFromEnd() and setReverseLayout() from the LinearLayoutManager my code in case that caused any issue, but that didn't solve the problem. I also added the line holder.setIsRecyclable(false);, and although that worked, I didn't want to use that solution since it defeats the purpose of a RecyclerView.
Gets user response, adds the response to the list of messages, generates the reply, and calls calls updateUI():
public void respond(String input) {
int position = getMessagePosition();
Log.d(CHAT_FRAGMENT_TAG, "Received Message: " + input);
String reply = mBot.reply("user", input);
Log.d(CHAT_FRAGMENT_TAG, "Bot Reply: " + reply);
mMessages.add(new Message(reply, Message.BOT_MESSAGE, messagePosition()));
updateUI(position);
Log.d(CHAT_FRAGMENT_TAG, "mMessages List: " + mMessages.get(position).getText());
}
updateUI():
public void updateUI(int insertPosition) {
if (mAdapter == null) {
mAdapter = new MessageAdapter();
mRecyclerView.setAdapter(mAdapter);
} else
mAdapter.notifyItemInserted(insertPosition);
}
onBindViewHolder():
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
Message message = mMessages.get(position);
if (getItemViewType(position) == Message.USER_MESSAGE)
((UserMessageHolder) holder).bind(message);
else
((BotMessageHolder) holder).bind(message);
}
bind():
public void bind(Message message) {
mBotTextView.setText(message.getText());
}
messagePosition() and getMessagePosition():
public static int messagePosition() {
//A convenience function to get the current position and increment it as well for the next array element.
int returnValue = mMessagePosition;
mMessagePosition++;
return returnValue;
}
public static int getMessagePosition() {
return mMessagePosition;
}
There is an onClick() method on an EditText that takes the user's response and calls the respond() method. The respond() method gets the bot's reply, and it calls the updateUI() method to let the adapter know that the array is larger since new messages were added (one from the user and one from the bot). This process works fine until there are about 10 messages and the user needs to scroll. From that point, the messages reload on the screen incorrectly by repeating themselves or not loading in their correct spot.
I have had the same issue. Consider storing all the conversation somewhere, for example I do it inside of Conversation class
//List adapter
class MessagesListAdapter : RecyclerView.Adapter<MessageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder = MessageViewHolder(LayoutInflater.from(parent.context).inflate(viewType, parent, false))
override fun getItemCount(): Int = Conversation.getCount()
override fun reloadDataSet() {}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
holder.bind(Conversation.getMessage(position))
}
override fun getItemViewType(position: Int): Int = when (Conversation.getMessage(position).response) {
true -> R.layout.message_response
false -> R.layout.message_sent
}
}
//Message Holder
class MessageViewHolder(private var item: View) : RecyclerView.ViewHolder(item) {
fun bind(message: TextMessage) {
if (message.response) {
item.findViewById<TextView>(R.id.tv_response_bubble).text = message.text
} else {
item.findViewById<TextView>(R.id.tv_user_bubble).text = message.text
}
}
}
//Activity class
class ChatActivity : AppCompatActivity() {
private val listAdapter = MessagesListAdapter()
private lateinit var messagesListView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat)
messagesListView = findViewById(R.id.recyclerView)
messagesListView.adapter = listAdapter
val layoutManager = LinearLayoutManager(this)
//For messages to appear from bottom to top
layoutManager.stackFromEnd = true
messagesListView.layoutManager = layoutManager
}
//Code you call from activity to update the list
private fun addMessage(message: TextMessage) {
if (!message.isNullOrBlank()) {
Conversation.add(message)
listAdapter.notifyItemChanged(Conversation.getCount() - 1)
messagesListView.smoothScrollToPosition(Conversation.getCount() - 1)
}
}
}
//Conversation class
public class Conversation {
private static List<TextMessage> currentConversation;
public static void clear() {
currentConversation = new ArrayList<>();
}
public static void add(final TextMessage message) {
if (currentConversation == null) {
clear();
}
currentConversation.add(message);
}
static TextMessage getMessage(final int pos) {
if (currentConversation == null) {
return null;
}
return currentConversation.get(pos);
}
static int getCount() {
return currentConversation == null ? 0 : currentConversation.size();
}
}
If you have problems with messages appearing in the wrong spot, or not appearing at all, the underlying problem is that your data set doesn't have the data your adapter thinks it should.
Imagine that you have a list of strings ["A", "B", "C"]. You add two messages to it, but you only notify the adapter that you've added one. The adapter thinks the list is now ["A", "B", "C", "D"], but it is actually ["A", "B", "C", "D", "E"]. This will eventually lead to problems.
In the code you've posted, you only have a single notifyItemInserted() call (for the bot's response to the user's message). But you've described a workflow that includes adding both a user message and a bot reply. That makes me think that you've missed a notify call you need.
You can replace all calls to notifyItemInserted() with notifyDataSetChanged() and see if this fixes the problem. In general, it is better to use notifyItemInserted(), but if this fixes the problem then you'll know that the problem is that you've missed a notify call you needed. Then you can go through the code very carefully and make sure that any time mMessages is changed you notify the adapter.
I have a listView that has a custom layout that contains a fragment. The problem I am having is when I add the fragment to the listView row I get a never ending loop that keeps going through the getView code.
I have only been doing Android coding for a couple months so any help would be great. Please let me know if there is any more of my code I need to post
This is my adapter code:
class AdapterReply(
private val context: Context,
private val ph: Phan,
private val replies: ArrayList<Reply> ) : BaseAdapter() {
override fun getCount(): Int {
return replies.size
}
override fun getItem(position: Int): Reply {
return replies[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, viewGroup: ViewGroup?): View? {
val rowMain: View?
val rowHolder: ListRowHolder
val contacts = ph.contacts
val reply = replies[position]
Log.d("AdapterReply", "Binding reply: Position $position ${reply.id} Detail: ${reply.detail}")
if (convertView == null) {
val layoutInflater = LayoutInflater.from(viewGroup!!.context)
rowMain = layoutInflater.inflate(R.layout.reply_item_row, viewGroup, false)
rowHolder = ListRowHolder(rowMain)
rowMain.tag = rowHolder
Log.d("AdapterReply", "New Item")
} else {
rowMain = convertView
rowHolder = rowMain.tag as ListRowHolder
Log.d("AdapterReply", "Old item from memory")
}
rowHolder.itemDetail.text = Helpers.anonymizeContent(reply.detail, ph.anonymousMetadata, ph.isViewerMember())
rowHolder.itemLastActive.text = Helpers.getFormattedDate(reply.lastActive())
if(reply.attachments.size > 0){
val imageAttachment = reply.attachments.filter { attachment: Attachment -> attachment.type == 0 }[0]
var imageUrl = Constants.BASE_URL + imageAttachment.url
if(imageAttachment.anonymous()){
imageUrl = Constants.BASE_URL + imageAttachment.anonUrl
}
Picasso.with(rowMain!!.context).load(imageUrl).into(rowHolder.itemImage)
}
var poster: Contact? = reply.contact
if(contacts.size > 0) {
val posterList = contacts.filter { contact: Contact -> contact.id == reply.rlContactID }
if (posterList.isNotEmpty()) {
poster = posterList[0]
}
}
if(poster != null) {
if(poster.anonymizedName.isNotEmpty()) {
rowHolder.itemPoster.text = poster.anonymizedName
} else {
val posterName = "${poster.firstName} ${poster.lastName}"
rowHolder.itemPoster.text = posterName
}
//Code the causes the problem
val manager = (rowMain!!.context as AppCompatActivity).supportFragmentManager
val posterAvatarFragment =
AvatarFragment.newInstance(poster)
manager.beginTransaction()
.add(R.id.reply_avatar_fragment, posterAvatarFragment, "ReplyAvatarFragment")
.commit()
//End Code the causes the problem
}
bindReplies(rowMain, ph, reply.replies)
return rowMain
}
internal class ListRowHolder(row: View?) {
var itemDetail : TextView = row!!.reply_view_detail
var itemImage : ImageView = row!!.reply_view_image
var itemPoster : TextView = row!!.reply_view_posterName
var itemLastActive : TextView= row!!.reply_view_lastActive
var itemReplyReplies: ListView = row!!.reply_view_replyList
}
private fun bindReplies(viewRow: View?, ph: Phan, replies : ArrayList<Reply>){
if(replies.isNotEmpty()) {
val myObject = AdapterReply(context, ph, replies)
val replyListView = viewRow!!.reply_view_replyList
Log.d("AdapterReply", "Binding Replies: ${ph.encodedId} ${replies.size}")
replyListView.adapter = myObject
}
}
}
manager.beginTransaction()
.add(R.id.reply_avatar_fragment, posterAvatarFragment, "ReplyAvatarFragment")
.commit()
Man, I'm not sure do you know, what function performs adapter class in listview. Adapter class is used to fill listview by rows passed in array inside class constructor. getView method is called for every row in array, so for example, if you have an array with 10 rows, this code will fire ten times. If you do automatically change fragment to another, and during filling an old view you will change layout to a another layout with the same data, your code will make an infinity loop, because you will repeat all time the same cases. You should avoid actions, which will dynamically change layout during load him. In my sugestion, if you want to use a multiple layouts inside one adapter, there are two special methods to set layout for row, based on some conditions: getItemViewType and getViewTypeCount. In first one you can use your conditions to check, which layout should be used for row. Second one is to set number of layouts, which will be used in your adapter class. Let search some examples of usage.