diff --git a/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java b/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java index ee138a5a6eb..28be3b20bdf 100644 --- a/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java +++ b/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java @@ -5046,6 +5046,49 @@ public void testOnAttachedOnDetachedWithViewportChanged() { } } + @Test + public void testItemsOnDetached() { + final int childHeightPx = 20; + final int widthPx = 200; + final int heightPx = 200; + + mRecyclerBinder = new RecyclerBinder.Builder().rangeRatio(RANGE_RATIO).build(mComponentContext); + final RecyclerView rv = mock(RecyclerView.class); + mRecyclerBinder.mount(rv); + + final List renderInfos = new ArrayList<>(); + final List components = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + final TestComponent component = + TestAttachDetachComponent.create(mComponentContext).heightPx(childHeightPx).build(); + components.add(component); + final Component child = Column.create(mComponentContext).child(component).build(); + renderInfos.add(ComponentRenderInfo.create().component(child).build()); + } + mRecyclerBinder.insertRangeAt(0, renderInfos); + mRecyclerBinder.notifyChangeSetComplete(true, NO_OP_CHANGE_SET_COMPLETE_CALLBACK); + + Size size = new Size(); + int widthSpec = SizeSpec.makeSizeSpec(widthPx, SizeSpec.EXACTLY); + int heightSpec = SizeSpec.makeSizeSpec(heightPx, SizeSpec.EXACTLY); + mRecyclerBinder.measure(size, widthSpec, heightSpec, null); + + mLayoutThreadShadowLooper.runToEndOfTasks(); + ShadowLooper.runUiThreadTasks(); + + mRecyclerBinder.detach(); + + final int rangeSize = heightPx / childHeightPx; + final int rangeStart = 0; + final int rangeTotal = (int) (rangeSize + (RANGE_RATIO * rangeSize)); + for (int i = rangeStart; i <= rangeStart + rangeTotal; i++) { + assertThat(components.get(i).wasOnDetachedCalled()).isTrue(); + } + for (int i = rangeStart + rangeTotal + 1; i < 200; i++) { + assertThat(components.get(i).wasOnDetachedCalled()).isFalse(); + } + } + private RecyclerBinder createRecyclerBinderWithMockAdapter(RecyclerView.Adapter adapterMock) { return new RecyclerBinder.Builder() .rangeRatio(RANGE_RATIO) diff --git a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java index b2327423490..9061f293d5c 100644 --- a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java +++ b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java @@ -42,6 +42,7 @@ import com.facebook.litho.annotations.LayoutSpec; import com.facebook.litho.annotations.OnCreateInitialState; import com.facebook.litho.annotations.OnCreateLayout; +import com.facebook.litho.annotations.OnDetached; import com.facebook.litho.annotations.OnEvent; import com.facebook.litho.annotations.OnTrigger; import com.facebook.litho.annotations.OnUpdateState; @@ -399,6 +400,11 @@ static void onScroll( sectionTree.requestFocusOnRoot(position); } + @OnDetached + static void onDetached(ComponentContext c, @State Binder binder) { + binder.detach(); + } + private static class RecyclerCollectionOnScrollListener extends OnScrollListener { private final RecyclerCollectionEventsController mEventsController; diff --git a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java index 57af85943d9..719ef4d4bf6 100644 --- a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java +++ b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java @@ -202,6 +202,11 @@ public boolean supportsBackgroundChangeSets() { return mUseBackgroundChangeSets; } + @Override + public void detach() { + mRecyclerBinder.detach(); + } + public void clear() { if (mUseBackgroundChangeSets) { mRecyclerBinder.clearAsync(); diff --git a/litho-widget/src/main/java/com/facebook/litho/widget/Binder.java b/litho-widget/src/main/java/com/facebook/litho/widget/Binder.java index 1e6f559fca0..efb9f56cb32 100644 --- a/litho-widget/src/main/java/com/facebook/litho/widget/Binder.java +++ b/litho-widget/src/main/java/com/facebook/litho/widget/Binder.java @@ -101,4 +101,7 @@ void measure( * view and the first item will need to be measured to determine the height of the view. */ void setCanMeasure(boolean canMeasure); + + /** Detach items under the hood. */ + void detach(); } diff --git a/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java b/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java index d4c14375675..7146897f78f 100644 --- a/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java +++ b/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java @@ -633,6 +633,20 @@ public void setCanMeasure(boolean canMeasure) { mCanMeasure = canMeasure; } + @Override + public void detach() { + final List toDetach = new ArrayList<>(); + synchronized (this) { + for (int i = 0, size = mComponentTreeHolders.size(); i < size; i++) { + toDetach.add(mComponentTreeHolders.get(i)); + } + } + + for (int i = 0, size = toDetach.size(); i < size; i++) { + toDetach.get(i).acquireStateAndReleaseTree(); + } + } + @UiThread public void notifyItemRenderCompleteAt(int position, final long timestampMillis) { final ComponentTreeHolder holder = mComponentTreeHolders.get(position);