I have a RecyclerView inside of a Fragment. I use Data Binding and Navigation Component.
My problem is, when I go back to the previous fragment using the back arrow or just navigate back, and after that I open again the fragment, where I have the RecyclerView, the last item will be still there, but the list is totally empty.
I call this method when I close and open the fragment with the RecyclerView.
fun removeAllItems()
{
listOfItems.clear()
notifyItemRangeRemoved(0, listOfItems.lastIndex)
}
I have tried notifyDataSetChanged() also, but still I have the same issue.
Any idea why does it happen?
Thanks.
EDIT:
Finally I could solve the problem.
The onCreate() method:
private lateinit var listItemAdapter: ListItemAdapter
listItemAdapter = ListItemAdapter(context, mutableListOf<ListItem>())
Log.d(TAG, "Size_4: ${listItemAdapter.itemCount}")
listItemAdapter.removeAllItems()
binding.rvCreateNewListFourthItem.apply {
layoutManager = LinearLayoutManager(context)
adapter = listItemAdapter
}
Log.d(TAG, "Size_6: ${listItemAdapter.itemCount}")
Results: Size_4: 0
Results: Size_6: 0
After thse lines I have only 2 setOnClickListeners, which opens 2 other dialogs.
override fun onResume()
{
super.onResume()
Log.d(TAG, "Size_3: ${listItemAdapter.itemCount}")
listItemAdapter.removeAllItems()
Log.d(TAG, "Size_5: ${listItemAdapter.itemCount}")
}
Results: Size_3: 1
Results: Size_5: 0
The solution was to call the removeAllItems() method on the adapter.
When the fragment reached the onResume() method, the list contained the last item from the last session.
Related
I have a single activity app (MainActivity) with a dialog fragment. The DialogFragment adds data to the sqllite data base and the MainActivity displays the data in a recycler view. The problem I'm facing is that when I click the "add button" in the DialogFragment and dismisses it, the newly inserted data does not get displayed in the recycler view until the activity gets recreated.
How do I display new data without restarting the app. I have already added "recyclerViewAdapterObject.notifyDataSetChanged" in the DialogFragment class, but it's still not working!
You can solve it by following activity lifecycle method. You just override onResume() method. Put your "data fetching" code and set your adapter inside onResume() method and call onResume() after the data save and before the DialogFragment dismiss. This fix your problem.
But it is not recommended. You should learn to use ViewModel, LiveData.
Example:
override fun onResume() {
super.onResume()
findViewById<RecyclerView>(R.id.rvSocket).also { rv ->
var userList : List<User> = fatchDataFromSQLite()
rv.layoutManager = LinearLayoutManager(this)
rv.setHasFixedSize(true)
adapter = UserAdapter(userList)
rv.adapter = adapter
}
}
Then call onResume() method where you save your data by button click before dismiss your DialogFragment.
I'm using paging 3 to paginate my retrieved data.
I want to show a badge on the bottom navigation bar when a new item has been added to the recycler view.
imagine the user is watching recycler view in the situation below:
item A
item B
item C
now the user refresh the page using a swipe refreshing button which calls this function:
binding.refresh.setOnRefreshListener {
adapter.refresh()
}
and a new item is added, so the situation would be like this:
item D
item A
item B
item C
the new data would be propagated using this line of code:
viewModel.tickets.observe(viewLifecycleOwner, {
viewLifecycleOwner.lifecycleScope.launch {
adapter.submitData(it)
}
})
I want to show a badge at this moment. so I added an interface into my recycler paged list adapter like this:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
ticketUtils.hasNewItem(getItem(0)?.ticket_id)
holder.bind(getItem(position))
}
which checks what is the first item of the recycler view.
so I have implemented the body of this interface inside of my fragment like this:
override fun hasNewItem(itemID: String?) {
if (itemID.isNullOrEmpty())
return
if (itemID != firstTicketID)
MainActivity.mainUtils.newTicket(showBadge = true)
}
and MainActivity.mainUtils.newTicket is an interface that shows a badge on the desired icon.
but I have faced to the problem that it seems the recycler view won't be notified for new data till the user start scrolling. I mean the code written in onBindedViewHolder would be called just when the user scrolls the page.
how can I solve that?
viewModel.tickets.observe(viewLifecycleOwner, {
//Here you get to know that there's update in your Datalist
adapter.submitData(it)
})
When you insert a new data into the database, observe the data inserted by returning the id of the data. For example if you are using Room, it can return a long value, which is the new rowId for the inserted item.
Compare with existing ones and then you can show a badge if there's new data in the list.
I have custom ViewPager and PagerAdapter which is loading items inside instantiateItem. It is working fine if I initialize it for the first time with set list of items.
But as I call refresh on the list and I want to populate Adapter with new (totally different) list, after calling viewPager.adapter?.notifyDataSetChanged(), PagerAdapter stops working properly and those items are blank pages without content and I can swipe through them in UI.
PageScreen is not Fragment. It is just ViewGroup container which is inflating layout and setting values out of specific item. It is similar to ViewHolder + Binder in RecyclerView.
Also instatiateItem() is called only once as i add new list and call notifyDataSetChanged(). At start it is called 3 items, which is amount of PageScreen items in first list.
//init
val pages = mutableListOf<PageScreen>()
pages.add(PageScreen(activity, app, itemJs1, onClick = {onItemClicked(itemJs1.id)}))
pages.add(PageScreen(activity, app, itemJs2, onClick = {onItemClicked(itemJs2.id)}))
pages.add(PageScreen(activity, app, itemJs3, onClick = {onItemClicked(itemJs3.id)}))
swipePager.adapter = CustomPagerAdapter(pages).also { it.notifyDataSetChanged() }
...
//on refresh after API call
pages.clear()
contentList.forEach{item-> pages.add(PageScreen(activity, app, item, onClick = {onItemClicked(item.id)}))}
(swipePager.adapter as? CustomPagerAdapter)?.notifyDataSetChanged()
Also tried this (same result):
//on refresh after API call
val newPages = mutableListOf<PageScreen>()
contentList.forEach{item-> newPages.add(PageScreen(activity, app, item, onClick = {onItemClicked(item.id)}))}
swipePager.adapter = CustomPagerAdapter(newPages).also { it.notifyDataSetChanged() }
Adapter:
class CustomPagerAdapter(private var pageList: MutableList<PageScreen>) : PagerAdapter() {
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
override fun getCount(): Int {
return pageList.size
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
override fun instantiateItem(container: ViewGroup, position: Int): View {
val layout = pageList[position].getScreen(container)
container.addView(layout)
return layout
}
}
Also I tried to properly refresh items (I expect that this is done internally by PagerAdapter and ViewPager when I call notifyDataSetChanged()) by removing them from ViewPager contentView and calling instantiateItem() for each item. But same result as above. Now every single page was blank. Function below is added to CustomPagerAdapter.
fun refreshItems(vp: ViewPager, data: MutableList<PageScreen>){
pageList.apply {
forEachIndexed{pos, item->
item.screenView?.let { sv->
destroyItem(vp, pos, sv)
}
}
clear()
addAll(data)
forEachIndexed { pos, _ -> instantiateItem(vp, pos) }
}
notifyDataSetChanged()
}
UPDATE:
I managed to "fix" this by setting ViewPager height to fixed value instead WRAP_CONTENT but its not a solution. I want ViewPager with dynamic height, because some of its children can have different height + setting something to static is not good approach in Android. Some phones with square displays could have cropped page then.
What happened is as I replaced all items, those "blank" pages were items with 0px height and 0px width for some unknown reason.
If I replaced ViewPager height to dp value, it "worked". But as I replaced those Views, first item was always blank. but as I scrolled to third one and back to first, item was there for some reason.
Also I don't get that height problem. I have function inside ViewPager which is setting its height based on tallest child in list. It works if list is static, but it is not working now as I refresh that.
Recreating whole ViewPagerAdapter is a solution for this problem.
I called API inside onPageSelected() and remembered position returned to listener function.
Then I recreated whole adapter like this:
swipePager.apply {
adapter = null
adapter = CustomPagerAdapter(newPages)
adapter?.notifyDataSetChanged()
invalidate()
}
and after refresh I scrolled to remembered position like this:
setCurrentItem(position, true)
This solution will not left blank pages, but I had to set static height for my ViewPager, which can be a problem on mdpi screens which sometimes cannot redraw particular dp value from XML layout correctly. Phone with this problem is for example Sony Xperia E5 which has xhdpi screen and part of my ViewPager is cropped from the bottom.
This have to be tuned manually with separate dimens.xml for both mdpi and xhdpi.
I designed an app using jetpack-navigation
There is a problem as illustrated in the following image when I move from one fragment to another one the status of the list disappears.
In fact, when returning from a layout, the article will be re-created in the stack and the list status will not be saved and the user will have to scroll again. Please help me?
jetpack-navigation
The fragment gets recreated on every navigation operation. You could store the scroll position in your activity and load it from there. But doing it like this, the scroll position will be lost on activity recreate (e.g. rotating device).
Better approch would be to store it in a ViewModel. (see https://developer.android.com/topic/libraries/architecture/viewmodel)
The view model survives activity recreation and you can store your scroll position.
Then you can load this position and tell the list to scroll to this position (e.g. for RecyclerView with LinearLayoutManager by calling scrollToPositionWithOffset(...))
I'm reloading recyclerView data every 15s. To keep scroll position when switching between apps, I use onSaveInstanceState() and onRestoreInstanceState(mRVState) methods in corresponding fragment overriden methods. But when I wanted to save position while switching between different fragments, I came up with this solution:
1.Set RecyclerView.OnScrollListener() in onResume() method of Fragment and get current first visible item position on each scroll. As you can see position variable is located in parent activity, so it's not lost on fragment replacement:
override fun onResume() {
super.onResume()
if (updateListRunnable != null) setAndRunUpdateListRunnable()
mRV?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
mainActivity.lastRVPosition =
(recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
}
})
}
2.Use scrollToPosition() method of recyclerView after data has been replaced inside adapter:
private fun setDataList(dataList: List<Data?>?) {
val mutableDataList = dataList?.toMutableList()
val currentItemCount = binding?.rvDataList?.adapter?.itemCount
if (currentItemCount == null || currentItemCount == 0) {
// create new adapter with initial data
val adapter = DataListAdapter(mutableDataList, baseVM, mainVM)
binding?.rvDataList?.adapter = adapter
binding?.rvDataList?.layoutManager = LinearLayoutManager(context)
mRV?.scrollToPosition(mainActivity.lastRVPosition);
} else {
// update existing adapter with updated data
mRVState = mRV?.layoutManager?.onSaveInstanceState()
val currentAdapter = binding?.rvDataList?.adapter as? DataListAdapter
currentAdapter?.updateDataList(dataList)
currentAdapter?.notifyDataSetChanged()
mRV?.layoutManager?.onRestoreInstanceState(mRVState)
mRV?.scrollToPosition(mainActivity.lastRVPosition);
}
}
As you can see I also use onSaveInstanceState()/onRestoreInstanceState() before/after replacing data, so that if there was no scroll before data replacement, position will still be saved. Scroll listener saved position is only useful when switching between fragments.
I am having trouble debugging a recycler view issue. Here I am trying to delete an item from recycler view using the following method:
when I click on the card (one that is to be deleted), it will store the title of a card in shared preference. (so that in future when I open the app I will know what cards are not to be displayed)
Then I am calling a method which checks if the title in shared preference matches the title of the card and make ArrayList consisting of cards which are not present in shared preference.
Now I am using this ArrayList to fill cards using notifyDataSetChanged
My code is as Follows.
Adapter.
class LocalAdapter(var localCardsInfo : ArrayList<LocalModel>?,var fragment: LocalListingFragment) : RecyclerView.Adapter<LocalHolder>() {
fun refreshDataOnOrientationChange(mLocalCards : ArrayList<LocalModel>?){
if (localCardsInfo!!.size>0)
localCardsInfo!!.clear()
localCardsInfo = mLocalCards
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return localCardsInfo!!.size
}
override fun onBindViewHolder(holder: LocalHolder, position: Int) {
if (localCardsInfo!=null)
holder.updateUI(holder.adapterPosition,localCardsInfo!![holder.adapterPosition])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocalHolder {
val card_a : View = LayoutInflater.from(parent.context).inflate(R.layout.card_local_a,parent,false)
return LocalHolder(card_a,fragment)
}
}
ViewHolder
class LocalHolder(itemView : View,val fragment: LocalListingFragment) : RecyclerView.ViewHolder(itemView),OpenedLayoutManagerLocal{
fun updateUI(position : Int , localModel: LocalModel){
moveButton.setOnClickListener({
archived()
})
private fun archived(){
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(fragment.activity!!,model!!.roomName,model!!.deviceName)
itemView.archiveLayout.visibility = View.GONE
itemView.elevateLayoutLocal.visibility = View.GONE
fragment.mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(fragment.activity!!)))
}
For example, if I have six item in recycler view and when I delete one I can see 5 items on the list while debugging, even onbind is called 5 times but only 4 items are displayed.
I tried my best to debug it yet failed to find a solution and I can't think about what to do next. I tried notifyItemRemoved and notifyItemRangeInserted also but still, I am facing the same issue. It would be a great help if someone can suggest a possible cause of such issue.
Thank you.
Edits:
I just experimented by adding dummy button in fragment where recycler view is placed. An to my surprise if I delete element here just element at that position gets deleted and there is no case of disappearing items. So it seems like problem happens when I delete an item from view holder. So I guess I got the cause of the error but can't think of a way to implement it because the original button is in the card so I am forced to use delete procedure inside view holder. Please do suggest if you have any suggestions. Code that I added in fragment is as follow:
v!!.findViewById<Button>(R.id.delete_button).setOnClickListener({
try {
val model = LocalDataService.ourInstance.getNonArchivedItems(activity!!)[0]
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(activity!!,model.roomName,model.deviceName)
mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(activity!!)))
}catch (e : Throwable){}
})
Here on this new dummy button click, it deletes the first item in the list gets deleted while on clicking the button on RecyclerView card it deletes one item and then one more item gets disappeared.