<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Filip Gajic</title>
    <description>The latest articles on DEV Community by Filip Gajic (@morph93).</description>
    <link>https://dev.to/morph93</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4083892%2Feee300e7-ac4f-4513-8650-b4e630cab9ed.png</url>
      <title>DEV Community: Filip Gajic</title>
      <link>https://dev.to/morph93</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/morph93"/>
    <language>en</language>
    <item>
      <title>Why your mobile XPath breaks: a practical ranking of locator strategies for Android, iOS and Flutter</title>
      <dc:creator>Filip Gajic</dc:creator>
      <pubDate>Wed, 26 Aug 2026 08:56:18 +0000</pubDate>
      <link>https://dev.to/morph93/why-your-mobile-xpath-breaks-a-practical-ranking-of-locator-strategies-for-android-ios-and-flutter-25la</link>
      <guid>https://dev.to/morph93/why-your-mobile-xpath-breaks-a-practical-ranking-of-locator-strategies-for-android-ios-and-flutter-25la</guid>
      <description>&lt;p&gt;A lot of what gets filed as "flaky mobile test" is a locator that was never stable in the first place. The test passed for three weeks, a developer wrapped a view in a &lt;code&gt;FrameLayout&lt;/code&gt;, and an absolute XPath that encoded the entire hierarchy fell over. Nobody touched the test. It still broke.&lt;/p&gt;

&lt;p&gt;This is the order I pick locators in, why, and what each one looks like when it fails. Android and iOS first, then a short Flutter section, then the part that actually fixes the problem long-term: getting IDs added where the feature is written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule underneath all of it
&lt;/h2&gt;

&lt;p&gt;Pick the attribute a developer set on purpose. Avoid the one the render tree happened to produce.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;accessibilityIdentifier&lt;/code&gt;, an &lt;code&gt;android:id&lt;/code&gt;, a &lt;code&gt;testID&lt;/code&gt;, a Flutter &lt;code&gt;Key&lt;/code&gt;: someone typed those. They change when a human decides to change them. Position in a hierarchy, sibling index, the class name of a wrapper: those change whenever the layout is refactored, a design system component is swapped, or an OS update renders a control differently. Every tier below is a version of that rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Accessibility ID
&lt;/h2&gt;

&lt;p&gt;It's the same strategy on both platforms and both drivers index it, so lookups are fast. By convention it's unique per screen. On Android it's &lt;code&gt;content-desc&lt;/code&gt; (set with &lt;code&gt;contentDescription&lt;/code&gt;). On iOS it's &lt;code&gt;accessibilityIdentifier&lt;/code&gt;. Appium calls both &lt;code&gt;accessibility id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python (Appium)
&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppiumBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACCESSIBILITY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout_button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Java&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppiumBy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessibilityId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"checkout_button"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WebdriverIO&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~checkout_button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's also what screen-reader users rely on, at least on Android where &lt;code&gt;content-desc&lt;/code&gt; is read aloud. That makes it the last thing a developer quietly deletes, and it means the automation team and the accessibility team want the same thing.&lt;/p&gt;

&lt;p&gt;When it breaks: the ID was generated (&lt;code&gt;button_3f9a&lt;/code&gt;), or the same ID was reused across a list, or on Android someone put a user-facing sentence in &lt;code&gt;contentDescription&lt;/code&gt; for TalkBack and it now changes per locale. If your app has that last problem, ask for a &lt;code&gt;resource-id&lt;/code&gt; and leave &lt;code&gt;content-desc&lt;/code&gt; to accessibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. resource-id (Android)
&lt;/h2&gt;

&lt;p&gt;Set in the layout as &lt;code&gt;android:id&lt;/code&gt;, never shown to a user, and changed only on a deliberate rename. In the tree it appears as &lt;code&gt;com.example.app:id/checkout_button&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppiumBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.example.app:id/checkout_button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on React Native, &lt;code&gt;testID&lt;/code&gt; surfaces as &lt;code&gt;resource-id&lt;/code&gt; on Android and &lt;code&gt;accessibilityIdentifier&lt;/code&gt; on iOS in current versions, so a single prop gives you both platforms.&lt;/p&gt;

&lt;p&gt;When it breaks: the build obfuscates or strips IDs, the app was migrated to Compose without &lt;code&gt;testTag&lt;/code&gt; plus &lt;code&gt;testTagsAsResourceId&lt;/code&gt;, or an ID lives inside a reused component so twenty rows share it. In the last case combine it with something else rather than falling back to XPath by index; the UiSelector note in tier 5 covers that.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Text, with care
&lt;/h2&gt;

&lt;p&gt;Fine for a static label in a single-language app with no near-duplicates on screen. Borrowed stability everywhere else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppiumBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;//*[@text=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Continue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The day someone ships localisation, "Continue" becomes "Nastavi" for the Serbian locale and every text locator dies at once. A/B copy tests do the same thing on a smaller scale. Dynamic strings ("3 items", "Hi, Filip") change per user. If you have a translations file in the repo, you already know text is a temporary anchor.&lt;/p&gt;

&lt;p&gt;Text is still useful as a secondary condition: an ID that's shared across a list plus the row's label narrows to one element without touching the hierarchy.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. iOS predicate string and class chain
&lt;/h2&gt;

&lt;p&gt;This is where iOS pays you back for not having &lt;code&gt;resource-id&lt;/code&gt;. Both are native XCUITest queries and both are materially faster than XPath on a deep tree.&lt;/p&gt;

&lt;p&gt;Predicate string filters on attributes and supports partial matching without dropping to XPath:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppiumBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IOS_PREDICATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label CONTAINS &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Continue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; AND visible == 1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Class chain adds hierarchy and indexing on top of predicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppiumBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IOS_CLASS_CHAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/XCUIElementTypeCell[`name == &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cart_row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;`]/XCUIElementTypeButton[1]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backticks are part of the class chain syntax. The way I keep them apart: if I'm filtering, predicate; if I'm navigating, class chain.&lt;/p&gt;

&lt;p&gt;When they break: a predicate on &lt;code&gt;label&lt;/code&gt; breaks with localisation just like text does, so prefer &lt;code&gt;name&lt;/code&gt; (which is the accessibility identifier). Class chain with an index breaks when a cell gains a child, same as XPath, only cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. XPath, last, and relative only
&lt;/h2&gt;

&lt;p&gt;Absolute paths break the moment someone wraps a view in another layout. This is the one that comes out of an inspector when you click "copy XPath" without thinking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/hierarchy/android.widget.FrameLayout[1]/android.widget.LinearLayout[2]/android.widget.FrameLayout[1]/android.view.ViewGroup[1]/android.widget.Button[2]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every segment is a dependency on the current layout. One wrapper view, one reordered sibling, and it points at nothing.&lt;/p&gt;

&lt;p&gt;If you need XPath, anchor it to a real attribute and move one hop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//*[@resource-id='com.example.app:id/cart']/following-sibling::*[1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing breaks that except deleting the anchor. It's still slower than the tiers above (XPath means the driver has to build the whole tree as XML and evaluate the query against it on every lookup), so on iOS especially I reach for class chain before this.&lt;/p&gt;

&lt;p&gt;An Android footnote that deserves more attention than it gets: &lt;code&gt;-android uiautomator&lt;/code&gt; with &lt;code&gt;UiSelector&lt;/code&gt; handles a lot of what people reach for XPath for, and it runs through UiAutomator rather than the XML tree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppiumBy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;androidUIAutomator&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"new UiSelector().resourceIdMatches(\".*:id/cart_row\").textContains(\"Shoes\")"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd rank it between resource-id and text on Android when you need a partial match or a combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  One element, five locators
&lt;/h2&gt;

&lt;p&gt;Here is one checkout button on an Android screen, as it might come out of an inspector, worst to best:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout[2]/android.widget.Button[1]
//android.widget.Button[@text='Continue to payment']
//*[@resource-id='com.example.app:id/summary_card']/following-sibling::android.widget.Button[1]
com.example.app:id/checkout_button
checkout_button          (accessibility id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line one breaks on any layout change. Line two breaks when marketing renames the button or the app gets a second language. Line three survives both and breaks only if the summary card goes away. The last two survive everything short of a deliberate rename, and the accessibility ID also works on iOS if the developer used the same identifier there. When I open a screen the question is only "which of these does this element actually have", and the ranking answers itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flutter
&lt;/h2&gt;

&lt;p&gt;Flutter draws its own widgets, so out of the box the native tree sees one big canvas with whatever the app's semantics layer exposes. Newer Flutter versions push semantics labels through to the platform accessibility APIs, so some elements are reachable with plain &lt;code&gt;accessibility id&lt;/code&gt; through UiAutomator2 or XCUITest. For the rest you want the Appium Flutter integration driver, which talks to the running app through the Dart VM.&lt;/p&gt;

&lt;p&gt;Its locators, best first: &lt;code&gt;key&lt;/code&gt; (a &lt;code&gt;ValueKey&lt;/code&gt; on the widget), &lt;code&gt;semantics label&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; (widget class name), and &lt;code&gt;text&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the app&lt;/span&gt;
&lt;span class="n"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ValueKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'checkout_button'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="n"&gt;Semantics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;label:&lt;/span&gt; &lt;span class="s"&gt;'cart_row'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Java, appium-flutter-integration-driver&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FlutterBy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"checkout_button"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FlutterBy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;semanticsLabel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cart_row"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same reasoning as native: &lt;code&gt;key&lt;/code&gt; and &lt;code&gt;semantics label&lt;/code&gt; are things a developer typed, &lt;code&gt;type&lt;/code&gt; is a class name that survives most refactors but not a component swap, and &lt;code&gt;text&lt;/code&gt; inherits every localisation problem from tier 3. One caveat from the driver's own README: &lt;code&gt;semanticsLabel&lt;/code&gt; lookups on real iOS devices have a known Flutter issue, so verify on hardware before you standardise on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to talk to developers about adding test IDs
&lt;/h2&gt;

&lt;p&gt;The thing that fixes locator stability long-term is a conversation, and it's easier than most QA people expect. What has worked for me:&lt;/p&gt;

&lt;p&gt;Ask for something specific and small: "these six controls on the checkout screen need stable IDs; here's the list and the names I'd like." A ten-line PR gets merged. A policy discussion doesn't.&lt;/p&gt;

&lt;p&gt;Name the mechanism per stack so nobody has to look it up: &lt;code&gt;android:id&lt;/code&gt; in XML or &lt;code&gt;Modifier.testTag&lt;/code&gt; (with &lt;code&gt;testTagsAsResourceId&lt;/code&gt;) in Compose, &lt;code&gt;accessibilityIdentifier&lt;/code&gt; in UIKit and SwiftUI, &lt;code&gt;testID&lt;/code&gt; in React Native, &lt;code&gt;Key&lt;/code&gt; in Flutter.&lt;/p&gt;

&lt;p&gt;Agree on a naming convention once (&lt;code&gt;screen_element_role&lt;/code&gt;, all lowercase, no spaces) and put it in the PR template. Half the value is that IDs stop being invented ad hoc.&lt;/p&gt;

&lt;p&gt;Point at the accessibility overlap. On Android, &lt;code&gt;content-desc&lt;/code&gt; serves TalkBack users and your tests at the same time, so the work counts twice.&lt;/p&gt;

&lt;p&gt;Show the cost. One example of an absolute XPath from your suite next to the ID version, with the git blame of the layout change that broke it, does more than any argument.&lt;/p&gt;

&lt;p&gt;Every hour spent here is worth ten spent hardening XPath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Stability&lt;/th&gt;
&lt;th&gt;When it breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility ID (&lt;code&gt;content-desc&lt;/code&gt;, &lt;code&gt;accessibilityIdentifier&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Android, iOS&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Generated or reused IDs; localised &lt;code&gt;contentDescription&lt;/code&gt; on Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resource-id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Obfuscated builds; Compose without &lt;code&gt;testTag&lt;/code&gt;; shared IDs in lists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;Android, iOS&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;Localisation, copy edits, dynamic strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS predicate string&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;High on &lt;code&gt;name&lt;/code&gt;, low on &lt;code&gt;label&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same as text when filtering on &lt;code&gt;label&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS class chain&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Indexed hops break when hierarchy changes, but cheaper than XPath&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-android uiautomator&lt;/code&gt; (UiSelector)&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Medium to high&lt;/td&gt;
&lt;td&gt;Same inputs as the attributes it matches on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relative XPath (anchored, one hop)&lt;/td&gt;
&lt;td&gt;Android, iOS&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Anchor removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Absolute XPath&lt;/td&gt;
&lt;td&gt;Android, iOS&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Any wrapper, reorder or component swap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter &lt;code&gt;key&lt;/code&gt; / &lt;code&gt;semantics label&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Key removed or renamed; &lt;code&gt;semanticsLabel&lt;/code&gt; on real iOS devices (known issue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter &lt;code&gt;type&lt;/code&gt; / &lt;code&gt;text&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;Component swap; localisation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This hierarchy is why Spectra grades every generated locator A to D: same logic, made visible next to each candidate instead of living in your head. The grading is in the free tier at &lt;a href="https://usespectra.dev/?utm_source=devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://usespectra.dev/?utm_source=devto&amp;amp;utm_medium=social&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Spectra is free for inspection. Download at &lt;a href="https://usespectra.dev/?utm_source=devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://usespectra.dev/?utm_source=devto&amp;amp;utm_medium=social&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://usespectra.dev/blog/mobile-locator-strategies-ranked?utm_source=devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;usespectra.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI help, edited by Filip Gajić.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>mobile</category>
      <category>android</category>
      <category>qa</category>
    </item>
  </channel>
</rss>
