UiObject vs UiObject2 in Android's UI Automator
The question I keep seeing on Stack Overflow is some version of: “UI Automator has UiObject and UiObject2, what is the actual difference, and which one should I use?” The short answer is that they are two generations of the same idea. UiObject is the original handle from UI Automator 1.x, paired with UiSelector. UiObject2 arrived with UI Automator 2.0, paired with BySelector and the Until conditions. Both click buttons and read text. They differ in how they find elements, how they wait, and what happens when the screen changes underneath them.
I will call UiObject “Vendor A” and UiObject2 “Vendor B” in the table and headings below, because that is the format this site uses for head-to-head pieces. Neither costs anything, so the usual price axes are replaced with the things that actually hurt when you run these on real phones. I run a lot of Android hardware in Singapore, and most of what follows comes from tests that flaked at 2am, not from reading the reference pages alone.
My verdict per use case: for new code, use UiObject2. Its waits are tied to conditions instead of polling guesses, and its gestures take a direction and a percentage instead of raw coordinates and step counts. Keep UiObject only when you are maintaining an older suite, or when you need a lazily resolved selector that survives a screen redraw without you re-querying. If you are mixing it with Espresso or Appium, there are a few wrinkles I cover below.
TL;DR comparison table
| UiObject (Vendor A) | UiObject2 (Vendor B) | |
|---|---|---|
| Pricing | free, part of AndroidX test | free, part of AndroidX test |
| Selector type | UiSelector | BySelector |
| Lookup | UiDevice.findObject(UiSelector) | UiDevice.findObject(BySelector) |
| Binding | lazy, re-resolves the selector on each call | bound to one accessibility node, can go stale |
| Waits | waitForExists, waitUntilGone | wait(Until…), clickAndWait, UiDevice.wait |
| Gestures | swipeUp/Down/Left/Right with steps, dragTo | swipe, scroll, fling, drag, pinchOpen/Close with Direction and percent |
| Error model | checked UiObjectNotFoundException | null from findObject, StaleObjectException on stale use |
| Support | reference docs, older Stack Overflow answers | reference docs, newer examples and Google codelabs |
| Target user | legacy suites, quick one-off scripts | new test suites, anything gesture heavy |
Vendor A at a glance: UiObject
UiObject has been in UI Automator since the first release on API 16 and above. You build a UiSelector, hand it to UiDevice.findObject, and get back a UiObject. The key thing to understand is that a UiObject is not an element. It is a description of an element. Every time you call click(), getText() or exists(), it runs the selector again against the current accessibility tree.
That lazy behaviour is the best and worst thing about it. It is the best because a RecyclerView that rebinds its rows, or a screen that redraws after a network call, does not break your reference. The next call simply looks again. It is the worst because every call is a fresh query, so you pay for the lookup each time, and a selector that matched one view a second ago can quietly match a different one now.
Waiting is limited. You get waitForExists(timeout) and waitUntilGone(timeout), both in milliseconds, and that is about it. If you want to wait for a button to become enabled, or for text to change, you write your own loop with sleeps. Most flaky UiObject suites I have seen are flaky because of exactly this.
Errors are a checked exception, UiObjectNotFoundException, so Java code ends up wrapped in try/catch or throws clauses all the way up. Kotlin hides that, which is part of why Kotlin test code looks tidier.
Gestures are the old style. swipeUp(steps) and friends take a step count, where each step is a small fixed time slice, so the swipe speed depends on a number that has no obvious meaning. dragTo takes coordinates or another object. It works, but tuning it feels like guessing.
I wrote a longer piece on whether the whole selector family is on its way out: is UiSelector deprecated in Android’s UI Automator. Short version: as of October 2026 the reference docs for UiObject still list it, so check that page before you plan a migration around anything I say here.
Vendor B at a glance: UiObject2
UiObject2 came with UI Automator 2.0 and the BySelector builder. You write By.res(“com.example:id/submit”) or By.text(“Sign in”).clazz(“android.widget.Button”), and UiDevice.findObject returns a UiObject2, or null if nothing matches. No checked exception on lookup, which already makes the code shorter.
The big change is that a UiObject2 is bound to a real accessibility node. You looked once, you got that node. Calls on it use that node directly, which is faster and more predictable. The price is staleness. If the view is recycled or the screen changes, the next call can throw StaleObjectException. In practice you handle this by re-querying at sensible points, not by wrapping everything in retries.
Waiting is where it pulls ahead. The Until class gives you conditions such as Until.hasObject, Until.gone, Until.enabled, Until.textEquals and Until.newWindow. You write uiDevice.wait(Until.hasObject(By.text(“Done”)), 5000) and it returns when the condition is true, not after a fixed sleep. clickAndWait(Until.newWindow(), timeout) clicks and waits for a window change in one call. The full list is on the Until reference page.
Gestures are cleaner too. UiObject2 has swipe(Direction, percent), scroll(Direction, percent), fling(Direction), drag(Point, speed), and pinchOpen/pinchClose with a percent. You say “swipe left across 80 percent of this view” and it works out the coordinates. That is how people expect gestures to work, and it is the main reason I stopped writing new tests with UiObject.
The official overview, including setup, is the Android UI Automator guide. It uses BySelector and UiObject2 in its examples.
head-to-head
The usual head-to-head axes for this site’s vs articles come from proxy comparisons (pool size, price per GB and so on). For two free APIs I have mapped each axis to the nearest thing that matters in UI Automator, and said so under each heading.
IP pool size: how many elements you can address
Proxy pool size has no direct counterpart. The nearest equivalent is how much of the screen each API can reach. Both see whatever the accessibility tree exposes. Neither can see inside a canvas, a game surface, or a view that marks itself as not important for accessibility. UiObject2 and BySelector give you a few more matching options, such as matching on class plus resource id plus enabled state in one chain, and hasObject checks without allocating an object. UiObject covers the same tree through UiSelector, with methods like childSelector and fromParent for walking relationships.
Result: tie on reach, slight edge to UiObject2 for ergonomics.
Rotation control: how you control which element you hit
Here “rotation” maps to how selectors resolve when several views match. UiSelector has instance(n) to pick the nth match. BySelector does not have instance on the selector. You call findObjects(BySelector), get a list, and index into it yourself. I like the second approach because the list is explicit, but if you have a lot of old tests using instance(2), migration means rewriting those lines.
UiObject’s lazy re-resolution also means “which element” can change between calls if the tree changes. UiObject2 pins it to one node. If you want predictable targeting, UiObject2 is the safer choice.
Result: UiObject2.
Geo coverage: device and API level coverage
Geographic coverage is irrelevant for an API, so I use device coverage. UiObject has been available since the first UI Automator, which means it works on older Android versions that UiObject2 may not. UiObject2 needs UI Automator 2.0 or later, which in practice means API 18+, and newer versions of the AndroidX library raise the floor further. Check the minSdk of the library version you pull in, because that moves between releases. For any phone you would reasonably test today, either works.
If you are testing on real devices in a different region, the API choice does not change. What changes is the device. When I need a phone on a real Singapore mobile network for a test, I use cloudf.one, which rents real Android phones in Singapore on dedicated hardware, controlled from the browser. It is Singapore only, so it is no help if you need another country. And a caution on adb: cloudf.one has no self-service adb. adb and Appium reach a rented phone only through a WireGuard tunnel that we set up on request (email [email protected]). If your UI Automator tests run through adb instrumentation, that tunnel is the route, and it is not instant. For where carrier networks actually matter, see testing apps on real carrier networks.
Result: tie, with a slight edge to UiObject for very old devices.
Connection success rate: how often a call does what you meant
I will not invent a flake percentage. I have no controlled benchmark and no one else’s I can honestly cite, so here is what I observed qualitatively across suites I have maintained.
UiObject suites tend to fail with “object not found” when timing is off, because the only waiting tool is waitForExists. UiObject2 suites tend to fail with stale object errors after screen changes, but those are easier to diagnose because the error names the problem. Condition-based waits remove a whole class of timing failures that sleeps cannot.
Result: UiObject2 for timing, UiObject for resilience to redraws. Overall UiObject2 wins for me because waits are the bigger source of flakes.
Speed
Because UiObject re-queries the accessibility tree on every call, a script with many calls on the same element does repeated work. UiObject2 queries once and reuses the node. I have not measured this on a fixed device set, so I will not give numbers, and the difference on a modern phone is often small compared with the time the app itself takes to respond. Where I do notice it is in loops that read the same field many times, or on cheap phones with big view trees.
Gesture speed is a separate issue. UiObject swipes are governed by steps, UiObject2 swipes by a speed value in pixels per second for drags and fling, with the swipe method using a default speed unless you pass one. UiObject2 is easier to tune.
Result: UiObject2, with the caveat that I have no benchmark.
Pricing per GB: cost
Both are free. They ship in the AndroidX test libraries under the Apache 2.0 licence, and you pay nothing per call or per GB. The real cost is engineer time: writing sleeps, debugging flakes, and migrating later. UiObject has lower upfront cost if you already have the suite. UiObject2 has lower ongoing cost because waits and gestures need less tuning.
Result: tie on price, UiObject2 on ongoing cost.
Session persistence: how long a handle stays valid
This is the axis where the two behave most differently, so it is worth a plain explanation.
A UiObject stays usable for as long as the selector matches something. It survives redraws, rotations and navigation, provided the matching view exists when you call. A UiObject2 is valid only while its node is. After a screen change, a scroll that recycles the view, or an app restart, it goes stale and the next call throws StaleObjectException. UiObject2 also has a recycle() method from older versions; check the reference for its current status before relying on it.
My rule: never hold a UiObject2 across a navigation or a long wait. Look it up, use it, drop it. If you need to reuse a target, keep the BySelector, not the object. That gives you UiObject’s re-resolving behaviour with UiObject2’s waits and gestures.
Result: UiObject for persistence, but the BySelector pattern closes most of the gap.
Concurrent connections: parallel devices and instances
Neither API manages concurrency. A UiDevice is tied to one instrumentation run on one device. To test on many phones you run many instrumentation processes, one per device, and the limit is your host, not the API. I wrote about the host side in host limits when running many Android phones. If you want to run UI Automator 2 inside containers, see running UiAutomator2 device automation inside a Docker container.
One thing worth knowing: Appium’s UiAutomator2 driver uses UI Automator under the hood, so if you drive tests through Appium you may never touch UiObject or UiObject2 directly. The Maestro vs Appium comparison covers when to pick which.
Result: tie.
use-case verdicts
Writing a new instrumentation test suite
Winner: UiObject2. Condition-based waits, direction-based gestures, and null-returning lookups give you shorter tests with fewer sleeps. If you are starting from nothing, there is no good reason to pick UiObject.
Maintaining or extending an older suite
Winner: UiObject, for now. Mixing the two in one file is fine, since both come from the same UiDevice, but rewriting a working suite just to change APIs rarely pays off. Migrate file by file when you touch a test for another reason. Keep the old selectors in a helper and add BySelector equivalents beside them.
Scrolling, swiping and pinch-heavy screens
Winner: UiObject2. Maps, carousels, pull-to-refresh and bottom sheets are where the direction-and-percent API is much easier to get right. UiObject has no pinch support, so you would be dropping to raw gesture injection.
Screens that redraw constantly
Winner: UiObject, or UiObject2 with stored BySelectors. Feeds, chat screens and anything with live updates will make a held UiObject2 stale. Either use the lazy UiObject, or re-query a stored BySelector right before each action.
who should pick Vendor A
Pick UiObject if you own a large existing suite written against UiSelector and the tests pass. Pick it if you support very old Android versions that the newer library no longer covers. Pick it for quick one-off scripts where a lazy handle saves you thinking about staleness. And if you are reading old Stack Overflow answers and copying snippets, most of them use UiObject, so you will see it everywhere.
A note on scope: use any of this only on apps and devices you own or are authorised to test. UI Automator is a testing framework. If an app blocks automation, that is a decision for the app owner, and this article does not cover getting around it. If you need elevated access on your own test devices, root vs non-root Android automation sets out the trade-offs, and uiautomation vs AccessibilityService explains what sits underneath both APIs.
who should pick Vendor B
Pick UiObject2 if you are starting a new suite, if your tests spend more time waiting than clicking, or if you do a lot of gestures. Pick it if your team reads the current Android documentation, because the examples there use BySelector and Until. And pick it if you want errors that say what went wrong: a stale object is a clearer message than a generic not-found after a timeout.
If you run on rented phones, plan for the adb path. On cloudf.one there is no self-service adb, so you would email [email protected] to get the WireGuard tunnel set up before your instrumentation run can reach the phone. That is a one-time setup step, but it is a step, and it is Singapore only.
verdict overall
UiObject2 wins for new work. The deciding factors are waits and gestures: Until conditions replace sleeps, and swipe, scroll and pinch take a direction and a percentage. That removes the most common causes of flaky UI Automator tests. The one real weakness is staleness, and the fix is simple: hold the BySelector, not the object, and re-query before you act.
UiObject is not wrong. It is the older, lazier model, and for existing suites and constantly redrawing screens it still does the job. I would not rewrite a passing suite just to switch. I would write every new test with UiObject2, and move old ones when I next have to touch them.
If you remember one thing from this article, make it this: a UiObject is a question you ask again each time, and a UiObject2 is an answer you got once. Use each accordingly.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-16.