Mobile apps have asynchronous UIs: animations complete, network calls return, view transitions settle. Tests that interact too early get NoSuchElementException or ElementNotInteractableException. Explicit waits — not Thread.sleep() — are the correct solution.
Why not Thread.sleep()
Thread.sleep(3000) always waits 3 seconds, even when the element appears in 200ms. On a suite with 200 tests, that's 10 minutes of guaranteed wasted time. Worse, on a slow device or CI machine, 3 seconds may not be enough, so you increase to 5 seconds and waste even more.
Explicit waits poll until the condition is true or the timeout expires. If the element appears in 200ms, the wait returns in 200ms.
WebDriverWait basics
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// Wait for element to be visible
WebElement element = wait.until(
ExpectedConditions.visibilityOfElementLocated(By.id("submitButton"))
);
// Wait for element to be clickable (visible + enabled)
WebElement button = wait.until(
ExpectedConditions.elementToBeClickable(AppiumBy.accessibilityId("login"))
);
// Wait for text to appear
wait.until(ExpectedConditions.textToBePresentInElementLocated(
AppiumBy.accessibilityId("status"),
"Order placed"
));Common ExpectedConditions for mobile
| Condition | When to use |
|---|---|
visibilityOfElementLocated | Element exists in DOM and has non-zero dimensions |
elementToBeClickable | Element is visible AND enabled (handles disabled buttons) |
invisibilityOfElementLocated | Wait for loading spinner to disappear |
presenceOfElementLocated | Element is in DOM (may be invisible — use sparingly) |
textToBePresentInElementLocated | Wait for async value update |
numberOfElementsToBeMoreThan | Wait for list to populate |
Custom wait conditions
ExpectedConditions doesn't have everything. Write lambdas for custom conditions:
// Wait for element count to stabilise (list finished loading)
wait.until(driver -> {
List<WebElement> items = driver.findElements(
AppiumBy.accessibilityId("listItem")
);
return items.size() > 0 ? items : null;
});
// Wait for attribute to change
wait.until(driver -> {
WebElement toggle = driver.findElement(AppiumBy.accessibilityId("darkModeToggle"));
return "true".equals(toggle.getAttribute("checked"));
});
// Wait for app to be on a specific screen (Android)
wait.until(driver -> {
String activity = ((AndroidDriver) driver).currentActivity();
return activity.contains("HomeActivity");
});The lambda returns null or false to signal "keep waiting", and any non-null/non-false value to signal "condition met — return this".
FluentWait for fine-grained control
FluentWait lets you configure polling interval and ignored exceptions independently:
import org.openqa.selenium.support.ui.FluentWait;
FluentWait<AppiumDriver> wait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(15))
.pollingEvery(Duration.ofMillis(500))
.ignoring(NoSuchElementException.class)
.ignoring(StaleElementReferenceException.class);
WebElement result = wait.until(driver ->
driver.findElement(AppiumBy.accessibilityId("searchResult"))
);pollingEvery(500ms) reduces the check frequency, which matters on slow devices where every findElement call takes 100ms. The default WebDriverWait polls every 500ms; you can tune this per-wait.
Waiting for animations to complete
Many mobile transitions have an animation that runs after a tap. Interacting with elements during the animation causes flake:
// After tap that triggers an animation:
button.click();
// Wait for the animated element to reach its final state
new WebDriverWait(driver, Duration.ofSeconds(3))
.pollingEvery(Duration.ofMillis(200))
.until(driver -> {
WebElement panel = driver.findElement(AppiumBy.accessibilityId("expandedPanel"));
// Panel height stops changing when animation is complete
int height = panel.getSize().getHeight();
Thread.sleep(200);
return height == panel.getSize().getHeight();
});This "stable dimension" technique works for slide-in and expand animations.
Waiting for loading spinners to disappear
After triggering an async operation, wait for the spinner to disappear before asserting on results:
private static final By LOADING_SPINNER = AppiumBy.accessibilityId("loadingIndicator");
public void waitForLoadingToComplete() {
// First, wait for the spinner to appear (may take up to 1 second)
try {
new WebDriverWait(driver, Duration.ofSeconds(1))
.until(ExpectedConditions.visibilityOfElementLocated(LOADING_SPINNER));
} catch (TimeoutException e) {
// Spinner may have appeared and disappeared before we checked
return;
}
// Then wait for it to disappear
new WebDriverWait(driver, Duration.ofSeconds(30))
.until(ExpectedConditions.invisibilityOfElementLocated(LOADING_SPINNER));
}Don't skip the "wait for spinner to appear" step — if your test jumps straight to "wait for spinner to disappear" and the spinner hasn't appeared yet, the condition is immediately true and you assert on stale data.
Centralising wait logic
Put common wait patterns in WaitUtils so page objects don't repeat them:
public class WaitUtils {
private final AppiumDriver driver;
private final WebDriverWait shortWait; // 5s for fast operations
private final WebDriverWait defaultWait; // 15s for network calls
private final WebDriverWait longWait; // 30s for slow operations
public WaitUtils(AppiumDriver driver) {
this.driver = driver;
this.shortWait = new WebDriverWait(driver, Duration.ofSeconds(5));
this.defaultWait = new WebDriverWait(driver, Duration.ofSeconds(15));
this.longWait = new WebDriverWait(driver, Duration.ofSeconds(30));
}
public WebElement waitForVisible(By locator) {
return defaultWait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
public WebElement waitForClickable(By locator) {
return defaultWait.until(ExpectedConditions.elementToBeClickable(locator));
}
public void waitForInvisible(By locator) {
longWait.until(ExpectedConditions.invisibilityOfElementLocated(locator));
}
public boolean isPresent(By locator) {
try {
shortWait.until(ExpectedConditions.presenceOfElementLocated(locator));
return true;
} catch (TimeoutException e) {
return false;
}
}
}