# Appium — Advanced Implementation Playbook
## §1 — Project Setup & Capabilities
```java
io.appium
java-client
9.1.0
org.seleniumhq.selenium
selenium-java
4.18.1
org.testng
testng
7.9.0
test
io.qameta.allure
allure-testng
2.25.0
```
### Android Capabilities
```java
UiAutomator2Options androidOptions() {
return new UiAutomator2Options()
.setDeviceName(System.getProperty("device.name", "Pixel_6"))
.setPlatformVersion(System.getProperty("platform.version", "14"))
.setApp(System.getProperty("app.path", "src/test/resources/app-debug.apk"))
.setAutomationName("UiAutomator2")
.setAutoGrantPermissions(true)
.setNoReset(Boolean.parseBoolean(System.getProperty("no.reset", "false")))
.setNewCommandTimeout(Duration.ofSeconds(300))
.setAdbExecTimeout(Duration.ofSeconds(60))
.setUiautomator2ServerInstallTimeout(Duration.ofSeconds(120))
.setAmplitude(new HashMap<>() {{ put("appWaitActivity", "*"); }});
}
```
### iOS Capabilities
```java
XCUITestOptions iosOptions() {
return new XCUITestOptions()
.setDeviceName(System.getProperty("device.name", "iPhone 15"))
.setPlatformVersion(System.getProperty("platform.version", "17.2"))
.setApp(System.getProperty("app.path", "src/test/resources/MyApp.app"))
.setAutomationName("XCUITest")
.setWdaLaunchTimeout(Duration.ofSeconds(120))
.setWdaConnectionTimeout(Duration.ofSeconds(120))
.setAutoAcceptAlerts(true)
.setShowXcodeLog(true);
}
```
## §2 — BaseTest with Thread-Safe Driver
```java
public class BaseTest {
protected static final ThreadLocal driverThread = new ThreadLocal<>();
protected AppiumDriver driver;
@Parameters({"platform", "deviceName", "platformVersion"})
@BeforeMethod(alwaysRun = true)
public void setUp(@Optional("android") String platform,
@Optional("Pixel_6") String deviceName,
@Optional("14") String platformVersion) throws Exception {
String appiumUrl = System.getProperty("appium.url", "http://127.0.0.1:4723");
BaseOptions> options;
if ("ios".equalsIgnoreCase(platform)) {
options = iosOptions()
.setDeviceName(deviceName)
.setPlatformVersion(platformVersion);
} else {
options = androidOptions()
.setDeviceName(deviceName)
.setPlatformVersion(platformVersion);
}
AppiumDriver d = new AppiumDriver(new URL(appiumUrl), options);
d.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driverThread.set(d);
driver = d;
}
@AfterMethod(alwaysRun = true)
public void tearDown(ITestResult result) {
AppiumDriver d = driverThread.get();
if (d != null) {
if (result.getStatus() == ITestResult.FAILURE) {
captureScreenshot(result.getName());
}
d.quit();
driverThread.remove();
}
}
public AppiumDriver getDriver() { return driverThread.get(); }
private void captureScreenshot(String testName) {
try {
byte[] screenshot = driverThread.get().getScreenshotAs(OutputType.BYTES);
Allure.addAttachment(testName + "-failure", "image/png",
new ByteArrayInputStream(screenshot), "png");
} catch (Exception e) { /* log */ }
}
protected boolean isAndroid() {
return driver.getCapabilities().getPlatformName()
.toString().equalsIgnoreCase("android");
}
}
```
## §3 — Cross-Platform Page Objects
```java
public class LoginScreen {
private final AppiumDriver driver;
private final WebDriverWait wait;
@AndroidFindBy(accessibility = "email_input")
@iOSXCUITFindBy(accessibility = "email_input")
private WebElement emailField;
@AndroidFindBy(accessibility = "password_input")
@iOSXCUITFindBy(accessibility = "password_input")
private WebElement passwordField;
@AndroidFindBy(accessibility = "login_button")
@iOSXCUITFindBy(accessibility = "login_button")
private WebElement loginBtn;
@AndroidFindBy(id = "com.app:id/error_text")
@iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND name CONTAINS 'error'")
private WebElement errorMsg;
public LoginScreen(AppiumDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));
PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(15)), this);
}
public HomeScreen login(String email, String password) {
emailField.clear(); emailField.sendKeys(email);
passwordField.clear(); passwordField.sendKeys(password);
hideKeyboard();
loginBtn.click();
return new HomeScreen(driver);
}
public String getError() {
wait.until(ExpectedConditions.visibilityOf(errorMsg));
return errorMsg.getText();
}
public boolean isDisplayed() {
try { return emailField.isDisplayed(); }
catch (Exception e) { return false; }
}
private void hideKeyboard() {
try { driver.hideKeyboard(); } catch (Exception e) { /* not shown */ }
}
}
```
## §4 — Advanced Gestures (W3C Actions)
```java
public class GestureHelper {
private final AppiumDriver driver;
public GestureHelper(AppiumDriver driver) { this.driver = driver; }
// Swipe direction
public void swipe(Direction direction, double percent) {
Dimension size = driver.manage().window().getSize();
int startX, startY, endX, endY;
switch (direction) {
case LEFT:
startX = (int)(size.width * 0.8); endX = (int)(size.width * 0.2);
startY = endY = size.height / 2; break;
case RIGHT:
startX = (int)(size.width * 0.2); endX = (int)(size.width * 0.8);
startY = endY = size.height / 2; break;
case UP:
startY = (int)(size.height * 0.8); endY = (int)(size.height * 0.2);
startX = endX = size.width / 2; break;
case DOWN:
startY = (int)(size.height * 0.2); endY = (int)(size.height * 0.8);
startX = endX = size.width / 2; break;
default: throw new IllegalArgumentException("Invalid direction");
}
performSwipe(startX, startY, endX, endY, 600);
}
private void performSwipe(int startX, int startY, int endX, int endY, int durationMs) {
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence swipe = new Sequence(finger, 0)
.addAction(finger.createPointerMove(Duration.ZERO,
PointerInput.Origin.viewport(), startX, startY))
.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
.addAction(finger.createPointerMove(Duration.ofMillis(durationMs),
PointerInput.Origin.viewport(), endX, endY))
.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
driver.perform(Collections.singletonList(swipe));
}
// Long press
public void longPress(WebElement element, int durationMs) {
Point center = element.getLocation();
Dimension elemSize = element.getSize();
int x = center.x + elemSize.width / 2;
int y = center.y + elemSize.height / 2;
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence longPress = new Sequence(finger, 0)
.addAction(finger.createPointerMove(Duration.ZERO,
PointerInput.Origin.viewport(), x, y))
.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
.addAction(new Pause(finger, Duration.ofMillis(durationMs)))
.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
driver.perform(Collections.singletonList(longPress));
}
// Pinch zoom
public void pinchZoom(int centerX, int centerY, int distance) {
PointerInput f1 = new PointerInput(PointerInput.Kind.TOUCH, "finger1");
PointerInput f2 = new PointerInput(PointerInput.Kind.TOUCH, "finger2");
Sequence s1 = new Sequence(f1, 0)
.addAction(f1.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), centerX, centerY))
.addAction(f1.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
.addAction(f1.createPointerMove(Duration.ofMillis(600),
PointerInput.Origin.viewport(), centerX, centerY - distance))
.addAction(f1.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
Sequence s2 = new Sequence(f2, 0)
.addAction(f2.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), centerX, centerY))
.addAction(f2.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
.addAction(f2.createPointerMove(Duration.ofMillis(600),
PointerInput.Origin.viewport(), centerX, centerY + distance))
.addAction(f2.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
driver.perform(Arrays.asList(s1, s2));
}
// Scroll to element — Android
public WebElement scrollToText(String text) {
return driver.findElement(AppiumBy.androidUIAutomator(
"new UiScrollable(new UiSelector().scrollable(true))" +
".scrollIntoView(new UiSelector().text(\"" + text + "\"))"));
}
// Scroll to element — iOS
public void scrollToElementIOS(String label) {
driver.executeScript("mobile: scroll", Map.of(
"direction", "down",
"predicateString", "label == '" + label + "'"
));
}
enum Direction { UP, DOWN, LEFT, RIGHT }
}
```
## §5 — WebView & Hybrid App Testing
```java
// Switch between NATIVE and WEBVIEW contexts
@Test
public void testHybridApp() {
// Start in native context
loginScreen.login("user@test.com", "pass123");
// Wait for WebView
new WebDriverWait(driver, Duration.ofSeconds(20)).until(d -> {
Set contexts = ((SupportsContextSwitching) d).getContextHandles();
return contexts.stream().anyMatch(c -> c.contains("WEBVIEW"));
});
// Switch to WebView
Set contexts = ((SupportsContextSwitching) driver).getContextHandles();
String webViewContext = contexts.stream()
.filter(c -> c.contains("WEBVIEW"))
.findFirst().orElseThrow();
((SupportsContextSwitching) driver).context(webViewContext);
// Now use web selectors
driver.findElement(By.cssSelector("[data-testid='web-content']")).click();
// Switch back to native
((SupportsContextSwitching) driver).context("NATIVE_APP");
assertTrue(driver.findElement(AppiumBy.accessibilityId("home_tab")).isDisplayed());
}
```
## §6 — Device Interactions
```java
// Push/pull files
driver.pushFile("/sdcard/Download/test.txt", new File("testdata/test.txt"));
byte[] fileData = driver.pullFile("/sdcard/Download/result.txt");
// App lifecycle
driver.terminateApp("com.myapp");
driver.activateApp("com.myapp");
boolean isRunning = driver.isAppInstalled("com.myapp");
driver.installApp("/path/to/app.apk");
driver.removeApp("com.myapp");
// Notifications (Android)
driver.openNotifications();
driver.findElement(AppiumBy.xpath("//android.widget.TextView[@text='New message']")).click();
// Clipboard
driver.setClipboardText("test data");
String clipboardText = driver.getClipboardText();
// Device orientation
driver.rotate(ScreenOrientation.LANDSCAPE);
driver.rotate(ScreenOrientation.PORTRAIT);
// Geolocation
driver.setLocation(new Location(37.7749, -122.4194, 10)); // San Francisco
// Network conditions (Android)
driver.toggleWifi();
driver.toggleAirplaneMode();
driver.toggleData();
// Deep linking
driver.get("myapp://screen/settings");
```
## §7 — Parallel Device Execution
```xml
```
## §8 — LambdaTest Real Device Cloud
```java
public AppiumDriver createLambdaTestDriver(String platform, String device) throws Exception {
String username = System.getenv("LT_USERNAME");
String accessKey = System.getenv("LT_ACCESS_KEY");
String url = "https://" + username + ":" + accessKey + "@mobile-hub.lambdatest.com/wd/hub";
HashMap ltOptions = new HashMap<>();
ltOptions.put("build", "Build " + System.getenv("BUILD_NUMBER"));
ltOptions.put("name", "Appium Tests");
ltOptions.put("isRealMobile", true);
ltOptions.put("video", true);
ltOptions.put("network", true);
ltOptions.put("console", true);
ltOptions.put("visual", true);
if ("android".equalsIgnoreCase(platform)) {
UiAutomator2Options options = new UiAutomator2Options()
.setDeviceName(device)
.setPlatformVersion("14")
.setApp("lt://APP_ID")
.setCapability("LT:Options", ltOptions);
return new AppiumDriver(new URL(url), options);
} else {
XCUITestOptions options = new XCUITestOptions()
.setDeviceName(device)
.setPlatformVersion("17")
.setApp("lt://APP_ID")
.setCapability("LT:Options", ltOptions);
return new AppiumDriver(new URL(url), options);
}
}
```
## §9 — CI/CD Integration
```yaml
# GitHub Actions
name: Appium Tests
on: [push, pull_request]
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 17 }
- name: Start emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
script: |
npm install -g appium
appium driver install uiautomator2
appium &
sleep 10
mvn test -DsuiteXmlFile=testng-android.xml -Dplatform=android
- uses: actions/upload-artifact@v4
if: always()
with:
name: appium-results
path: |
target/surefire-reports/
allure-results/
screenshots/
cloud:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 17 }
- run: mvn test -DsuiteXmlFile=testng-cloud.xml
env:
LT_USERNAME: ${{ secrets.LT_USERNAME }}
LT_ACCESS_KEY: ${{ secrets.LT_ACCESS_KEY }}
```
## §10 — Debugging Quick-Reference
| Problem | Cause | Fix |
|---------|-------|-----|
| Element not found | Wrong locator strategy | Use Appium Inspector; prefer `accessibilityId` |
| Session creation fails | Incompatible capabilities | Verify device name, platform version, app path |
| App not installed | Invalid APK/IPA path | Use absolute path, verify file exists |
| iOS WDA build fails | Xcode/provisioning issue | Clean DerivedData, increase `wdaLaunchTimeout` |
| Android permission dialog | Runtime permission blocking | Set `autoGrantPermissions: true` |
| Slow element finding | XPath locator | Switch to `accessibilityId` or `id` (10x faster) |
| Keyboard covers element | Soft keyboard blocking | Call `driver.hideKeyboard()` before tap |
| Touch action fails | Wrong coordinates | Use element center: `element.getRect()` |
| WebView not found | Context not switched | List contexts first, wait for WEBVIEW context |
| App crash mid-test | App state corrupted | Use `noReset: false` for clean state between tests |
| Flaky on real devices | Timing/animation issues | Increase waits, disable animations in developer options |
| Parallel port conflicts | Same Appium port | Use different `systemPort` per device |
## §11 — Best Practices Checklist
- ✅ Use `accessibilityId` locator for cross-platform (fastest, most stable)
- ✅ Use `@AndroidFindBy` / `@iOSXCUITFindBy` for platform-specific elements
- ✅ Use `ThreadLocal` for parallel-safe execution
- ✅ Use W3C Actions API for gestures (not deprecated TouchAction)
- ✅ Use `GestureHelper` utility class for reusable swipe/scroll/zoom
- ✅ Set `autoGrantPermissions: true` (Android) and `autoAcceptAlerts: true` (iOS)
- ✅ Use `noReset: true` for speed, `fullReset: true` for clean state
- ✅ Call `hideKeyboard()` after text input before next interaction
- ✅ Use Appium Inspector for locator discovery and validation
- ✅ Capture screenshots + page source on failure via listener
- ✅ Run on real device cloud (LambdaTest) for production confidence
- ✅ Use WebView context switching for hybrid app testing
- ✅ Structure: `screens/`, `tests/`, `utils/`, `testdata/`, `listeners/`