-
Notifications
You must be signed in to change notification settings - Fork 12
Efficient Strategies for Locating Items in Compose LazyList
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)) }
}
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>()
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.
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)
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)))