Is there any way to simulate a click on a RecyclerView item with Robolectric?
So far, I have tried getting the View at the first visible position of the RecyclerView, but that is always null. It's getChildCount() keeps returning 0, and findViewHolderForPosition is always null. The adapter returns a non-0 number from getItemCount() (there are definitely items in the adapter).
I'm using Robolectric 2.4 SNAPSHOT.
Seems like the issue was that RecyclerView needs to be measured and layed out manually in Robolectric. Calling this solves the problem:
recyclerView.measure(0, 0);
recyclerView.layout(0, 0, 100, 10000);
With Robolectric 3 you can use visible():
ActivityController<MyActivity> activityController = Robolectric.buildActivity(MyActivityclass);
activityController.create().start().visible();
ShadowActivity myActivityShadow = shadowOf(activityController.get());
RecyclerView currentRecyclerView = ((RecyclerView) myActivityShadow.findViewById(R.id.myrecyclerid));
currentRecyclerView.getChildAt(0).performClick();
This eliminates the need to trigger the measurement of the view by hand.
Expanding on Marco Hertwig's answer:
You need to add the recyclerView to an activity so that its layout methods are called as expected. You could call them manually, (like in Elizer's answer) but you would have to manage the state yourself. Also, this would not be simulating an actual use-case.
Code:
#Before
public void setup() {
ActivityController<Activity> activityController =
Robolectric.buildActivity(Activity.class); // setup a default Activity
Activity activity = activityController.get();
/*
Setup the recyclerView (create it, add the adapter, add a LayoutManager, etc.)
...
*/
// set the recyclerView object as the only view in the activity
activity.setContentView(recyclerView);
// start the activity
activityController.create().start().visible();
}
Now you don't need to worry about calling layout and measure everytime your recyclerView is updated (by adding/removing items from the adapter, for example).
Just invoke
Robolectric.flushForegroundThreadScheduler()
before performClick() to ensure that all ui operations (including measure and layout phases of recycler view after populating with the dataset) are finished
Related
I have a RecyclerView and related Adapter class. And I changed the GridLayoutManager's span count.
fun switchLayout() {
if (layoutManager?.spanCount == 1) {
layoutManager.spanCount = 5
} else {
layoutManager?.spanCount = 1
}
notifyItemRangeChanged(0, itemCount)
}
And then some animation comes up like below
It's interesting but I don't need it. So, How can I remove this animation?
instead of notifyItemRangeChanged(0, itemCount) just use notifyDataSetChanged() - first one is causing animations, second one will force-redraw whole grid from scratch (without respecting previous positions of items, thus moving animations)
for disabling all animations (for adding new item, removing, changing positions/order of 2 or more) you can use recyclerView.setItemAnimator(null) (like comment under question pointing out). then you can still use all notifyItem... methods, changes won't be animated.
or you can also define own custom ItemAnimator with disabled some of animations. e.g. in your case: animateChange should contain line dispatchAnimationFinished(viewHolder) and then return false. in HERE some nice template to use. your class may extends DefaultItemAnimator and then you may override only methods/animations which you want to disable/change
I'm creating a slideshow with ViewPager2. For example, the slideshow has 3 items and I want to show the second item when the activity opens. I use setCurrentItem(int item, boolean smoothScroll) method but it doesn't work and nothing happens. How can I achieve it?
viewPager.adapter = adapter
viewPager.setCurrentItem(1, true)
I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g
viewPager.post {
viewPager.setCurrentItem(1, true)
}
setCurrentItem(int item, boolean smoothScroll) works correctly in ViewPager but in ViewPager2 it does not work as expected. Finally, I faced this problem by adding setCurrentItem(int item, boolean smoothScroll) method into a delay like this:
Handler().postDelayed({
view.viewPager.setCurrentItem(startPosition, false)
}, 100)
Do not use timers, you will run into a lot of probable states in which the user has a slow phone and it actually takes a lot longer than 100 ms to run, also, you wouldn't want too slow of a timer making it ridiculously un-reliable.
Below we do the following, we set a listener to our ViewTreeObserver and wait until a set number of children have been laid out in our ViewPager2's RecyclerView (it's inner working). Once we are sure x number of items have been laid out, we start our no-animation scroll to start at the position.
val recyclerView = (Your ViewPager2).getChildAt(0)
recyclerView.apply {
val itemCount = adapter?.itemCount ?: 0
if(itemCount >= #(Position you want to scroll to)) {
viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
// False for without animation scroll
(Your ViewPager2).scrollToPosition(#PositionToStartAt, false)
}
}
}
First off, I think that the accepted answer shouldn't be #hosseinAmini 's, since it's suggesting to use a delay to work around the problem. You should first be looking for what the assumed bug is caused by, rather than trusting unreasonable solutions like that.
#Rune's proposal is correct, instead; so I'm quoting their code in my answer:
viewPager.post {
viewPager.setCurrentItem(1, true)
}
The only thing I'd argue about is the aforementioned one's belief that their solution is just deferring the execution of that lambda in the next run cycle. This wouldn't make anything buggy work properly. Rather, what it is actually being done is deferring the execution of that lambda to once the view has been attached to a window, which implies it's also been added to a parent view. Indeed, there looks to be an issue as to changing the current ViewPager2 item before being attached to a window. Some evidence to support this claim follows:
Using whichever Handler won't work nearly as effectively.
Handler(Looper.getMainLooper()).post {
viewPager.setCurrentItem(1, true) // Not working properly
}
From a theoretical standpoint, it might incidentally work due to the ViewPager2 being attached to a window acquiring priority in the message queue of the main looper, but this shouldn't ever be relied upon as there's just no guarantee that it'll work (it's even more likely it won't) and if it even turned out to be working, further investigation running multiple tests should make my point clear.
View.handler gets null, which means the view hasn't been attached to any window yet.
View.handler // = null
Despite Android UI being tied to the main looper, which will always uniquely correspond to the main thread –hence also called the UI thread,– a weird design choice stands in the handler not being associated to each view until they get attached to a window. A reason why this may lay on the consequent inability of views to schedule any work on the main thread while they're not part of the hierarchy, which may turn useful when implementing a view controller that schedules view updates while unaware of their lifecycle (in which case it would employ the View's handler, if any — or just skip scheduling whatever it was going to if none).
EDIT:
Also, #josias has pointed out in a comment that it'd be clearer to use:
viewPager.doOnAttach {
viewPager.setCurrentItem(1, true)
}
Thanks for that suggestion! It expresses better the actual intent, rather than relying on the behavior of the View.post method.
Do not use timers and all that stuff with 'post', it's not the reliable solution and just a piece of code that smells.
Instead, try use viewPager.setCurrentItem(1, false). That 'false' is about smoothScroll, you can't smooth scroll your viewPager2 when your activity is just opened. Tested it on a fragment in onViewCreated() method, it also didn't work with "true", but works with "false"
As it was mentioned above you have to use setCurrentItem(position, smoothScroll) method on ViewPager2 in order to show selected item. To make it work you have to define a callback, here is an example:
ViewPager2.OnPageChangeCallback callback = new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
};
And then you have to register it as follow:
viewPager.registerOnPageChangeCallback(callback);
Also do not forget to unregister it:
viewPager.unregisterOnPageChangeCallback(callback);
When you call setCurrentItem(position) method it will call onPageSelected(int position) method from your callback passing your argument, and then method createFragment(int position) from FragmentStateAdapter class will be called to show your fragment.
I tried changing viewpager2 page in Handler().dely() and viewPager2.post{} and even 'viewPager2.get(0).post all didn't work for me, I'm using ViewPager with FragmentStateAdapter with Tablayout.
What worked for me is changing the position of the RecylerView in ViewPager2 after binding FragmentStateAdapter to yourViewPager2View.adapter manually:
(yourViewPager2View[0] as RecyclerView).scrollToPosition(moveToTabNumber)
Why
My problem is onCreateFragment(position:Int):Fragmeet function in FragmentStateAdapter always starting fragment at 0 position no matter what pageNumber I set the page
viewPager.setCurrentItem = pageNumber
I checked where it's called in FragmentStateAdapter it's called in FragmentStateAdapter:
onBindViewHolder(final #NonNull FragmentViewHolder holder, int position)`
so all I needed is to force onBindViewHolder to call onCreateFragment(position:Int) with the page number I wanted.
mViewPager.setCurrentItem(1, true); ---> this is sufficient as you written above
That should work,
in doubt, just check your position:
#Override
public void onPageSelected(int i) {
if (LOG_DEBUG) Log.v(TAG, " ++++++++ onPageSelected: " + i);
mViewPager.setCurrentItem(i);
//TODO You can use this position: to write other dependent logic
}
and also check
getItem(int position) in PagerAdapter
or else paste your code.
I noticed that it works fine when the view is initially created if you opt to not animate it.
viewPager2.setCurrentItem(index, false)
This is usually fine depending on your use case - this initial/default item probably doesn't need to be animated in.
I met the same problem. In my case, I make the viewPager2 Gone by default until network requests succeed, I fix it by setting the CurrentItem after I make the viewPager2 visible.
My answer may not be helpful now but i see no harm to post my expreince, i just came to this problem using ViewPager and ViewPager2 and unexpectedly solved it by just changing some line codes order.
Here is (java) solution for ViewPager:
reviewWordViewPager.addOnPageChangeListener(changeListener);
reviewWordViewPager.setCurrentItem(viewPosition, true/false);
reviewWordTabIndicator.setupWithViewPager(reviewWordViewPager, true);
(Java) solution for ViewPager2:
wordViewPager.registerOnPageChangeCallback(viewPager2OnPageChangeCallback);
wordViewPager.setCurrentItem(vpPosition, true/false);
new TabLayoutMediator(tabIndicator, wordViewPager,
((tab, position) -> tab.setText(viewPagerTitle[position]))).attach();
I did not look up for ViewPager2 whether it needs the following old code used in ViewPager
#Override
public int getItemPosition(#NonNull Object object) {
// refresh all fragments when data set changed
return POSITION_NONE;
}
But surprisingly no need for it in ViewPager2 to solve the problem i've been having, hope it helps others
In case you use context.startActivity to start new activities no need to use wordViewPager.setCurrentItem(item, smoothScroll) in your onResume function to get back to the last selected tab before you started new activity you just save ViewPager/ViewPager2 position like vpPisition = wordViewPager.getCurrentItem(); in onStop function.
vpPisition is a global variable.
as #Daniel Kim but a java version
RecyclerView rvOfViewPager2 = (RecyclerView) viewPager2.getChildAt(0);
rvOfViewPager2.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
rvOfViewPager2.getViewTreeObserver().removeOnGlobalLayoutListener(this);
viewPager2.setCurrentItem(currentTabId, false);
}
});
First You need to Initilaze the Main activity under any listener or button You want then After that You need to put this Line..
here MainActvity is the Viewpager Main Class You are using and and 2 is the position where you want to move
MainActivity main = (MainActivity ) mContext;
main.selectTab(2, true);
When an item is added or removed using DiffUtil, inside android.support.v7.util.AdapterListUpdateCallback only the notifyItemRangeInserted(1, 1) or notifyItemRangeRemoved(1,1) respectively is invoked, notifyItemRangeChanged() is not invoked. I set the positions on each of the views using setTag(position) but are not getting updated for the existing items. Wouldn't prefer to update items manually.
list update:
private void updateItems(final List<Feed> newPosts) {
List<Feed> olderPosts = new ArrayList<>(currentPosts);
final CustomDiffCallback DIFF_CALLBACK = new CustomDiffCallback(olderPosts, newPosts);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(DIFF_CALLBACK, true);
}
Turns out that we can't use setTag() and getTag() for passing in position to the views. Since the main purpose of DiffUtil is to minimize the updations for views that haven't changed, the views that already have positions set on them cannot get through an updation pass hence would return stale positions. Though In my case, I had the custom listener in a separate class, the update part was a bit unusual. I had to get getLayoutPosition() of ViewHolder. Also can use getAdapterPosition() for the same.
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.
This is a question about RecyclerView internal behavior for someone that knows its mechanics or is willing to dig into the source code. I’d like an answer backed up by references to the source.
Original question
(scroll down to ‘In other words’ for a more focused question)
I need to understand how notify* actions (for example, notifyItemInserted()) are enqueued. Imagine I have an adapter backed up by this list:
ArrayList<String> list = Arrays.asList("one", "three", "four");
I want to add the values zero and two, that are missing.
Example 1
list.add(1, "two");
// notify the view
adapter.notifyItemInserted(1);
// Seconds later, I go on with zero
list.add(0, "zero");
// notify the view
adapter.notifyItemInserted(0);
This is pretty straightforward and clear, nothing to tell.
Example 2
But what if the two actions are very close to each other, and there’s no layout pass in between?
list.add(1, "two");
list.add(0, "zero”);
What should I do now?
adapter.notifyItemInserted(1);
adapter.notifyItemInserted(0);
Or maybe
adapter.notifyItemInserted(2);
adapter.notifyItemInserted(0);
? From the adapter perspective, the list immediately switched from one, three, four to zero, one, two, three, four so the second option seems more reasonable.
Example 3
list.add(0, “zero”);
adapter.notifyItemInserted(0);
list.add(2, “two”);
adapter.notifyItemInserted(...)
What about it now? 1 or 2 ? The list was updated immediately after, but I am sure there was no layout pass in between.
Question
You got the main issue, and I want to know how should I behave in these situations. The real case is that I have multiple asynchronous tasks ending up in an insert() method. I can enqueue their operations, but:
I don’t want to do that if there’s already an internal queue, and there surely is
I don’t know what happens if two actions happen without a layout pass in between, see Example 3.
In other words
To update recycler, 4 actions must happen:
I actually alter the data model (e.g. insert something into the backing array)
I call adapter.notify*()
Recycler receives the call
Recycler performs the action (e.g. calls getItem*() and onBind() on the adapter) and lays out the change.
It’s easy to understand this when there’s no concurrency, and they happen in sequence:
1. => 2. => 3. => 4. => (new update) 1. => 2. => 3. => 4. ...
Let’s see what happens between steps.
Between 1. and 2.: I would say it is the developer responsibility to call notify() immediately after having altered the data. That’s OK.
Between 2. and 3.: This happens immediately, no issue here.
Between 3. and 4.: This does not happen immediately! AFAIK. So it perfectly possible that a new update (steps 1 and 2) comes between steps 3 and 4 of the previous update.
I want to understand what happens in this case.
How should we behave?
Should I ensure that step 4 of the previous update did took place before inserting new stuff? If so how?
I thought about similar questions before, and I decided:
If I want to insert more than 1 item directly to end of list and
want to get a animation for all, I should:
list.add("0");
list.add("1");
adapter.notifyItemRangeInserted(5, 2); // Suppose there were 5 items before so "0" has index of 5 and we want to insert 2 items.
If I want to insert more than 1 item directly to end of list, but
want to get separated animation for each inserted item, I should:
list.add("0");
list.add("1");
adapter.notifyItemInserted(0);
mRecyclerView.postDelayed(new Runnable() {
#Override
public void run() {
// before this happens, Be careful to call other notify* methods. Never call notifyDataSetChanged.
adapter.notifyItemInserted(1);
}
}, mRecyclerView.getItemAnimator().getAddDuration());
If I want to insert more than 1 item to different position of list,
similar as 2.
Hope this can help.
So lets start from little intro to RecyclerView works with notify items. And works pretty simple with other list of saved ViewGroup items (ListView for ex.)
RecyclerView has Queue of View Items which already drawn. And doesn't know about any your updates, without calling notify(...) methods. When you added new Items and notify RecyclerView, it starts cycle for checking all Views one by one.
RecyclerView contains and drawn next objects
View view-0 (position 0), view-1 (position 1), View-2 (position 2)
// Here is changes after updating
You added Item View view-new into (position 1) and Notify
RecyclerView starts loop to check changes
RecyclerView received unmodified view-0(position-0) and left them;
RecyclerView found new item view-new(position 1)
RecyclerView removing old item view-1(position 1)
RecyclerView drawing new item view-new(position 1)
// In RecyclerView queue in position-2 was item view-2,
// But now we replacing previous item to this position
RecyclerView found new item view-1 (new position-2)
RecyclerView removing old item view-2(position 2)
RecyclerView drawing new item view-1(position 2)
// And again same behavior
RecyclerView found new item view-3 (new position-3)
RecyclerView drawing new item view-1(position 2)
// And after all changes new RecyclerView would be
RecyclerView contains and drawn next objects
View view-0 (position 0), view-new (position 1) view-1 (position 2), View-2 (position 3)
It's just main flow of working notify functions, but what should know all this actions happens on UI Thread, Main Thread, even you can calling updating from Async Tasks. And answering you 2 Question - You can call Notify to the RecyclerView as much as you want, and make sure, you action would be on the correct Queue.
RecyclerView works correct in any usage, more complicated questions would be to your Adapter work. First of all, you need to synchronize you Adapter action, like adding removing items, and totally refuse of index usage. For example, it's would be better for your Example 3
Item firstItem = new Item(0, “zero”);
list.add(firstItem);
adapter.notifyItemInserted(list.indexOf(firstItem));
//Other action...
Item nextItem = new Item(2, “two”);
list.add(nextItem);
adapter.notifyItemInserted(list.indexOf(nextItem))
//Other actions
UPDATE |
Related to RecyclerView.Adapter Doc, where you can see functions same with notifyDataSetChanged(). And where this RecyclerView.Adapter invokes child items with android.database.Observable extensions, see more About Observable. Access to this Observable Holder is synchronized, until View Element in RecyclerView release usage.
See also RecyclerView from support library version 25.0 Lines 9934 - 9988;
It should not be a problem if you make multiple updates between layout passes. The RecyclerView is designed to handle (and optimize) this case :
RecyclerView introduces an additional level of abstraction between the
RecyclerView.Adapter and RecyclerView.LayoutManager to be able to
detect data set changes in batches during a layout calculation. [...]
There are two types of position related methods in RecyclerView:
layout position: Position of an item in the latest layout calculation. This is the position from the LayoutManager's
perspective.
adapter position: Position of an item in the adapter. This is the position from the Adapter's perspective.
These two positions are the same except the time between dispatching
adapter.notify* events and calculating the updated layout.
In your case the steps are :
You update the data layer
You call adapter.notify*()
The recyclerview record the change (in AdapterHelper.mPendingUpdates if I understand the code correctly). This change will be reflected in ViewHolder.getAdapterPosition(), but not yet in ViewHolder.getLayoutPosition().
At some point the recyclerView apply the recorded changes, basically it reconcile the layout's point of view with the adapter's point of view. It seems that this can happen before the layout pass.
The 1., 2., 3. sequence can happen any number of times as long as 2. immediately follows 1. (and both happen on the main thread).
(1. => 2. => 3.) ... (1. => 2. => 3.) ... 4.
Item firstItem = new Item(0, “zero”);
list.add(firstItem);
adapter.notifyItemInserted(list.indexOf(firstItem));
//Other action...
Item nextItem = new Item(2, “two”);
list.add(nextItem);
adapter.notifyItemInserted(list.indexOf(nextItem))