Skip to content

Commit

Permalink
Add RecyclerViewCompositionStrategy.DisposeOnRecycled for re-using mo…
Browse files Browse the repository at this point in the history
…re compositions in RecyclerView
  • Loading branch information
rubensousa committed Jan 28, 2024
1 parent 808f087 commit fbd993a
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 12 deletions.
11 changes: 11 additions & 0 deletions dpadrecyclerview-compose/api/dpadrecyclerview-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ public class com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolder : com
public fun Content (Ljava/lang/Object;ZZLandroidx/compose/runtime/Composer;I)V
}

public final class com/rubensousa/dpadrecyclerview/compose/RecyclerViewCompositionStrategy {
public static final field $stable I
public static final field INSTANCE Lcom/rubensousa/dpadrecyclerview/compose/RecyclerViewCompositionStrategy;
}

public final class com/rubensousa/dpadrecyclerview/compose/RecyclerViewCompositionStrategy$DisposeOnRecycled : androidx/compose/ui/platform/ViewCompositionStrategy {
public static final field $stable I
public static final field INSTANCE Lcom/rubensousa/dpadrecyclerview/compose/RecyclerViewCompositionStrategy$DisposeOnRecycled;
public fun installFor (Landroidx/compose/ui/platform/AbstractComposeView;)Lkotlin/jvm/functions/Function0;
}

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.recyclerview.widget.RecyclerView
Expand All @@ -28,6 +29,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers
import com.google.common.truth.Truth.assertThat
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
import com.rubensousa.dpadrecyclerview.ExtraLayoutSpaceStrategy
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions
import org.junit.Rule
Expand All @@ -48,6 +50,15 @@ class DpadComposeViewHolderTest {

assertFocus(item = 2, isFocused = false)
assertSelection(item = 2, isSelected = false)

KeyEvents.pressDown()
waitForIdleScroll()

assertFocus(item = 0, isFocused = false)
assertSelection(item = 0, isSelected = false)

assertFocus(item = 1, isFocused = true)
assertSelection(item = 1, isSelected = true)
}

@Test
Expand Down Expand Up @@ -107,7 +118,7 @@ class DpadComposeViewHolderTest {
}

@Test
fun testCompositionIsCleared() {
fun testCompositionIsClearedWhenClearingAdapter() {
val viewHolders = ArrayList<RecyclerView.ViewHolder>()
composeTestRule.activityRule.scenario.onActivity { activity ->
viewHolders.addAll(activity.getViewsHolders())
Expand All @@ -121,6 +132,41 @@ class DpadComposeViewHolderTest {
composeTestRule.onNodeWithText("0").assertDoesNotExist()
}

@Test
fun testCompositionIsNotClearedWhenDetachingFromWindow() {
composeTestRule.activityRule.scenario.onActivity { activity ->
activity.getRecyclerView().setExtraLayoutSpaceStrategy(object : ExtraLayoutSpaceStrategy {
override fun calculateStartExtraLayoutSpace(state: RecyclerView.State): Int {
return 1080
}
})
}
repeat(3) {
KeyEvents.pressDown()
waitForIdleScroll()
}

composeTestRule.onNodeWithText("0").assertExists()
composeTestRule.onNodeWithText("0").assertIsNotDisplayed()
}

@Test
fun testCompositionIsClearedWhenViewHolderIsRecycled() {
repeat(5) {
KeyEvents.pressDown()
waitForIdleScroll()
}

composeTestRule.onNodeWithText("0").assertDoesNotExist()

var disposals: List<Int> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
disposals = activity.getDisposals()
}

assertThat(disposals).contains(0)
}

private fun waitForIdleScroll() {
onView(ViewMatchers.isAssignableFrom(DpadRecyclerView::class.java))
.perform(DpadRecyclerViewActions.waitForIdleScroll())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ class TestActivity : AppCompatActivity() {

private lateinit var recyclerView: DpadRecyclerView
private val clicks = ArrayList<Int>()
private val disposals = ArrayList<Int>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.compose_test)
recyclerView = findViewById(R.id.recyclerView)
recyclerView.adapter = Adapter(List(100) { it })
recyclerView.adapter = Adapter(
items = List(100) { it },
onDispose = { item ->
disposals.add(item)
}
)
recyclerView.requestFocus()
}

Expand All @@ -53,10 +59,16 @@ class TestActivity : AppCompatActivity() {
return clicks
}

fun getDisposals(): List<Int> {
return disposals
}

fun removeAdapter() {
recyclerView.adapter = null
}

fun getRecyclerView(): DpadRecyclerView = recyclerView

fun getViewsHolders(): List<RecyclerView.ViewHolder> {
val viewHolders = ArrayList<RecyclerView.ViewHolder>()
recyclerView.children.forEach { child ->
Expand All @@ -65,28 +77,35 @@ class TestActivity : AppCompatActivity() {
return viewHolders
}

inner class Adapter(private val items: List<Int>) :
RecyclerView.Adapter<DpadComposeViewHolder<Int>>() {
inner class Adapter(
private val items: List<Int>,
private val onDispose: (item: Int) -> Unit,
) : RecyclerView.Adapter<DpadComposeViewHolder<Int>>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): DpadComposeViewHolder<Int> {
return DpadComposeViewHolder(parent,
return DpadComposeViewHolder(
parent,
composable = { item, isFocused, isSelected ->
TestComposable(
modifier = Modifier
.fillMaxWidth()
.height(150.dp),
item = item,
isFocused = isFocused,
isSelected = isSelected
isSelected = isSelected,
onDispose = {
onDispose(item)
}
)
},
onClick = {
clicks.add(it)
},
isFocusable = true)
isFocusable = true
)
}

override fun getItemCount(): Int = items.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package com.rubensousa.dpadrecyclerview.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand All @@ -37,7 +39,8 @@ fun TestComposable(
modifier: Modifier = Modifier,
item: Int,
isFocused: Boolean,
isSelected: Boolean
isSelected: Boolean,
onDispose: () -> Unit = {},
) {
val backgroundColor = if (isFocused) {
Color.White
Expand All @@ -51,10 +54,24 @@ fun TestComposable(
.background(backgroundColor),
contentAlignment = Alignment.Center,
) {
Text(modifier = Modifier.semantics {
set(TestComposable.focusedKey, isFocused)
set(TestComposable.selectedKey, isSelected)
}, text = item.toString())
Text(
modifier = Modifier.semantics {
set(TestComposable.focusedKey, isFocused)
set(TestComposable.selectedKey, isSelected)
},
text = item.toString(),
style = MaterialTheme.typography.headlineLarge,
color = if(isFocused) {
Color.Black
} else {
Color.White
}
)
}
DisposableEffect(key1 = item) {
onDispose {
onDispose()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ abstract class DpadAbstractComposeViewHolder<T>(

init {
val composeView = itemView as ComposeView
composeView.setViewCompositionStrategy(RecyclerViewCompositionStrategy.DisposeOnRecycled)
composeView.isFocusable = isFocusable
composeView.isFocusableInTouchMode = isFocusable
composeView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 Rúben Sousa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.rubensousa.dpadrecyclerview.compose

import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.customview.poolingcontainer.PoolingContainerListener
import androidx.customview.poolingcontainer.addPoolingContainerListener
import androidx.customview.poolingcontainer.removePoolingContainerListener
import com.rubensousa.dpadrecyclerview.DpadRecyclerView

object RecyclerViewCompositionStrategy {

/**
* Similar to [ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool] but skips
* releasing compositions when detached from window. This is useful for re-using compositions
* a lot more often when scrolling a RecyclerView.
*
* If you use [DpadRecyclerView.setRecycleChildrenOnDetach],
* this will behave exactly the same as [ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool].
*
* If you use [DpadRecyclerView.setExtraLayoutSpaceStrategy],
* please profile the compositions before considering using this strategy.
*/
object DisposeOnRecycled : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val poolingContainerListener = PoolingContainerListener {
view.disposeComposition()
}
view.addPoolingContainerListener(poolingContainerListener)
return {
view.removePoolingContainerListener(poolingContainerListener)
}
}
}

}

0 comments on commit fbd993a

Please sign in to comment.