Designing Mobile Page Classes in Java

7 min read

The Page Object Model (POM) works on mobile the same way it does on web: one class per screen, locators and actions inside, assertions outside. The difference is that mobile screens are more navigation-heavy — swipes, back-button presses, and tab switches are part of normal user flows and belong in the page object, not scattered across tests.

The minimal page object contract

Every page object needs three things:

  1. A reference to the driver
  2. Locator constants as private static final By fields
  3. Action methods that return the next page object (or this for staying on the same page)
package com.example.pages;
 
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.AppiumBy;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
 
public class LoginPage {
 
    private final AppiumDriver driver;
    private final WebDriverWait wait;
 
    private static final By EMAIL_FIELD = AppiumBy.accessibilityId("emailInput");
    private static final By PASSWORD_FIELD = AppiumBy.accessibilityId("passwordInput");
    private static final By LOGIN_BUTTON = AppiumBy.accessibilityId("loginButton");
    private static final By ERROR_BANNER = AppiumBy.accessibilityId("errorBanner");
 
    public LoginPage(AppiumDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }
 
    public HomePage login(String email, String password) {
        driver.findElement(EMAIL_FIELD).sendKeys(email);
        driver.findElement(PASSWORD_FIELD).sendKeys(password);
        driver.findElement(LOGIN_BUTTON).click();
        return new HomePage(driver);
    }
 
    public String getErrorMessage() {
        return wait.until(d -> d.findElement(ERROR_BANNER)).getText();
    }
}

login() returns a HomePage — this chain enforces the navigation contract in the type system. If login should stay on the page (wrong password), getErrorMessage() returns without navigating.

Fluent method chaining

Returning the next page object enables readable test chains:

String cartTotal = new LoginPage(driver)
    .login("user@example.com", "password")
    .tapProduct("Wireless Headphones")
    .addToCart()
    .viewCart()
    .getCartTotal();
 
assertThat(cartTotal).isEqualTo("$99.99");

Each method returns the page it lands on, so the chain reads like a user journey.

Handling navigation patterns

Mobile apps navigate differently from web. Model these patterns in page objects:

Tab navigation:

public class HomePage {
    private static final By PROFILE_TAB = AppiumBy.accessibilityId("profileTab");
 
    public ProfilePage openProfile() {
        driver.findElement(PROFILE_TAB).click();
        return new ProfilePage(driver);
    }
}

Back button:

public class ProductDetailPage {
    public ProductListPage goBack() {
        driver.navigate().back();
        return new ProductListPage(driver);
    }
}

Bottom sheet / modal:

public class CartPage {
    private static final By CHECKOUT_SHEET_CONFIRM = AppiumBy.accessibilityId("confirmCheckout");
 
    public OrderConfirmationPage confirmCheckout() {
        driver.findElement(CHECKOUT_SHEET_CONFIRM).click();
        return new OrderConfirmationPage(driver);
    }
}

Shared base page

Extract the wait and driver boilerplate into a base class that all page objects extend:

package com.example.pages;
 
import io.appium.java_client.AppiumDriver;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
 
public abstract class BasePage {
 
    protected final AppiumDriver driver;
    protected final WebDriverWait wait;
 
    protected BasePage(AppiumDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));
    }
 
    protected WebElement waitForVisible(By locator) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }
 
    protected WebElement waitForClickable(By locator) {
        return wait.until(ExpectedConditions.elementToBeClickable(locator));
    }
 
    protected boolean isDisplayed(By locator) {
        try {
            return driver.findElement(locator).isDisplayed();
        } catch (org.openqa.selenium.NoSuchElementException e) {
            return false;
        }
    }
}

Page objects then extend BasePage:

public class LoginPage extends BasePage {
 
    public LoginPage(AppiumDriver driver) {
        super(driver);
    }
 
    public HomePage login(String email, String password) {
        waitForClickable(EMAIL_FIELD).sendKeys(email);
        waitForClickable(PASSWORD_FIELD).sendKeys(password);
        waitForClickable(LOGIN_BUTTON).click();
        return new HomePage(driver);
    }
}

What NOT to put in a page object

  • Assertions: belong in the test class, not the page object. Page objects describe capability, not correctness.
  • Test data: the page object shouldn't generate or own credentials, product names, or expected values.
  • Control flow: if (platform == iOS) in a page object is a smell — use separate page objects for screens that differ significantly across platforms.

Cross-platform page objects

For screens with structurally identical UX but different locators:

public abstract class LoginPage extends BasePage {
    public abstract HomePage login(String email, String password);
    public abstract String getErrorMessage();
}
 
public class AndroidLoginPage extends LoginPage {
    private static final By EMAIL = AppiumBy.androidUIAutomator(
        "new UiSelector().resourceId(\"com.example.app:id/email_field\")"
    );
    // ...
}
 
public class IOSLoginPage extends LoginPage {
    private static final By EMAIL = AppiumBy.iOSNsPredicateString(
        "type == 'XCUIElementTypeTextField' AND name == 'emailField'"
    );
    // ...
}

The test receives a LoginPage reference. Which concrete class it gets is decided in the base test setup based on the platform parameter.

// tip to track lessons you complete and pick up where you left off across devices.