Status Update
Comments
je...@google.com <je...@google.com> #2
This is actually happening in a lot of cases beyond just stable IDs; that just happens to be a particularly easy case to repro it on. Running the RecyclerView test suite on the commit in aosp/2345032 (which validates the intended invariants around temp-detached ViewHolders) will reveal the full extent (thousands of failing tests).
There are two possible ways to fix this:
-
When binding a temporarily detached ViewHolder, temporarily re-attach it, perform the bind operation, then immediately temporarily detach it again. This is implemented as aosp/2366713 and passes all tests. I favor this solution, as it's very, very simple. There's not much in the way of potential to go wrong, confuse a third-party LayoutManager, have incorrect bookkeeping, etc. The only reason not to do this is that it performs more temp detaches/reattaches than is strictly necessary, though this is a sufficiently lightweight operation that we currently temp-detach every view on every layout. If this were a bottleneck, I'd think that'd be a place to look at as well.
-
When binding a temporarily detached ViewHolder, temporarily re-attach it, marking it as being temporarily reattached for binding. If the LayoutManager attaches the View manually, simply clear the flag. At the end of layout, go through the scrap views and properly handle these. I do not favor this approach, as it has many possible edge cases (for example, the LayoutManager might add the View somewhere other than the end, which isn't yet handled in the implementation). I've implemented the basics of this as aosp/2371868 as a proof of concept (though one test is currently failing—I am a bit surprised it's not more). I don't want to spend much more time on it, though, as I favor approach #1 absent a reason not to do that.
After discussion, we've decided on approach 1.
sh...@google.com <sh...@google.com> #3
Branch: androidx-main
commit ed0e0d25ef93ae87ee6e5910364d902d755b9330
Author: Ryan Mentley <ryanmentley@google.com>
Date: Tue Dec 13 00:20:00 2022
Temporarily re-attach temp-detached views for binding, add validation of various intended invariants
Test: Existing test suite should not fail with added invalidation (it does fail very hard without the fix)
Fixes: 258144648
Fixes: 265347515
Change-Id: I7244f2c749238c7241f36e57fdec155b1bea77cf
M recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
je...@google.com <je...@google.com> #4
For AndroidX, the Design assumption violated.
in
As of 2023-03-06,
@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
final long itemId = holder.getItemId();
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null && boundItemId != itemId) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
ensureFragment(position);
/** Special case when {@link RecyclerView} decides to keep the {@link container}
* attached to the window, but not to the view hierarchy (i.e. parent is null) */
final FrameLayout container = holder.getContainer();
if (ViewCompat.isAttachedToWindow(container)) {
if (container.getParent() != null) {
throw new IllegalStateException("Design assumption violated."); // <================ FAILS HERE
}
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (container.getParent() != null) {
container.removeOnLayoutChangeListener(this);
placeFragmentInViewHolder(holder);
}
}
});
}
gcFragments();
}
The error has shown up
I don't know whether the original AndroidX was making bad assumptions on Android, or that
Note that this is impacting Chrome autoroll for AndroidX (
je...@google.com <je...@google.com>
ap...@google.com <ap...@google.com> #5
Hmm I cannot CC jgielzak@ (but I can still add comment?!). Pinging him directly instead.
je...@google.com <je...@google.com> #6
It looks like this code in ViewPager2 is a workaround for the bug that's fixed in this change.
Specifically, it looks to be working around the fact that onBindViewHolder
was previously called with temporarily detached views, and using this listener to wait for it to be properly attached so that it can perform operations that rely on it being properly attached.
The previous state was bad precisely because it forced hacky workarounds like this due to the lack of a proper way to listen for changes in temp-detached state (there are listeners, but they don't work, and would likely require changes in both RecyclerView and framework to make them work...it's a very flawed design). Until seeing this workaround, I wasn't aware it was even possible at all to figure it out (and even this has some limitations that probably aren't really relevant to VP2's use-case).
I think the correct solution here is to remove this workaround in ViewPager2 and release new RecyclerView and ViewPager2 versions in parallel to address this, with release notes advising users to upgrade. Jakub, what do you think?
(also, I think I vaguely recall hearing that it's possible to do some sort of Gradle module metadata thing where we can also add the information that VP2 should be at least a certain version if being used with a given RV version? I'm not sure if/how we could do that, though)
ap...@google.com <ap...@google.com> #7
Thanks for the explanation!
I'm wondering if there will be versioning issues re. Android platform support vs. AndroidX? (NVM, this is all AndroidX).
Description
The reasoning is that the padding defines the content rectangle, and whether or not children's pixels are clipped to that rectangle or not doesn't change the semantics of that rectangle.
Consider for example a horizontal ViewPager2 that has unequal padding on the left and the right. When it is in idle state, a page exactly fills the rectangle described by the inner edge of the padding, so the page looks to be off-center by design. When snapping kicks in (e.g., because of a swipe), the page to which it is snapping should end up in that same "off-center" position, exactly matching the rectangle described by the inner edge of the padding, regardless of the value of clipToPadding.