So this recyclerview somehow only shows 5 items. When I delete the 5th item, the 6th item will comeout, so it seems it can only shows 5 items. Why?
For further information, this recyclerview is using Groupie library. I have used the same code in other activity but this one, each row is much bigger in terms of height because it has more information, does it have to do with the reason why the 6th item cannot be shown? The other recyclerview using groupie only shows name in each row, so it can show up to 7 items so far, only this one, despite using the same code, only shows 5 items maximum.
Following is the recyclerview code:
private fun fetchProducts() {
databaseReferenceProducts = FirebaseDatabase.getInstance().getReference("produk")
databaseReferenceProducts.child(uid).addListenerForSingleValueEvent(object: ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
binding.kedaiConstraintLayout.setPadding(0,0,0,0)
val adapter = GroupAdapter<GroupieViewHolder>()
snapshot.children.forEach {
val user = it.getValue(DataProduk::class.java)
if (user != null) {
binding.daftarProdukRecyclerView.adapter = adapter
adapter.add(productItems(user))
}
}
binding.daftarProdukRecyclerView.addItemDecoration(
DividerItemDecoration(
context,
DividerItemDecoration.VERTICAL
)
)
}
override fun onCancelled(snapshot: DatabaseError) {
}
})
}
For whoever facing this kind of issue, this is because the RecyclerView is overlapped with ScrollView. Replace it with NestedScrollView instead.
Related
I have a recyclerview inside a fragment that displays a mutable list of tasks that each have a title and description, wrapped in mutable live data.
private val _tasks = MutableLiveData<MutableList<Task>>()
To add those items, i implemented a bottom sheet dialog fragment with text edits for both values.
When i add a task item without specifying the index the recyclerview updates correctly :
_tasks.value!!.add(Task(taskEditText,descriptionEditText))
However, when i specify i want the new task item at index 0 and i add multiple task items, the recyclerview displays the first task i added over and over.
Things i've tried:
Using notifyDataSetChanged inside the adapter works and updates the recyclerview correctly, however i tried adding it to my add task button inside my bottom sheet dialog and it does nothing.
I tried adding the items to a temporary list and then setting it to _tasks.value but the same thing happened, only updates when i dont specify the index.
Here are relevant files :
AddTaskFragment (Bottom Sheet Dialog) :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
// if the textfields are not empty, adds the task and leaves the dialog
binding.buttonAdd.setOnClickListener{
if (binding.addTaskEditText.text!!.isNotEmpty() && binding.addDescriptionEditText.text!!.isNotEmpty()) {
viewModel.addTask(binding.addTaskEditText.text.toString(), binding.addDescriptionEditText.text.toString())
dismiss()
}
}
}
addTask function inside viewmodel :
fun addTask(taskEditText : String, descriptionEditText : String) {
_tasks.value!!.add(0,Task(taskEditText,descriptionEditText))
}
Adapter:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val task = viewModel.tasks.value!![position]
holder.itemTitle.text = task.text
holder.itemDescription.text = task.description
holder.textViewOptions.setOnClickListener {
onMenuClick(position, holder, task)
}
}
Thanks in advance and i hope you pros can help me
viewModel.tasks.observe(viewLifecycleOwner, Observer {
adapter.notifyDataSetChanged()
})
Try it.
I have an App that is communicating with a meshNetwork and constantly receiving messages over wifi with the properties of meshNodes. Those nodes should be displayed in a RecyclerView and also updated when a property changes with the usage of LiveData.
However when I receive multiple messages at almost the same time, the RecyclerView does not update the list.
E.g. a message form a meshNode is received, if the node is not already in a list inside LiveData<List<>> it will be added. After adding it to the list, the bound recycler view displays the node, everything is perfect by now. Immediatly after a new message from another meshNode that is not in the list is received, and added to the list, the meshNode is not displayed in the RecyclerView.
I really dont know why, and every help will be appreciated.
MeshNodeHandler handles messages received from meshNetwork, and updating nodes list
class MeshNodeHandler() : MeshHandler.MeshListener() {
private val mMeshNodes= mutableListOf<MeshNode>()
private val mMeshNodesLiveData = MutableLiveData<List<MeshNode>> = MutableLiveData()
val meshNodes: LiveData<List<MeshNode>>= mMeshNodesLiveData
override fun onNodeMessageReceived(nodeMessage: NodeMessage) {
val node =
mMeshNodes.firstOrNull {
it.meshID == nodeMessage.meshID
}
if (node != null && checkIfNodePropertiesChanged(node, nodeMessage)) {
Timber.d("Update mesh node")
// Update node in List...
mMeshNodesLiveData.postValue(mMeshNodes)
} else if (node == null) {
Timber.d("Add mesh node")
// Add node to list
mMeshNodesLiveData.postValue(mMeshNodes)
}
}
}
MeshNodesListViewModel just exposing the list from the NodeHandler
class MeshNodesListViewModel #Inject constructor(
private val meshNodeHandler: MeshNodeHandler
) : ViewModel() {
val meshNodes: LiveData<List<MeshNode>> = meshNodeHandler.meshNodes
}
And MeshNodesListFragment that observes the LiveData and submits the list to the adapter
class MeshNodesListFragment : BaseFragment<FragmentMeshNodesListBinding, MeshNodesListViewModel>(
layoutId = R.layout.fragment_mesh_nodes_list
) {
#Inject
lateinit var viewAdapter: MeshNodesAdapter
override fun onInitDataBinding() {
// DataBinding stuff ...
viewBinding.meshNodesRecyclerView.apply {
adapter = viewAdaper
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.meshNodes.observe(viewLifecycleOwner, {
Timber.d("Submit list")
viewAdapter.submitList(it)
})
}
}
Logcat:
D/MeshNodeHandler: Add mesh node
D/MeshNodesListFragment: Submit List
...
D/MeshNodehandler: Add mesh node
D/MeshNodesListFragment: Submit List
So the log says that a second node has been added to the LiveData<List<>>, and the list should also be submitted to the Adaper, but there is no second item displayed in the RecyclerView. If I destroy the Fragment and creating it again by switchig to portrait mode, then both items are displayed.
I'm thankful for every reply, cheers and stay healthy!
I think you are missing the notifyDataSetChanged() after you update the list. My understanding is that the list is updated but not reflecting in the view because recyclerView is not updating it's views. Try to call notifyItemInserted() or notifyItemRangeInserted() variants based on if you are adding single or multiple items. Hope this resolves the issue. Let me know if it helps.
First: I created a sample project showing this problem. By now I begin to think that this is a bug in either RecyclerView or MotionLayout.
https://github.com/muetzenflo/SampleRecyclerView
This project is set up a little bit different than what is described below: It uses data binding to toggle between the MotionLayout states. But the outcome is the same. Just play around with toggling the state and swiping between the items. Sooner than later you'll come upon a ViewHolder with the wrong MotionLayout state.
So the main problem is:
ViewHolders outside of the screen are not updated correctly when transition from one MotionLayout state to another.
So here is the problem / What I've found so far:
I am using a RecyclerView.
It has only 1 item type which is a MotionLayout (so every item of the RV is a MotionLayout).
This MotionLayout has 2 states, let's call them State big and State small
All items should always have the same State. So whenever the state is switched for example from big => small then ALL items should be in small from then on.
But what happens is that the state changes to small and most(!) of the items are also updated correctly. But one or two items are always left with the old State. I am pretty sure it has to do with recycled ViewHolders. These steps produce the issue reliably when using the adapter code below (not in the sample project):
swipe from item 1 to the right to item 2
change from big to small
change back from small to big
swipe from item 2 to the left to item 1
=> item 1 is now in the small state, but should be in the big state
Additional findings:
After step 4 if I continue swiping to the left, there comes 1 more item in the small state (probably the recycled ViewHolder from step 4). After that no other item is wrong.
Starting from step 4, I continue swiping for a few items (let's say 10) and then swipe all the way back, no item is in the wrong small state anymore. The faulty recycled ViewHolder seems to be corrected then.
What did I try?
I tried to call notifyDataSetChanged() whenever the transition has completed
I tried keeping a local Set of created ViewHolders to call the transition on them directly
I tried to use data-binding to set the motionProgress to the MotionLayout
I tried to set viewHolder.isRecycable(true|false) to block recycling during the transition
I searched this great in-depth article about RVs for hint what to try next
Anyone had this problem and found a good solution?
Just to avoid confusion: big and small does not indicate that I want to collapse or expand each item! It is just a name for different arrangement of the motionlayouts' children.
class MatchCardAdapter() : DataBindingAdapter<Match>(DiffCallback, clickListener) {
private val viewHolders = ArrayList<RecyclerView.ViewHolder>()
private var direction = Direction.UNDEFINED
fun setMotionProgress(direction: MatchCardViewModel.Direction) {
if (this.direction == direction) return
this.direction = direction
viewHolders.forEach {
updateItemView(it)
}
}
private fun updateItemView(viewHolder: RecyclerView.ViewHolder) {
if (viewHolder.adapterPosition >= 0) {
val motionLayout = viewHolder.itemView as MotionLayout
when (direction) {
Direction.TO_END -> motionLayout.transitionToEnd()
Direction.TO_START -> motionLayout.transitionToStart()
Direction.UNDEFINED -> motionLayout.transitionToStart()
}
}
}
override fun onBindViewHolder(holder: DataBindingViewHolder<Match>, position: Int) {
val item = getItem(position)
holder.bind(item, clickListener)
val itemView = holder.itemView
if (itemView is MotionLayout) {
if (!viewHolders.contains(holder)) {
viewHolders.add(holder)
}
updateItemView(holder)
}
}
override fun onViewRecycled(holder: DataBindingViewHolder<Match>) {
if (holder.adapterPosition >= 0 && viewHolders.contains(holder)) {
viewHolders.remove(holder)
}
super.onViewRecycled(holder)
}
}
I made some progress but this is not a final solution, it has a few quirks to polish. Like the animation from end to start doesn't work properly, it just jumps to the final position.
https://github.com/fmatosqg/SampleRecyclerView/commit/907ec696a96bb4a817df20c78ebd5cb2156c8424
Some things that I changed but are not relevant to the solution, but help with finding the problem:
made duration 1sec
more items in recycler view
recyclerView.setItemViewCacheSize(0) to try to keep as few unseen items as possible, although if you track it closely you know they tend to stick around
eliminated data binding for handling transitions. Because I don't trust it in view holders in general, I could never make them work without a bad side-effect
upgraded constraint library with implementation "androidx.constraintlayout:constraintlayout:2.0.0-rc1"
Going into details about what made it work better:
all calls to motion layout are done in a post manner
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
Compared actual vs desired properties
val goalProgress =
if (currentState) 1f
else 0f
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
if (motionLayout.currentState != desiredState) {
safeRunBlock {
startTransition(currentState)
}
}
}
This would be the full class of the partial solution
class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
val motionLayout: MotionLayout =
binding.root.findViewById<MotionLayout>(R.id.root_item_recycler_view)
.also {
it.setTransitionDuration(1_000)
it.setDebugMode(DEBUG_SHOW_PROGRESS or DEBUG_SHOW_PATH)
}
var lastPosition: Int = -1
fun bind(item: T, position: Int, layoutState: Boolean) {
if (position != lastPosition)
Log.i(
"OnBind",
"Position=$position lastPosition=$lastPosition - $layoutState "
)
lastPosition = position
setMotionLayoutState(layoutState)
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
fun setMotionLayoutState(currentState: Boolean) {
val goalProgress =
if (currentState) 1f
else 0f
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
if (motionLayout.currentState != desiredState) {
Log.i("Pprogress", "Desired doesn't match at position $lastPosition")
safeRunBlock {
startTransition(currentState)
}
}
}
}
fun startTransition(currentState: Boolean) {
if (currentState) {
motionLayout.transitionToStart()
} else {
motionLayout.transitionToEnd()
}
}
}
Edit: added constraint layout version
I have a RecyclerView which allows swipe-to-delete functionality. After deliting, a Snackbar shows to confirm deletion with an action that allows users to "undo" the delete.
Everything works fine until I delete the item at position 0 then hit undo. The item will be reinserted back into the list but users will need to scroll up to bring it back into view.
What I have tried
Setting recyclerView.isNestedScrollingEnabled" on the RecyclerView
Using layoutCoordinator.scrollTo(0, 0) on the Coordinator Layout
Using recyclerView.smoothScrollToPosition(0) on the RecyclerView
Using recyclerView.scrollToPosition(0) on the RecyclerView
Using recyclerView.scrollTo(0, 0) on the RecyclerView
Using itemAdapter.notifiyItemInserted(0) on the Adapter
Using itemAdapter.notifiyItemchanged(0) on the Adapter
Using itemAdapter.notifyDataSetChanged() on the Adapter
Using layoutManager.scrollToPositionWithOffset(0, 0) on the
LayoutManager
Creating a custom LayoutManager and overriding
smoothScrollToPosotion() with my own implementation.
None of the above have offered a solution.
Below are the workings.
ItemsFragment
Inside onCreateView - here is setting up the itemAdapter and recyclerView:
val itemAdapter = object : ItemRecyclerViewAdapter() {
override fun onItemClicked(item: Item) {
// todo
}
}
val layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = itemAdapter
Here is my swipeToDeleteCallback with Snackbar action to "undo" the delete:
val swipeToDeleteCallback = object : SwipeToDeleteCallback() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val item = itemAdapter.currentList[position]
viewModel.deleteItem(item)
val snackBar = Snackbar.make(
recyclerView,
getString(R.string.snackbar_msg_deleted_card),
Snackbar.LENGTH_LONG
)
snackBar.setAction(R.string.snack_bar_undo) {
viewModel.restoreItem(item)
recyclerView.smoothScrollToPosition(position)
}
snackBar.show()
}
}
val itemTouchHelper = ItemTouchHelper(swipeToDeleteCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
Setting the items which are LiveData observed from the ViewModel. Changes are immediately passed to the adapter:
val items = viewModel.items.await()
items.observe(viewLifecycleOwner, Observer {
it?.let {
itemAdapter.setItems(it)
}
})
ItemsAdapter
Submitting the list with DiffUtilCallback:
fun setItems(list: List<Item>?) {
adapterScope.launch {
val itemsList = when(list) {
null -> emptyList()
else -> list.sortedByDescending {
it.itemId
}
}
withContext(Dispatchers.Main) {
submitList(itemsList)
}
}
}
Temporary fix
So far the only thing that has worked is this hacky solution inside my Snackbar action:
snackBar.setAction(R.string.snack_bar_undo) {
viewModel.restoreItem(item)
if (position == 0) {
itemsAdapter.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0)
}
}
Here I'm checking if the item position is 0. If so then telling the adapter that there's a new item inserted at position 0 - then initiating scrolling. Otherwise, don't do anything because items at any position other than 0 will insert and animate fine beceause of the DiffUtilCallback.
This temporary fix works but the scrolling is "snappy" and produces an error in the logs:
RecyclerView: Passed over target position while smooth scrolling
Also, this solution does not work 100% of the time. It's more like 60% of the time.
Does anybody know of a better solution/something I am missing and a way to resolve the error above?
I am an iPhone developer and currently porting my app to Android with Kotlin language and this is my first android app so I do not know about the Android studio or Kotlin much so bare my question.
I have an app where I show all the images stored into the firebase and each image has like node. So when the users pressed the like button the user Id will be added to the node and pressing the button again will remove the user Id from the firebase node. I use Groupie RecyclerView for the images to for rows. The only problem is when I click the like button the RecyclerView scrolls to the top which is kind of irritating and not a good user interface. How can I stop the like button to stop scrolling to the top, I believe its due scrolling due to firbase database refreshing the RecyclerView.
Below is the function I have used for fetching the data from the database
private fun fetchFactsFromFirebase(){
val factsDb = FirebaseDatabase.getInstance().getReference("/Facts").orderByChild("/timestamp")
factsDb.addValueEventListener(object : ValueEventListener{
override fun onDataChange(p0: DataSnapshot) {
val adapter = GroupAdapter<ViewHolder>()
p0.children.forEach {
val facts = it.getValue(Facts::class.java)
if (facts != null) {
adapter.add(FactsItems(facts))
}
}
recycleview_facts.adapter = adapter
}
Below is the facts class I have used to bind my database to the Row
class FactsItems(val facts: Facts) : Item<ViewHolder>(){
val factsId : String = ""
val currentUser = FirebaseAuth.getInstance().currentUser?.uid
override fun bind(viewHolder: ViewHolder, position: Int) {
viewHolder.itemView.textview_facts.text = facts.captionText
Picasso.get().load(facts.factsLink).placeholder(R.drawable.progress_image).into(viewHolder.itemView.imageview_facts)
viewHolder.itemView.likeview_facts.text = facts.likes.count().toString()
val likeButon = viewHolder.itemView.like_facts_image_button
if (facts.likes.contains(currentUser!!)){
likeButon.setImageResource(R.drawable.like)
likeButon.isSelected = true
}else{
likeButon.setImageResource(R.drawable.nolike)
likeButon.isSelected = false
}
viewHolder.itemView.imageview_facts
viewHolder.itemView.like_facts_image_button.isSelected
viewHolder.itemView.like_facts_image_button.setOnClickListener {
if (likeButon.isSelected == true){
likeButon.isSelected = false
likeButon.setImageResource(R.drawable.nolike)
addSubtractLikes(false,position, viewHolder)
}else {
likeButon.isSelected = true
likeButon.setImageResource(R.drawable.like)
addSubtractLikes(true, position, viewHolder)
}
}
}
Below is the addSubtract function to add or subtract the likes of the user ignore the postion and view holder I just tried to get the postion in this function but it did not work.
fun addSubtractLikes(addlike: Boolean, position: Int, viewHolder: ViewHolder){
val currentUsers = FirebaseAuth.getInstance().currentUser?.uid
if (addlike){
facts.likes.add(currentUsers!!)
Log.d("Like","User Liked ${currentUsers}")
}else {
facts.likes.remove(currentUsers!!)
Log.d("Like","User DisLiked ${currentUsers}")
}
val likeRef = FirebaseDatabase.getInstance().getReference("/Facts").child(facts.factsId).child("/likes")
likeRef.setValue(facts.likes)
Log.d("Like","liked Users ${facts.likes}")
}
What all I have tried.
When I get the instance of the RecyclerView into the factsItem class it gives me an error nullPointerException
so i cannot use RecyclerView.scrollToPostion or RecyclerView any function
Thank you any help will be appreciated.