Is there a way for me to set starting position for recyclerview, so when the user installs the app for the first time, and starts the app, the recyclerview will be shown at some position, and not from the start?
I have been using:
recyclerView.scrollToPosition(myPosition, 0)
But since the app is started for the first time, it is always default position shown in the recyclerview. The first param (myPosition) is set in the onBindViewHolder function.
I think this happens because recyclerview creates and destroys views while scrolling, so probably I would need to first iterate through the whole list, but not sure if the views will get destroyed in the end.
You can do this, but you need some form of persistence to store if is the first time seen it.
So, first of all, you can't just scroll to the position because even if you pass the data immediately, the adapter doesn't immediately update. There is a delay.
You have to attach a RecyclerView.AdapterDataObserver to the adapter and inside there do the scrolling.
val observer = object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
if (isFirstTime) {
//scrollToPosition
//mark first time as no longer first time
}
}
}
adapter.registerAdapterDataObserver(observer)
You might still need a delay, if so try with
viewlifeCycleOwner.lifeCycleScope.launch {
delay(400)
//scrollToPosition
}
You need to make your firstTime some form of persistence, so it remains across app-restart. I think SharedPrefferences should be enough.
You could save and load a variable to shared preferences, and once the user scrolls for the first time, that variable would change. (Just save a boolean - isFreshInstall, set to true as default and on scroll set it to false)
Then if the variable indicates it's the first time seeing the app just use a simple if to see ifyou should use myPosition or the position you want to show.
Related
I have a classic implementation of a recycler view that, when I click on an item inside the recycler view, that item gets deleted.
The problem is that, when I successively click twice one after another (without any noticeable delay between the clicks) on an item in that recycler view, then the second click on that same item is registered at a different position.
The way I identify the item that received the click is by holder.adapterPosition (where holder is an instantiation of ViewHolder class). I wonder if I'm doing wrong by relying on this.
To further troubleshoot, I added the following println statement to troubleshoot:
println("layoutpos ${holder.layoutPosition} adapterpos ${holder.adapterPosition} oldpos ${holder.oldPosition}")
Then, as I repeated those successive clicks, I got the following output in Android Studio's Run tab:
[Galaxy_Nexus_API_22 [emulator-5554]]: I/System.out: layoutpos 1 adapterpos 1 oldpos -1
[Galaxy_Nexus_API_22 [emulator-5554]]: I/System.out: layoutpos 0 adapterpos -1 oldpos -1
Right now, my thoughts are: use adapterPosition, and ignore it when its value is -1 (assume that -1 means a declaration of a racing condition). But I feel that I might be missing something deeper.
How should I handle this situation?
Show the user that the system is refreshing while you're disabling the user from deleting a new object until the previous transaction is completed.
I found two solutions:
if (holder.adapterPosition == -1) return // Race condition; do nothing
// else, do stuff
This does the trick. However, it is not elegant in my view, as: why receive clicking events to begin with if we are not supposed to? It doesn't seem to be solving the problem from its roots.
To solve it more elegantly (from its roots), I did this in the setOnClickListener:
holder.item.setOnClickListener {
// we don't want 2nd click right? so let's delete the listener
holder.item.setOnClickListener{}
/* then, do the stuff for this listener. this stuff
will be done once, as we deleted its listener earlier,
so, subsequent clicks are not possible. */
}
This way, the item with that listener is clicked on once, and a second click does not happen to begin with, hence a racing condition is not possible from its roots. Because the clicking listener is deleted right when the first click is received. Should I want to allow the item to get clicks again, I can redefine a listener for it again.
There is a situation where I have to scroll the recyclerview to last position. The problem starts where I am using Paging 3. I don't know what is the proper way to do it every time I submit data to adapter. by the way I have to say that I do not submit data once, but for example every time user submits a new comment to backend I have to get the new list from server and submit it.
This is what I tried. that's how I observe data:
private fun observeComments() {
exploreViewModel.commentsLiveData.observe(viewLifecycleOwner) { comments ->
commentsAdapter.submitData(viewLifecycleOwner.lifecycle, comments)
}
}
And this is how I tried to scroll the recyclreview to last item:
commentsAdapter.addLoadStateListener { loadStates ->
if(loadStates.refresh.endOfPaginationReached){
if(commentsAdapter.itemCount>1)
binding.rvComments.smoothScrollToPosition(commentsAdapter.itemCount-1)
}
}
I also set app:stackFromEnd="true" to recyclerview but this is not useful. whenever I submit a new comment and get the list, recyclerview scrolls to the middle, I thinks it scrolls to the first pages end. but I want it to be scrolled to the end of the list.
Any help or suggestion would be appreciated.
You can implement PagingSource.getRefreshKey() to control the key passed to .load() after invalidation and set initialKey in Pager() to control the key passed to initial call of .load().
Since your list is paginated, scrolling to the end doesn't work because you would need to sequentially load all pages until you get to your desired position, so instead the better strategy is to start loading from where you want the user's scroll position to start, then let paging prepend pages as they scroll back up.
I'm making an API call getData(forPage: Int): Response which returns a page-worth of data (10 items max) and thereIsMoreData: Boolean.
The recyclerView is implemented that by scrolling, the scroll listener automatically fetches more data using that API call:
val scrollListener = object : MyScrollListener() {
override fun loadMoreItems() {
apiFunctionForLoading(currentPage + 1)
}
}
The problem is that with longer screen devices that have more space for items (let's say 20), the RV receives 10 items and then doesn't allow scrolling, because there's no more items to scroll to. Without scrolling, more data cannot be loaded.
My naive solution:
load first set of data
if thereIsMoreData == true I load another page of data
now I have more data than the screen can display at once hence allowing scroll
Is there a more ellegant solution?
Android has this Paging Library now which is about displaying chunks of data and fetching more when needed. I haven't used it and it looks like it might be a bit of work, but maybe it's worth a look?
Codepath has a tutorial on using it and I think their stuff is pretty good and easy to follow, so maybe check that out too. They also have this older tutorial that's closer to what you're doing (handling it yourself) so there's that too.
I guess in general, you'd want your adapter to return an "infinite" number for getItemCount() (like Integer.MAX_VALUE). And then in your onBindViewHolder(holder, position) method you'd either set the item at position, or if you don't have that item yet you load in the next page until you get it.
That way your initial page will always have the right amount of content, because it will be full of ViewHolders that have asked for data - if there's more than 10, then item 11 will have triggered the API call. But actually handling the callback and all the updating is the tricky part! If you have that working already then great, but it's what the Paging library was built to take care of for you (or at least make it easier!)
An elegant way would be to check whether the view can actually scroll down:
recyclerView.canScrollVertically(1)
1 means downwards -> returns true if it is possible tro scroll down.
So if it returns false, your page is not fully filled yet.
My outer RecyclerView crashes either with
IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true...
or
IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
Like the title suggests I have an RecyclerView in the list item layout of the first RecyclerView. This layout is used to display messages and the
inner RecyclerView to display attachments that come with the message. The inner RecyclerViews visibility is set to either GONE or VISIBLE depending whether the message has any attachments or not. The simplified outer list item layout looks like this
ConstraintLayout
TextView
TextView
TextView
RecyclerView
And the part of the adapter that handles the inner RecyclerView looks like this
private fun bindFiles(message: Message?) = with(itemView) {
if (message != null && message.attachments.isNotEmpty())
{
sent_message_attachments.setAsVisible()
sent_message_attachments.layoutManager = GridLayoutManager(this.context,Math.min(message.attachments.size,3))
sent_message_attachments.adapter = AttachmentAdapter(message.attachments)
sent_message_attachments.itemAnimator = null
sent_message_attachments.setHasFixedSize(true)
}
else{
sent_message_attachments.setAsGone()
sent_message_attachments.adapter = null
sent_message_attachments.layoutManager = null
}
}
The bug has something to do with the way I fetch the attachments in the inner adapter since once I disable the part that start the download process, everything is fine. There's no problem when loading images from the device, but once I start the download process, everything goes to hell. This is the part that handles images and kicks off the download process in the inner adapter. I have functions for videos and for other file types that are pretty much the same exact thing but use slightly different layout.
private fun bindImage(item: HFile?) = with(itemView) {
if (item != null)
{
if (item.isOnDevice && !item.path.isNullOrEmpty())
{
if (item.isGif)
{
attachment_image.displayGif(File(item.path))
}
else
{
attachment_image.displayImage(File(item.path))
}
}
else
{
//TODO: Add option to load images manually
FileHandler(item.id).downloadFileAsObservable(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
//TODO: Show download process
},
{ error ->
error.printStackTrace()
//TODO: Enable manual retry
},
{ notifyItemChanged(adapterPosition)} //onComplete
)
}
}
}
I use the same structure as above in my DiscussionListAdapter to load discussion portraits (profile pictures etc.) and it does not have the same issue.
These are the extensions functions used to inflate the viewHolders and to display the images
fun ViewGroup.inflate(layoutRes: Int): View
{
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}
fun ImageView.displayGif(file:File){
GlideApp.with(context).asGif().load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
fun ImageView.displayImage(file:File){
GlideApp.with(context).load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
I've been on this for the past couple of days and just can't get my head around it. Any help in any direction is greatly appreciated. I know my explanations can be a bit all over the place so just ask for clarification when needed :)
UPDATE
I have now been able to produce this with a GridLayout as well as with RecyclerView. It's safe to assume that the nested RecyclerViews were not the culprit here. I even tried to ditch the Rx-piece that handled loading the images and created an IntentService for the process, but the same crashes still occur.
With GridLayout I mean that instead of having another adapter to populate the nested RecyclerView I use only one adapter to populate the message and to inflate and populate views for the attachments as well and to attach those views to the nested GridLayout.
The crash happens when I start to download a file and then scroll the view, that is supposed to show the downloaded file, out of the screen. That view should get recycled but for some reason the download process (which in my test cases only takes around 100ms-400ms) causes the app to throw one of the two errors mentioned in the original question. It might be worth noting that I'm using Realm and the adapter takes in a RealmResults<Message> list as it's dataset. My presenter looks for changes in the list and then notifies the adapter when needed (changed due to the implementation of IntentService).
This is how I'm capable to reproduce this time and time again:
Open a discussion that has messages with attachments
Start to scroll upwards for more messages
Pass a message with an attachment and scroll it off screen while it's still loading
Crash
There is no crash if I stop and wait for the download to complete and everything works as intended. The image/video/file gets updated with a proper thumbnail and the app wont crash if I scroll that out of view.
UPDATE 2
I tried swapping the nested ViewGroup for a single ImageView just to see is the problem within the nestedness. Lo and behold! It still crashes. Now I'm really confused, since the DiscussionListAdapter I mentioned before has the same exact thing in it and that one works like a charm... My search continues. I hope someone, some day will benefit from my agony.
UPDATE 3
I started to log the parent of every ViewHolder in the onBindViewHolder() function. Like expected I got nulls after nulls after nulls, before the app crashed and spew this out.
04-26 21:54:50.718 27075-27075/com.hailer.hailer.dev D/MsgAdapter: Parent of ViewHolder: android.view.ViewOverlay$OverlayViewGroup{82a9fbc V.E...... .......D 0,0-1440,2168}
There's a method to my madness after all! But this just poses more questions. Why is ViewOverlay used here? As a part of RecyclerView or as a part of the dark magicians plans to deprive me of my sanity?
Sidenote
I went digging into RecyclerViews code to check if I could find a reason for the ViewOverlaymystery. I found out that RecyclerView calls the adapters onCreateViewHolder() function only twice. Both times providing itself as the parent argument for the function. So no luck there... What the hell can cause the item view to have the ViewOverlay as it's parent? The parent is an immutable value, so the only way for the ViewOverlay to be set as the parent, is for something to construct a new ViewHolder and supply the ViewOverlay as the parent object.
UPDATE 4
Sometimes I amaze myself with my own stupidity. The ViewOverlay is used because the items are being animated. I didn't even consider this to be an option since I've set the itemAnimator for the RecyclerView as null, but for some odd reason that does not work. The items are still being animated and that is causing this whole charade. So what could be the cause of this? (How I chose to ignore the moving items, I do not know, but the animations became very clear when I forced the app to download same picture over and over again and the whole list went haywire.)
My DiscussionInstanceFragment contains the RecyclerView in question and a nested ConstraintLayout that in turn contains an EditText for user input and a send button.
val v = inflater.inflate(R.layout.fragment_discussion_instance, container, false)
val lm = LinearLayoutManager(context)
lm.reverseLayout = true
v.disc_instance_messages_list.layoutManager = lm
v.disc_instance_messages_list.itemAnimator = null
v.disc_instance_messages_list.adapter = mPresenter.messageAdapter
This is the piece that handles the initialization of the RecyclerView. I'm most definitely setting the itemAnimator as null, but the animations just wont stop! I've tried setting the animateLayoutChanges xml attribute on the root ConstraintLayout and on the RecyclerView but neither of them worked. It's worth mentioning that I also checked whether the RecyclerView had an itemAnimator in different states of the program, and every time I check the animator, it is null. So what is animating my RecyclerView?!
I have faced the same issue
Try this in your child RecyclerView it works for me
RecyclerView childRC = itemView.findViewById(R.id.cmol_childRC);
layoutManager = new LinearLayoutManager(context);
childRC.setItemAnimator(null);
childRC.setLayoutManager(layoutManager);
childRC.setNestedScrollingEnabled(false);
childRC.setHasFixedSize(true);
now set your Adapter like this
ArrayList<Model> childArryList = new ArrayList<>();
childArryList.addAll(arrayList.get(position).getArrayList());
ChildOrderAdapter adapter = new ChildOrderAdapter(context, childArryList);
holder.childRC.swapAdapter(adapter, true);
hope this helps
I finally figured out what was causing this. In my DiscussionInstanceView I have a small view that is animated into and out of view with ConstraintLayout keyframe animations. This view only shows the download progress of the chat history and is used only once, when the discussion is first opened. BUT since I had a call to hiding that view every time my dataset got updated, I was forcing the ConstraintLayout to fire of an animation sequence thus making everything animate during the dataset update. I just added a simple check whether I was downloading the history or not and this problem got fixed.
I am trying to follow what is going on with the boolean variable, hasMoreData with EndlessAdapter and why is seems to be prematurely turning false.
Let me start from beginning to run through what happens. Note: I am using a task and setRunInBackground(false);
I start off setting my list and setting the adapter:
profileList = new ArrayList<ProfileReview>();
endlessAdapter = new EndlessProfileAdapter(getActivity(), profileList);
endlessAdapter.setRunInBackground(false);
listView.setAdapter(endlessAdapter);
Sidenote: Not sure if this is correct, but it seems I am setting the list with an empty adapter.
The first thing that appears to happen after adapter is set is the method cacheInBackground(), where my profileList size is zero, so it sets 0 as int startPoint when calling my AsyncTask where hasMoreData is set to true. Meanwhile, in this (cache) method, hasMoreData returns true. Not sure why? Because the list is zero in size? Or because its still associated with the default value of true?
In the task, it grabs first 10 items.
Then as user scrolls, the thobber starts spinning. And next 10 are displayed. Log.d tells me that profileList.size() is now 10 and hasMoreData is therefore false.
public void onItemsReady(ArrayList<ProfileReview> data) {
profileList.addAll(data);
endlessAdapter.onDataReady();
hasMoreData = profileList.isEmpty(); \\ Log.'d this out
}
My questions: My list starts with 10 items, users scrolls, it grabs 10 more. Then stops after a total of 20 items (or when hasMoreData == false.) But I have many more items to pull from. How do I keep hasMoreData == true? What is the trigger for this? Obviously the trigger is list size (I think?), and why would the list size ever be 0 once it starts to grab data? (until the end of course)
Not sure if this is correct, but it seems I am setting the list with an empty adapter.
EndlessAdapter is definitely designed to start with a non-empty adapter. In fact, it is designed assuming that the user must scroll to get it to load more data. Behavior in your current approach is unspecified, and I do not recommend that approach. Please load some data, then populate the list once your first batch of data is ready.
Meanwhile, in this (cache) method, hasMoreData returns true. Not sure why? Because the list is zero in size? Or because its still associated with the default value of true?
Since EndlessAdapter does not have a hasMoreData method. A search of the source code to EndlessAdapter turns up nothing named hasMoreData. Heck, the only places the word "more" appears is in comments.
A sample app has a hasMoreData value. Since you are not using this sample app, I cannot help you with random data members of random classes in your own code.
In the sample app, in EndlessAdapterCustomTaskFragment, I use a data member named hasMoreData. This is a boolean value, designed to be returned from cacheInBackground(). The responsibility of cacheInBackground() is to return true if we should continue to load data (after the current batch just loaded), false otherwise. In the case of this sample app, hasMoreData is populated by the call to onItemsReady(), itself triggered by onPostExecute() of the AsyncTask simulating loading some data. hasMoreData is set to true or false depending upon whether the items collection is empty, so it basically does a single load of additional data, then calls it quits.
But that is the behavior of a sample app. I didn't even write most of this class -- it came as a patch adding in support for your own data-fetching task. Do not consider sample code to be anything more than a sample.
Hence, you need to set your hasMoreData value to whatever makes sense for your application logic to serve whatever role you decided to use hasMoreData for. If hasMoreData has the same role in your code as it does in the sample, leave it true until you have determined that you are out of data, then set it false.