Finding elements is the foundation of every Appium test. Use the wrong strategy and your tests are slow, fragile, and expensive to maintain. Use the right one and tests run fast, survive app updates, and communicate intent clearly. This lesson covers every locator strategy available in Appium, explains when to use each, and shows you how to rank your choices.
The locator hierarchy
Apply strategies in this order. Start from the top and only drop down when the element above is unavailable:
- Accessibility ID — fastest, most portable, survives refactors
- ID / resource-id (Android) — direct attribute lookup, fast
- UIAutomator2 / XCUITest predicates — platform-specific but powerful
- XPath — always works, but slow and brittle
Accessibility ID
AppiumBy.accessibilityId("value") maps to:
- Android:
content-descattribute - iOS:
accessibilityIdentifieroraccessibilityLabel
This is the only strategy that works identically on both platforms. If you have a cross-platform test base (e.g., a single test class that runs on Android and iOS with different capabilities), Accessibility ID is the strategy that keeps your page objects clean.
driver.findElement(AppiumBy.accessibilityId("login_button")).click();The catch: developers must explicitly set content-desc (Android) or accessibilityIdentifier (iOS). Many apps don't. In that case, move down the list.
ID (resource-id on Android)
driver.findElement(AppiumBy.id("com.example.myapp:id/email_input"));resource-id is reliable and fast. Every generated Android view has one if the developer used android:id in the layout XML. The format is always <package>:<type>/<name>.
If the full package prefix seems fragile (it changes if the app is repackaged), use UiSelector with a partial match:
driver.findElement(
AppiumBy.androidUIAutomator("new UiSelector().resourceIdMatches(\".*email_input.*\")")
);XPath
driver.findElement(By.xpath("//android.widget.Button[@text='Login']"));XPath is universally supported but has two problems on mobile:
Speed: Appium must serialise the entire accessibility tree to XML and then run an XPath query against it. On a complex screen with 200+ elements, this can take 2–3 seconds per lookup.
Fragility: XPath expressions that depend on position (//LinearLayout[2]/Button[1]) break whenever the layout changes. Even text-based XPaths (@text='Login') break when the app is localised.
Use XPath only when no ID, Accessibility ID, or platform selector is available, and prefer attribute-based XPath over positional XPath.
UIAutomator2 (Android only)
UIAutomator2 selectors use Google's UiSelector API, which has a richer query language than simple attribute matching:
// By exact text
AppiumBy.androidUIAutomator("new UiSelector().text(\"Sign In\")")
// By text containing
AppiumBy.androidUIAutomator("new UiSelector().textContains(\"Sign\")")
// By text matching a regex
AppiumBy.androidUIAutomator("new UiSelector().textMatches(\"Sign.*\")")
// By class name
AppiumBy.androidUIAutomator("new UiSelector().className(\"android.widget.Button\")")
// By description
AppiumBy.androidUIAutomator("new UiSelector().description(\"Login button\")")
// Chained conditions
AppiumBy.androidUIAutomator(
"new UiSelector().className(\"android.widget.Button\").text(\"Submit\")"
)
// Find a child element within a parent
AppiumBy.androidUIAutomator(
"new UiSelector().resourceId(\"com.example:id/form\").childSelector(new UiSelector().text(\"Submit\"))"
)
// Find element by index within class
AppiumBy.androidUIAutomator("new UiSelector().className(\"android.widget.EditText\").instance(0)")UIAutomator2 scrolling is unique — you can scroll to find an element that is off-screen:
driver.findElement(AppiumBy.androidUIAutomator(
"new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(" +
"new UiSelector().text(\"Terms of Service\"))"
));This is one of the most useful patterns in Android testing — no manual swipe coordinate calculation needed.
iOS NSPredicate strings
// Single condition
AppiumBy.iOSNsPredicateString("label == 'Sign In'")
// Multiple conditions
AppiumBy.iOSNsPredicateString("type == 'XCUIElementTypeButton' AND label == 'Sign In'")
// Contains
AppiumBy.iOSNsPredicateString("label CONTAINS 'Sign'")
// Not equal
AppiumBy.iOSNsPredicateString("type == 'XCUIElementTypeTextField' AND value != ''")NSPredicate strings are compiled and cached by the XCUITest engine, making them significantly faster than XPath for complex queries on iOS.
iOS class chains
Class chains navigate the element tree with explicit hierarchy:
// Third button in a navigation bar
AppiumBy.iOSClassChain("**/XCUIElementTypeNavigationBar/XCUIElementTypeButton[3]")
// A text field within a specific cell
AppiumBy.iOSClassChain("**/XCUIElementTypeCell[`label == 'Email'`]/XCUIElementTypeTextField")Use backtick predicates inside class chains to filter by attribute at any level of the hierarchy.
Comparing strategies: speed benchmark
Approximate lookup times on a mid-range Android device for a screen with ~150 elements:
| Strategy | Typical lookup time |
|---|---|
| resource-id | ~100 ms |
| Accessibility ID | ~100 ms |
| UiAutomator2 UiSelector | ~150 ms |
| XPath (attribute-based) | ~800 ms |
| XPath (positional) | ~800 ms |
The difference compounds across 200 tests. A 700ms penalty per lookup adds 2+ minutes to a suite that runs 200 locates per test.
Using findElements for collection assertions
List<WebElement> items = driver.findElements(
AppiumBy.androidUIAutomator("new UiSelector().className(\"android.widget.CheckBox\")")
);
Assert.assertEquals(items.size(), 5, "Should show 5 checkboxes");findElements returns an empty list (not an exception) if no elements match — useful for asserting that something is absent.