Skip to content

Commit

Permalink
Simplify api for compose ViewHolders
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensousa committed Mar 17, 2024
1 parent 4ac0912 commit 7e0320f
Show file tree
Hide file tree
Showing 11 changed files with 29 additions and 100 deletions.
20 changes: 6 additions & 14 deletions dpadrecyclerview-compose/api/dpadrecyclerview-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,19 @@ public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeExtensions
public static final fun dpadClickable (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/Modifier;
}

public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder, com/rubensousa/dpadrecyclerview/DpadViewHolder {
public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder {
public static final field $stable I
public fun <init> (Landroid/view/ViewGroup;Landroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function4;)V
public synthetic fun <init> (Landroid/view/ViewGroup;Landroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Landroid/view/ViewGroup;Landroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function3;)V
public synthetic fun <init> (Landroid/view/ViewGroup;Landroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getItem ()Ljava/lang/Object;
public fun getSubPositionAlignments ()Ljava/util/List;
public fun onViewHolderDeselected ()V
public fun onViewHolderSelected ()V
public fun onViewHolderSelectedAndAligned ()V
public final fun setItemState (Ljava/lang/Object;)V
}

public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder, com/rubensousa/dpadrecyclerview/DpadViewHolder {
public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder {
public static final field $stable I
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 <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function4;)V
public synthetic fun <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getItem ()Ljava/lang/Object;
public fun getSubPositionAlignments ()Ljava/util/List;
public fun onViewHolderDeselected ()V
public fun onViewHolderSelected ()V
public fun onViewHolderSelectedAndAligned ()V
public final fun setItemState (Ljava/lang/Object;)V
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.rubensousa.dpadrecyclerview.compose

import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
Expand Down Expand Up @@ -50,13 +51,11 @@ class DpadComposeFocusViewHolderTest {
@Test
fun testComposeItemsReceiveFocus() {
assertFocus(item = 0, isFocused = true)
assertSelection(item = 0, isSelected = true)

KeyEvents.pressDown()
waitForIdleScroll()

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

@Test
Expand All @@ -67,15 +66,12 @@ class DpadComposeFocusViewHolderTest {

Espresso.onIdle()
assertFocus(item = 0, isFocused = false)
assertSelection(item = 0, isSelected = true)

composeTestRule.activityRule.scenario.onActivity { activity ->
activity.requestFocus()
}

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

@Test
fun testClicksAreDispatched() {
Expand All @@ -101,8 +97,8 @@ class DpadComposeFocusViewHolderTest {
}

viewHolders.forEach { viewHolder ->
val composeView = viewHolder.itemView as DpadComposeView
assertThat(composeView.hasComposition()).isFalse()
val composeView = viewHolder.itemView as ComposeView
assertThat(composeView.hasComposition).isFalse()
}
composeTestRule.onNodeWithText("0").assertDoesNotExist()
}
Expand Down Expand Up @@ -152,9 +148,4 @@ class DpadComposeFocusViewHolderTest {
.assert(SemanticsMatcher.expectValue(TestComposable.focusedKey, isFocused))
}

private fun assertSelection(item: Int, isSelected: Boolean) {
composeTestRule.onNodeWithText(item.toString()).assertIsDisplayed()
.assert(SemanticsMatcher.expectValue(TestComposable.selectedKey, isSelected))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,11 @@ class DpadComposeViewHolderTest {
@Test
fun testComposeItemsReceiveFocus() {
assertFocus(item = 0, isFocused = true)
assertSelection(item = 0, isSelected = true)

KeyEvents.pressDown()
waitForIdleScroll()

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

@Test
Expand All @@ -66,14 +64,12 @@ class DpadComposeViewHolderTest {

Espresso.onIdle()
assertFocus(item = 0, isFocused = false)
assertSelection(item = 0, isSelected = true)

composeTestRule.activityRule.scenario.onActivity { activity ->
activity.requestFocus()
}

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

@Test
Expand Down Expand Up @@ -160,9 +156,4 @@ class DpadComposeViewHolderTest {
.assert(SemanticsMatcher.expectValue(TestComposable.focusedKey, isFocused))
}

private fun assertSelection(item: Int, isSelected: Boolean) {
composeTestRule.onNodeWithText(item.toString()).assertIsDisplayed()
.assert(SemanticsMatcher.expectValue(TestComposable.selectedKey, isSelected))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,12 @@ class ComposeFocusTestActivity : AppCompatActivity() {
): DpadComposeFocusViewHolder<Int> {
return DpadComposeFocusViewHolder(
parent = parent,
content = { item, isSelected ->
content = { item ->
TestComposableFocus(
modifier = Modifier
.fillMaxWidth()
.height(150.dp),
item = item,
isSelected = isSelected,
onClick = {
clicks.add(item)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,17 @@ import androidx.compose.ui.tooling.preview.Preview

object TestComposable {
val focusedKey = SemanticsPropertyKey<Boolean>("Focused")
val selectedKey = SemanticsPropertyKey<Boolean>("Selected")
}

@Composable
fun TestComposable(
modifier: Modifier = Modifier,
item: Int,
isFocused: Boolean,
isSelected: Boolean,
onDispose: () -> Unit = {},
) {
val backgroundColor = if (isFocused) {
Color.White
} else if (isSelected) {
Color.Blue
} else {
Color.Black
}
Expand All @@ -67,7 +63,6 @@ fun TestComposable(
Text(
modifier = Modifier.semantics {
set(TestComposable.focusedKey, isFocused)
set(TestComposable.selectedKey, isSelected)
},
text = item.toString(),
style = MaterialTheme.typography.headlineLarge,
Expand All @@ -89,15 +84,12 @@ fun TestComposable(
fun TestComposableFocus(
modifier: Modifier = Modifier,
item: Int,
isSelected: Boolean,
onClick: () -> Unit,
onDispose: () -> Unit = {},
) {
var isFocused by remember { mutableStateOf(false) }
val backgroundColor = if (isFocused) {
Color.White
} else if (isSelected) {
Color.Blue
} else {
Color.Black
}
Expand All @@ -116,7 +108,6 @@ fun TestComposableFocus(
Text(
modifier = Modifier.semantics {
set(TestComposable.focusedKey, isFocused)
set(TestComposable.selectedKey, isSelected)
},
text = item.toString(),
style = MaterialTheme.typography.headlineLarge,
Expand All @@ -139,7 +130,6 @@ fun TestComposableFocus(
fun TestComposablePreviewNormal() {
TestComposableFocus(
item = 0,
isSelected = false,
onClick = {}
)
}
Expand All @@ -151,20 +141,9 @@ fun TestComposablePreviewFocused() {
TestComposableFocus(
item = 0,
modifier = Modifier.focusRequester(focusRequester),
isSelected = false,
onClick = {}
)
SideEffect {
focusRequester.requestFocus()
}
}

@Preview(widthDp = 300, heightDp = 300)
@Composable
fun TestComposablePreviewSelected() {
TestComposableFocus(
item = 0,
isSelected = true,
onClick = {}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,13 @@ class ViewFocusTestActivity : AppCompatActivity() {
clicks.add(it)
},
isFocusable = true
) { item, isFocused, isSelected ->
) { item, isFocused ->
TestComposable(
modifier = Modifier
.fillMaxWidth()
.height(150.dp),
item = item,
isFocused = isFocused,
isSelected = isSelected,
onDispose = {
onDispose(item)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ package com.rubensousa.dpadrecyclerview.compose
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

/**
* Similar to [DpadComposeViewHolder], but sends the focus down to composables
Expand All @@ -46,36 +46,26 @@ import com.rubensousa.dpadrecyclerview.DpadViewHolder
class DpadComposeFocusViewHolder<T>(
parent: ViewGroup,
compositionStrategy: ViewCompositionStrategy = RecyclerViewCompositionStrategy.DisposeOnRecycled,
private val content: @Composable (item: T, isSelected: Boolean) -> Unit
) : RecyclerView.ViewHolder(DpadComposeView(parent.context)), DpadViewHolder {
private val content: @Composable (item: T) -> Unit
) : RecyclerView.ViewHolder(ComposeView(parent.context)) {

private val itemState = mutableStateOf<T?>(null)
private val selectionState = mutableStateOf(false)

init {
val composeView = itemView as DpadComposeView
val composeView = itemView as ComposeView
composeView.apply {
setFocusConfiguration(
isFocusable = true,
dispatchFocusToComposable = true
)
isFocusable = true
isFocusableInTouchMode = true
descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS
setViewCompositionStrategy(compositionStrategy)
setContent {
itemState.value?.let { item ->
content(item, selectionState.value)
content(item)
}
}
}
}

override fun onViewHolderSelected() {
selectionState.value = true
}

override fun onViewHolderDeselected() {
selectionState.value = false
}

fun setItemState(item: T?) {
itemState.value = item
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ 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

/**
* A basic ViewHolder that forwards [Content] to [composable]
* A basic ViewHolder that forwards [content] to a [ComposeView]
* and handles focus and clicks inside the View system.
*
* Focus is kept inside the internal [ComposeView] to ensure that it behaves correctly
Expand All @@ -42,8 +41,8 @@ import com.rubensousa.dpadrecyclerview.DpadViewHolder
*
* ```kotlin
* override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DpadComposeViewHolder<Int> {
* return DpadComposeViewHolder(parent) { item, isFocused, isSelected ->
* ItemComposable(item, isFocused, isSelected)
* return DpadComposeViewHolder(parent) { item, isFocused ->
* ItemComposable(item, isFocused)
* }
* }
* ```
Expand All @@ -61,12 +60,11 @@ class DpadComposeViewHolder<T>(
onLongClick: ((item: T) -> Boolean)? = null,
isFocusable: Boolean = true,
compositionStrategy: ViewCompositionStrategy = RecyclerViewCompositionStrategy.DisposeOnRecycled,
private val composable: DpadComposable<T>,
) : RecyclerView.ViewHolder(DpadComposeView(parent.context)), DpadViewHolder {
private val content: @Composable (item: T, isFocused: Boolean) -> Unit
) : RecyclerView.ViewHolder(DpadComposeView(parent.context)) {

private val focusState = mutableStateOf(false)
private val itemState = mutableStateOf<T?>(null)
private val selectionState = mutableStateOf(false)

init {
val composeView = itemView as DpadComposeView
Expand All @@ -81,7 +79,7 @@ class DpadComposeViewHolder<T>(
setViewCompositionStrategy(compositionStrategy)
setContent {
itemState.value?.let { item ->
composable(item, focusState.value, selectionState.value)
content(item, focusState.value)
}
}
}
Expand All @@ -98,19 +96,9 @@ class DpadComposeViewHolder<T>(
}
}

override fun onViewHolderSelected() {
selectionState.value = true
}

override fun onViewHolderDeselected() {
selectionState.value = false
}

fun setItemState(item: T?) {
itemState.value = item
}

fun getItem(): T? = itemState.value
}

typealias DpadComposable<T> = @Composable (T, Boolean, Boolean) -> Unit
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ComposeGridAdapter : MutableListAdapter<Int, DpadComposeFocusViewHolder<In
parent: ViewGroup,
viewType: Int
): DpadComposeFocusViewHolder<Int> {
return DpadComposeFocusViewHolder(parent) { item, isSelected ->
return DpadComposeFocusViewHolder(parent) { item ->
GridItemComposable(
item = item,
onClick = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ComposeItemAdapter(
parent: ViewGroup,
viewType: Int
): DpadComposeFocusViewHolder<Int> {
return DpadComposeFocusViewHolder(parent) { item, isSelected ->
return DpadComposeFocusViewHolder(parent) { item ->
ItemComposable(
modifier = Modifier
.width(dimensionResource(id = R.dimen.list_item_width))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ComposePlaceholderAdapter(
return DpadComposeViewHolder(
parent = parent,
isFocusable = false
) { _, _, _ ->
) { _, _ ->
composable()
}
}
Expand Down

0 comments on commit 7e0320f

Please sign in to comment.