In an Appium `-android uiautomator` expression, which selector does UiScrollable take and which does scrollIntoView?
answer
- outer selector is the container
- inner selector is the target
- scrollable(true) narrows the container
- one string, still one find
basics
~20 sThe UiSelector passed to UiScrollable addresses the scrollable container, usually one with scrollable set to true. The UiSelector passed to scrollIntoView addresses the element you actually want back. Swapping the two is the common mistake.
solid answer
~40 s`UiScrollable` wraps a container, so its constructor argument is a `UiSelector` for the **scrolling view itself** — typically `new UiSelector().scrollable(true)`, narrowed with a `resourceId` or `.instance(0)` when a screen has more than one. The argument to `scrollIntoView(...)` is a **separate `UiSelector` for the target**, and the target is what the find returns. The whole thing is still one `-android uiautomator` string in one find request handled by Appium's UiAutomator2 driver on Android. Helpers on the wrapper shape the *search* rather than the *match*: `setAsHorizontalList()` for a horizontal pager, `setMaxSearchSwipes(n)` for how far it will look. If the outer selector matches nothing, the expression fails on the container long before the target is ever considered.
code
java · 16 linesimport io.appium.java_client.AppiumBy;
import io.appium.java_client.android.AndroidDriver;
import org.openqa.selenium.WebElement;
public final class ScrollIntoViewSelector {
private static final String FEBRUARY_ROW = """
new UiScrollable(new UiSelector().resourceId("com.example.museum:id/month_list")).scrollIntoView(new UiSelector().text("February"))""";
private ScrollIntoViewSelector() {
}
public static WebElement februaryRow(AndroidDriver driver) {
return driver.findElement(AppiumBy.androidUIAutomator(FEBRUARY_ROW));
}
}go deeper
Remember the shape: the selector inside UiScrollable names the list, and the selector inside scrollIntoView names the row you want back. Getting those two the right way round is most of the battle.
Explain that the whole expression is one string evaluated on the device in one find, and that direction and swipe bounds shape the search while the inner selector alone decides what matches.
Show how you split a failing expression: prove the container selector matches exactly one view on its own before wrapping it, since the device gives you one error for the whole string.
Weigh what nesting matching and traversal in one opaque string costs a suite in debuggability, and be clear about which failures it can no longer tell apart for you.
## Two selectors, two jobs The scrolling form of the Appium `-android uiautomator` dialect nests one selector inside another, and the whole trick to reading it is knowing which is which: ``` new UiScrollable(new UiSelector().scrollable(true)) .scrollIntoView(new UiSelector().text("February")) ``` - The `UiSelector` handed to the **`UiScrollable` constructor** addresses the **container** — the view that will be swiped. - The `UiSelector` handed to **`scrollIntoView(...)`** addresses the **target** — the element you want, and the element the find returns. Swapping them is the single most common error with this expression, and it fails in a confusing way: the framework tries to swipe something that does not scroll, or finds no container at all, so the message points at the container rather than at the row you were chasing. ## Why a container selector is needed at all A scroll has to happen *on* something. Android's UiAutomator framework does not guess which view scrolls; you tell it, and `scrollable(true)` is the usual way, because a scrolling view reports itself as scrollable in the accessibility hierarchy. That default is deliberately loose, and on a real screen it is often too loose. Ways to tighten the container selector, in rough order of preference: - `new UiSelector().resourceId("com.example.museum:id/month_list")` — name the list outright when it has an id. - `new UiSelector().scrollable(true).instance(0)` — pick the first scrollable when several exist and only their order is stable. - `new UiSelector().className("androidx.recyclerview.widget.RecyclerView")` — pin the widget type when the id is not stable. A nested scrollable is the case that catches people: an outer container that also reports `scrollable(true)` can win the match, and then the swipes land on the wrong view while the target never comes into range. ## Shaping the search, not the match `UiScrollable` carries a few calls that change **how the search is conducted** rather than **what counts as a match**: - `setAsHorizontalList()` — swipe left and right instead of up and down, for a pager or a carousel. - `setAsVerticalList()` — the default direction, worth stating explicitly when a screen mixes both. - `setMaxSearchSwipes(n)` — bound how many swipes the framework will spend before giving up, so a very long list fails as a not-found rather than searching on and on. None of these touches the target `UiSelector`. If the target expression is wrong, no amount of search shaping will find it; if the container is wrong, the search never happens on the right view. Keeping those three concerns separate — container, target, search shape — is what makes these expressions debuggable. ## It is still one find A point that is easy to miss: this is a single `-android uiautomator` selector value, sent in a single find request. Appium is not issuing a scroll command and then a find. The whole expression is parsed on the device by the UiAutomator2 driver's on-device server, and everything — the container match, the swiping, the target match — happens there before one result comes back. That is why the failure, when it comes, arrives as a find failure rather than as a gesture failure. ## Reading a failure | symptom | most likely cause | |---|---| | error mentions the scrollable container | the outer selector matched nothing, or nothing scrollable | | swipes happen but the target is never found | the wrong container matched, or the target selector is wrong | | works on a short screen, fails on a long one | the search gave up inside its swipe bound | | swipes vertically on a carousel | the list direction was never declared | ## What this leaf's version of the question is The interview question here is about **expression shape**. The thing to be able to say without hesitating is: the outer selector names the thing that moves, the inner selector names the thing you want, and the return value is the inner one. Everything else — the direction, the swipe bound, the choice of container matcher — hangs off that sentence. A final habit worth stating: build the container selector on its own first, as a plain expression, and confirm it matches exactly one element. Only then wrap it in `UiScrollable`. Because the whole thing is one opaque string evaluated on the device, splitting it in half is the only way to learn which half is wrong.
- Two scrollable views are on screen — how do you pin the right one in the expression?Narrow the **container** selector, not the target. Add `.resourceId("com.example.museum:id/month_list")` or `.instance(0)` to the `UiSelector` passed to `UiScrollable`. A bare `scrollable(true)` lets the framework take the first scrollable it meets, which is often an outer wrapper rather than the list you meant.
- What does `setMaxSearchSwipes` change about the expression?It bounds how many swipes the framework spends looking before it gives up, so a very long list reports a not-found instead of searching indefinitely. It shapes the search, never the match — the target `UiSelector` is untouched, and raising the bound will not find an element the target expression cannot describe.
saying these in an interview costs you the question
- Passes the target selector to UiScrollable instead of the container
- Assumes scrollable(true) is specific enough when two lists are visible
- Thinks the expression sends two separate requests to the device
- Expects a horizontal carousel to work without declaring the direction
- Believes setMaxSearchSwipes changes which element matches