Skip to content

Efficient Strategies for Locating Items in Compose LazyList

Aleksei Tiurin edited this page Feb 25, 2024 · 2 revisions

Please, read article about Compose before this one.

Let's start with approaches that you can use without additional efforts. For example, you have identified LazyList in your tests code like

val lazyList = composeList(listMatcher = hasTestTag("listTestTag"))

class ComposeListItem : UltronComposeListItem() {
    val name by lazy { getChild(hasTestTag(contactNameTestTag)) }
    val status by lazy { getChild(hasTestTag(contactStatusTestTag)) }
}

1. ..visibleItem

This is probably the most unstable approach. It's only suitable in case you didn't interact with LazyList and would like to reach an item that is on the screen.

Use the following methods:

lazyList.firstVisibleItem()
lazyList.visibleItem(index = 3)
lazyList.lastVisibleItem()

lazyList.getFirstVisibleItem<ComposeListItem>()
lazyList.getVisibleItem<ComposeListItem>(index = 3)
lazyList.getLastVisibleItem<ComposeListItem>()

2. Item by unique SemanticsMatcher

A more stable way to find the item is to use SemanticsMatcher. It allows you to find the item not only on the screen.

lazyList.item(hasAnyDescendant(hasText("Some unique text")) 
lazyList.getItem<ComposeListItem>(hasAnyDescendant(hasText("Some unique text")) 

The next two approaches require additional code in the application. These are the most stable and preferable ways.

3. Set up positionPropertyKey

By default, a compose list item doesn't have a property that stores its position in the list. We can add this property in a really simple way.

Here is the application code:

// create custom SemanticsPropertyKey
val ListItemPositionPropertyKey = SemanticsPropertyKey<Int>("ListItemPosition")
var SemanticsPropertyReceiver.listItemPosition by ListItemPositionPropertyKey

// specify it for item and store item index in this property
@Composable
fun ContactsListWithPosition(contacts: List<Contact>
) {
    LazyColumn(
        modifier = Modifier.semantics { testTag = "listTestTag" }
    ) {
        itemsIndexed(contacts) { index, contact ->
            Column(
                modifier = Modifier.semantics {
                    listItemPosition = index
                }
            ) {
                // item content
            }
        }
    }
}

After that, you need to specify the custom SemanticsPropertyKey in the test code:

val lazyList = composeList(
    listMatcher = hasTestTag("listTestTag"),
    positionPropertyKey = ListItemPositionPropertyKey
)

It allows you to reach the item by its position in the list:

lazyList.firstItem()
lazyList.item(position = 25)
lazyList.getFirstItem<ComposeListItem>()
lazyList.getItem<ComposeListItem>(position = 7)

4. Set up item testTag

It is recommended to build testTag in a separate function based on data object.

For example, let's assume we have a Contact data class that stores data to be presented in the item.

data class Contact(val id: Int, val name: String, val status: String, val avatar: String)

We can create function to build testTag based on contact.id

fun getContactItemTestTag(contact: Contact) = "contactId=${contact.id}"

We can use this function in the application code to specify testTag and in the test code to find the item by testTag:

// application code
@Composable
fun ContactsListWithPosition(contacts: List<Contact>
) {
    LazyColumn(
        modifier = Modifier.semantics { testTag = "listTestTag" }
    ) {
        itemsIndexed(contacts) { index, contact ->
            Column(
                modifier = Modifier.semantics {
                    listItemPosition = index
                    testTag = getContactItemTestTag(contact)
                }
            ) {
                // item content
            }
        }
    }
}

//test code
val lazyList = composeList(listMatcher = hasTestTag("listTestTag"))

lazyList.item(hasTestTag(getContactItemTestTag(contact)))
lazyList.getItem<ComposeListItem>(hasTestTag(getContactItemTestTag(contact)))