Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RecyclerViewCompositionStrategy.DisposeOnRecycled #185

Merged
merged 7 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ buildscript {
}

plugins {
id 'com.android.application' version '8.0.0' apply false
id 'com.android.library' version '8.0.0' apply false
id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
id 'com.android.application' version '8.2.2' apply false
id 'com.android.library' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.13.0'
}

Expand Down
9 changes: 9 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Version 1.2.0

### 1.2.0-beta02

2024-01-28

#### New Features

- Added new `RecyclerViewCompositionStrategy.DisposeOnRecycled` for compose interop
to re-use compositions when views are detached and attached from the window again.

### 1.2.0-beta01

2024-01-17
Expand Down
19 changes: 15 additions & 4 deletions dpadrecyclerview-compose/api/dpadrecyclerview-compose.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
public abstract class com/rubensousa/dpadrecyclerview/compose/DpadAbstractComposeViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder, com/rubensousa/dpadrecyclerview/DpadViewHolder {
public static final field $stable I
public fun <init> (Landroid/view/ViewGroup;Z)V
public synthetic fun <init> (Landroid/view/ViewGroup;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Landroid/view/ViewGroup;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;)V
public synthetic fun <init> (Landroid/view/ViewGroup;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public abstract fun Content (Ljava/lang/Object;ZZLandroidx/compose/runtime/Composer;I)V
public final fun getItem ()Ljava/lang/Object;
public fun getSubPositionAlignments ()Ljava/util/List;
Expand All @@ -14,8 +14,19 @@ public abstract class com/rubensousa/dpadrecyclerview/compose/DpadAbstractCompos

public class com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolder : com/rubensousa/dpadrecyclerview/compose/DpadAbstractComposeViewHolder {
public static final field $stable I
public fun <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function5;)V
public synthetic fun <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function5;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function5;)V
public synthetic fun <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function5;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
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(10) {
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 @@ -20,6 +20,7 @@ import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.DpadViewHolder

Expand All @@ -41,7 +42,8 @@ import com.rubensousa.dpadrecyclerview.DpadViewHolder
*/
abstract class DpadAbstractComposeViewHolder<T>(
parent: ViewGroup,
isFocusable: Boolean = true
isFocusable: Boolean = true,
compositionStrategy: ViewCompositionStrategy = RecyclerViewCompositionStrategy.DisposeOnRecycled
) : RecyclerView.ViewHolder(ComposeView(parent.context)), DpadViewHolder {

private val itemState = mutableStateOf<T?>(null)
Expand All @@ -50,6 +52,7 @@ abstract class DpadAbstractComposeViewHolder<T>(

init {
val composeView = itemView as ComposeView
composeView.setViewCompositionStrategy(compositionStrategy)
composeView.isFocusable = isFocusable
composeView.isFocusableInTouchMode = isFocusable
composeView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.rubensousa.dpadrecyclerview.compose

import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ViewCompositionStrategy

/**
* A basic implementation of [DpadAbstractComposeViewHolder]
Expand Down Expand Up @@ -44,8 +45,9 @@ open class DpadComposeViewHolder<T>(
onClick: ((item: T) -> Unit)? = null,
onLongClick: ((item: T) -> Boolean)? = null,
isFocusable: Boolean = true,
compositionStrategy: ViewCompositionStrategy = RecyclerViewCompositionStrategy.DisposeOnRecycled,
private val composable: DpadComposable<T>,
) : DpadAbstractComposeViewHolder<T>(parent, isFocusable) {
) : DpadAbstractComposeViewHolder<T>(parent, isFocusable, compositionStrategy) {

init {
if (onClick != null) {
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)
}
}
}

}
3 changes: 3 additions & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public final class com/rubensousa/dpadrecyclerview/DpadLoopDirection : java/lang
public static final field MAX Lcom/rubensousa/dpadrecyclerview/DpadLoopDirection;
public static final field MIN_MAX Lcom/rubensousa/dpadrecyclerview/DpadLoopDirection;
public static final field NONE Lcom/rubensousa/dpadrecyclerview/DpadLoopDirection;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/rubensousa/dpadrecyclerview/DpadLoopDirection;
public static fun values ()[Lcom/rubensousa/dpadrecyclerview/DpadLoopDirection;
}
Expand Down Expand Up @@ -237,6 +238,7 @@ public final class com/rubensousa/dpadrecyclerview/FocusableDirection : java/lan
public static final field CIRCULAR Lcom/rubensousa/dpadrecyclerview/FocusableDirection;
public static final field CONTINUOUS Lcom/rubensousa/dpadrecyclerview/FocusableDirection;
public static final field STANDARD Lcom/rubensousa/dpadrecyclerview/FocusableDirection;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/rubensousa/dpadrecyclerview/FocusableDirection;
public static fun values ()[Lcom/rubensousa/dpadrecyclerview/FocusableDirection;
}
Expand Down Expand Up @@ -292,6 +294,7 @@ public final class com/rubensousa/dpadrecyclerview/ParentAlignment$Edge : java/l
public static final field MIN Lcom/rubensousa/dpadrecyclerview/ParentAlignment$Edge;
public static final field MIN_MAX Lcom/rubensousa/dpadrecyclerview/ParentAlignment$Edge;
public static final field NONE Lcom/rubensousa/dpadrecyclerview/ParentAlignment$Edge;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/rubensousa/dpadrecyclerview/ParentAlignment$Edge;
public static fun values ()[Lcom/rubensousa/dpadrecyclerview/ParentAlignment$Edge;
}
Expand Down
Loading
Loading