PageFactory bridges the annotation-declared locators on a page object class with the actual Appium session. On mobile, AppiumFieldDecorator replaces Selenium's standard decorator to handle the mobile-specific annotation types and timing requirements.
How PageFactory works internally
When you call PageFactory.initElements(decorator, pageObject), it reflects over every field in the class looking for locator annotations. For each annotated field, it replaces the field with a dynamic proxy. When you call a method on the proxy (like click() or sendKeys()), the proxy:
- Calls
driver.findElement(locator)to locate the current element - Delegates the method to the located element
This on-demand lookup means the element is always fetched fresh, preventing stale references from page reloads or recreations.
AppiumFieldDecorator vs DefaultFieldDecorator
Selenium's DefaultFieldDecorator doesn't know about @AndroidFindBy or @iOSXCUITFindBy. Using it in Appium tests silently ignores mobile annotations:
// Wrong — mobile annotations ignored
PageFactory.initElements(driver, this);
// Correct — mobile-aware decorator
PageFactory.initElements(new AppiumFieldDecorator(driver), this);AppiumFieldDecorator understands both Selenium's @FindBy and Appium's mobile-specific annotations. It selects the right annotation for the current platform automatically.
Configuring the implicit wait timeout
The decorator has a configurable timeout for element lookup. The default varies by Appium client version — set it explicitly:
// Wait up to 10 seconds for each element
PageFactory.initElements(
new AppiumFieldDecorator(driver, Duration.ofSeconds(10)),
this
);This timeout applies to the proxy's element lookup. If the element isn't found within the timeout, NoSuchElementException is thrown. This is distinct from Appium's implicit wait setting — the decorator manages its own timeout.
Initialising from the base page
In the base page pattern, call PageFactory.initElements in the base constructor:
public abstract class BasePage {
protected final AppiumDriver driver;
protected BasePage(AppiumDriver driver) {
this.driver = driver;
PageFactory.initElements(
new AppiumFieldDecorator(driver, Duration.ofSeconds(10)),
this
);
}
}
public class LoginPage extends BasePage {
@AndroidFindBy(accessibility = "loginButton")
@iOSXCUITFindBy(accessibility = "loginButton")
private WebElement loginButton;
public LoginPage(AppiumDriver driver) {
super(driver); // PageFactory.initElements called here
}
}PageFactory with inherited fields
PageFactory.initElements initialises fields declared in the class it's called on AND all parent classes. Annotated fields in BasePage (if any) are also initialised when you call initElements(decorator, loginPageInstance).
This means you can put common elements — headers, bottom navigation bars, system overlays — in a parent class:
public abstract class BasePage {
@AndroidFindBy(accessibility = "navigationBar")
@iOSXCUITFindBy(accessibility = "navigationBar")
protected WebElement navigationBar;
protected BasePage(AppiumDriver driver) {
PageFactory.initElements(new AppiumFieldDecorator(driver), this);
}
public boolean isNavigationBarVisible() {
try {
return navigationBar.isDisplayed();
} catch (NoSuchElementException e) {
return false;
}
}
}Reusing page objects across driver instances
Page objects must not be reused across driver sessions. The decorator proxy captures the driver reference at construction time. If you create a new session mid-test (rare, but happens in app reinstall scenarios), construct a new page object:
// Reinstall app
DriverManager.quitDriver();
DriverManager.initDriver("Android");
// Must create a NEW LoginPage — the old one still references the dead session
LoginPage freshLogin = new LoginPage(DriverManager.getDriver());Verifying page load with PageFactory
A common pattern is to assert that a page has loaded after navigation by checking a landmark element:
public class HomePage extends BasePage {
@AndroidFindBy(accessibility = "homeTitle")
@iOSXCUITFindBy(accessibility = "homeTitle")
private WebElement homeTitle;
public HomePage(AppiumDriver driver) {
super(driver);
verifyLoaded();
}
private void verifyLoaded() {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until(ExpectedConditions.visibilityOf(homeTitle));
}
}Calling verifyLoaded() in the constructor means the page object throws if the expected screen isn't visible within the timeout. Tests get fast, clear failure messages ("Home screen did not appear within 15s") instead of cryptic NoSuchElementException from the wrong screen.
Limitations
PageFactory doesn't handle elements inside dynamic containers (RecyclerView rows, collection view cells) well — the proxy can't parameterise the locator by item index. For list items, use direct findElement(By) calls:
public WebElement getProductAt(int index) {
return driver.findElement(AppiumBy.androidUIAutomator(
"new UiSelector().className(\"android.widget.FrameLayout\").instance(" + index + ")"
));
}