Skip to content

Commit

Permalink
Merge pull request #199 from rubensousa/clean-api
Browse files Browse the repository at this point in the history
Simplify api for compose ViewHolders
  • Loading branch information
rubensousa committed Mar 17, 2024
2 parents 4ac0912 + dab1b09 commit 7d8e5e3
Show file tree
Hide file tree
Showing 16 changed files with 188 additions and 130 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
4 changes: 2 additions & 2 deletions dpadrecyclerview-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ dependencies {

// Test dependencies
debugImplementation libs.androidx.test.compose.ui.manifest
debugImplementation libs.androidx.compose.ui.tooling
debugImplementation libs.androidx.compose.material3
androidTestImplementation libs.androidx.compose.ui.tooling
androidTestImplementation libs.androidx.compose.material3
androidTestImplementation project(':dpadrecyclerview-testing')
androidTestImplementation project(':dpadrecyclerview-test-fixtures')
androidTestImplementation libs.androidx.test.compose.ui.junit4
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Rúben Sousa
~ 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.
Expand Down Expand Up @@ -28,7 +28,7 @@

<application tools:ignore="MissingTvBanner">
<activity
android:name=".ComposeFocusTestActivity"
android:name="com.rubensousa.dpadrecyclerview.compose.test.ComposeFocusTestActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true">
<intent-filter>
Expand All @@ -38,7 +38,7 @@
</intent-filter>
</activity>
<activity
android:name=".ViewFocusTestActivity"
android:name="com.rubensousa.dpadrecyclerview.compose.test.ViewFocusTestActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true">
<intent-filter>
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 All @@ -29,9 +30,13 @@ 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.compose.test.ComposeFocusTestActivity
import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent
import com.rubensousa.dpadrecyclerview.testfixtures.recording.ScreenRecorderRule
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.R
import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions
import com.rubensousa.dpadrecyclerview.testing.actions.DpadViewActions
import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule
import org.junit.Rule
import org.junit.Test
Expand All @@ -48,15 +53,16 @@ class DpadComposeFocusViewHolderTest {
val composeTestRule = createAndroidComposeRule<ComposeFocusTestActivity>()

@Test
fun testComposeItemsReceiveFocus() {
fun testFirstItemHasFocus() {
assertFocus(item = 0, isFocused = true)
assertSelection(item = 0, isSelected = true)
}

@Test
fun testNextItemReceivesFocus() {
KeyEvents.pressDown()
waitForIdleScroll()

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

@Test
Expand All @@ -67,14 +73,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)
}

@Test
Expand All @@ -101,20 +105,21 @@ 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()
}

@Test
fun testCompositionIsNotClearedWhenDetachingFromWindow() {
composeTestRule.activityRule.scenario.onActivity { activity ->
activity.getRecyclerView().setExtraLayoutSpaceStrategy(object : ExtraLayoutSpaceStrategy {
override fun calculateStartExtraLayoutSpace(state: RecyclerView.State): Int {
return 1080
}
})
activity.getRecyclerView()
.setExtraLayoutSpaceStrategy(object : ExtraLayoutSpaceStrategy {
override fun calculateStartExtraLayoutSpace(state: RecyclerView.State): Int {
return 1080
}
})
}
repeat(3) {
KeyEvents.pressDown()
Expand All @@ -127,10 +132,8 @@ class DpadComposeFocusViewHolderTest {

@Test
fun testCompositionIsClearedWhenViewHolderIsRecycled() {
repeat(10) {
KeyEvents.pressDown()
waitForIdleScroll()
}
KeyEvents.pressDown(times = 10)
waitForIdleScroll()

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

Expand All @@ -142,6 +145,49 @@ class DpadComposeFocusViewHolderTest {
assertThat(disposals).contains(0)
}

@Test
fun testFocusEventIsReceivedForFirstChild() {
var focusEvents: List<DpadFocusEvent> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
focusEvents = activity.getFocusEvents()
}

// when
onView(ViewMatchers.withId(R.id.recyclerView))
.perform(
DpadViewActions.waitForCondition<DpadRecyclerView>(
description = "Wait for focus event",
condition = { recyclerView -> focusEvents.isNotEmpty() }
)
)

assertThat(focusEvents).hasSize(1)
val event = focusEvents.first()
assertThat(event.position).isEqualTo(0)
assertThat(event.child).isInstanceOf(ComposeView::class.java)
}

@Test
fun testAllViewHoldersAreFocusedOnKeyPress() {
// given
val events = 10

// when
repeat(events) {
KeyEvents.pressDown()
waitForIdleScroll()
}

// then
var focusEvents: List<DpadFocusEvent> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
focusEvents = activity.getFocusEvents()
}

assertThat(focusEvents).hasSize(events + 1)
assertThat(focusEvents.map { it.position }).isEqualTo(List(events + 1) { it })
}

private fun waitForIdleScroll() {
onView(ViewMatchers.isAssignableFrom(DpadRecyclerView::class.java))
.perform(DpadRecyclerViewActions.waitForIdleScroll())
Expand All @@ -152,9 +198,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 @@ -28,9 +28,13 @@ 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.compose.test.ViewFocusTestActivity
import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent
import com.rubensousa.dpadrecyclerview.testfixtures.recording.ScreenRecorderRule
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.R
import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions
import com.rubensousa.dpadrecyclerview.testing.actions.DpadViewActions
import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule
import org.junit.Rule
import org.junit.Test
Expand All @@ -49,13 +53,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 +68,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 @@ -135,10 +135,8 @@ class DpadComposeViewHolderTest {

@Test
fun testCompositionIsClearedWhenViewHolderIsRecycled() {
repeat(10) {
KeyEvents.pressDown()
waitForIdleScroll()
}
KeyEvents.pressDown(times = 10)
waitForIdleScroll()

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

Expand All @@ -150,6 +148,49 @@ class DpadComposeViewHolderTest {
assertThat(disposals).contains(0)
}

@Test
fun testFocusEventIsReceivedForFirstChild() {
var focusEvents: List<DpadFocusEvent> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
focusEvents = activity.getFocusEvents()
}

// when
onView(ViewMatchers.withId(R.id.recyclerView))
.perform(
DpadViewActions.waitForCondition<DpadRecyclerView>(
description = "Wait for focus event",
condition = { recyclerView -> focusEvents.isNotEmpty() }
)
)

assertThat(focusEvents).hasSize(1)
val event = focusEvents.first()
assertThat(event.position).isEqualTo(0)
assertThat(event.child).isInstanceOf(DpadComposeView::class.java)
}

@Test
fun testAllViewHoldersAreFocusedOnKeyPress() {
// given
val events = 10

// when
repeat(events) {
KeyEvents.pressDown()
waitForIdleScroll()
}

// then
var focusEvents: List<DpadFocusEvent> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
focusEvents = activity.getFocusEvents()
}

assertThat(focusEvents).hasSize(events + 1)
assertThat(focusEvents.map { it.position }).isEqualTo(List(events + 1) { it })
}

private fun waitForIdleScroll() {
onView(ViewMatchers.isAssignableFrom(DpadRecyclerView::class.java))
.perform(DpadRecyclerViewActions.waitForIdleScroll())
Expand All @@ -160,9 +201,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))
}

}

0 comments on commit 7d8e5e3

Please sign in to comment.