Skip to content

Platform support

Android is the fullest surface. iOS reaches parity for the loop that matters — inspect, act, assert — and where it cannot, it says so: a named error and an exit code, never a command that quietly does nothing.

This page is the canonical matrix. Other pages link here rather than restating it.

Meaning
✅ Works.
⚠️ Works, with a caveat that changes how you write the test.
⊘ Accepted and exits 0, but does nothing, or answers with a placeholder. The intent is already satisfied, or there is nothing to report.
❌ Refused with a named reason and a non-zero exit — 3 for an environment/capability gap, 2 for a usage error. Never a silent no-op.

A ⊘ is only used where the intent is already true (a simulator never sleeps, so stay-awake has nothing to do). Everything else refuses rather than reporting a success that did not happen.

The two Android columns differ on exactly one row: a phone and an emulator run the same code. What varies on Android is the device, and three differences bite in practice:

  • OEM skins silently ignore settings put / svc / cmd. Every device-state write is therefore verified by reading it back.
  • Effective font scale depends on the API level. font-scale=1.3 lands at 1.30 on API 31 and about 1.26 on API 34, which scales non-linearly. Assert that a scale grew and was restored, never that it equals a literal.
  • Some skins break individual commands. Known cases are noted beside the command below.
CommandAndroidiOS
PhysicalEmulatorSimulatorPhysical
Inspect
ui / dump✅✅⚠️ --tree renders flat⚠️ --tree renders flat
find✅✅✅✅
assert✅✅✅✅
wait✅✅✅✅
current✅✅⊘ always (unknown)⊘ always (unknown)
log / logs✅ logcat✅✅ via log show❌ 3
Act
tap / click✅✅✅✅
text✅✅⚠️ --clear unreliable⚠️ --clear unreliable
type✅✅✅✅
key✅✅⚠️ different key set⚠️ different key set
back✅✅❌ 2 — no hardware Back❌ 2 — no hardware Back
home / enter✅✅✅✅
swipe / scroll✅✅⚠️ --duration ignored⚠️ --duration ignored
screenshot / shot✅✅✅✅
launch / open✅✅⚠️ --clear exits 3⚠️ --clear exits 3
stop✅✅✅✅
clear✅ pm clear✅❌ 3 — no per-app reset❌ 3 — no per-app reset
install✅ .apk — replaces a differently-signed build, allows a downgrade✅ .apk — replaces a differently-signed build, allows a downgrade⚠️ .ipa or .app — no replace⚠️ .ipa or .app — no replace
Device state
device set✅ all eight keys✅ all eight keys⚠️ four of eight❌ 3 — none
device get✅✅⚠️ four of eight⊘ n/a for every key
device reset✅✅⚠️ restores what it could read⊘ nothing was captured
device prep⚠️ needs an explicit --device✅⊘ every knob is a no-op or unsupported⊘ same
device caps✅✅✅⚠️ reports the simulator table
device release✅✅✅✅
Run a test
batch✅✅✅✅
ai✅✅⚠️ --reset-app force-stops only⚠️ --reset-app force-stops only
suite✅✅⚠️ --app force-stops only⚠️ --app force-stops only
suite --devices / --servers (parallel)⚠️ all prefers emulators; name a serial to pool a phone✅⚠️ all prefers simulators; name a UDID to pool a phone✅
server --devices (device pool)⚠️ all prefers emulators; name a serial to pool a phone✅⚠️ all prefers simulators; name a UDID to pool a phone✅
run (start/status/archive/clear)✅✅✅⚠️ archive carries no device log
Environment
devices✅✅✅✅
devices start / stop / restart❌ 2 — never power-cycled✅ via emulator / adb emu kill✅ via simctl❌ 2 — never power-cycled
devices start|restart --wipe❌ 2✅ -wipe-data✅ simctl erase❌ 2
doctor✅✅⚠️ --fix is Android-only⚠️ --fix is Android-only
companion✅✅❌ Android-only❌ Android-only
server✅✅✅✅
help / version✅✅✅✅

Notes on the rows that carry a caveat:

  • --tree renders flat on iOS. idb’s accessibility list carries no nesting depth. ui and find are unaffected; only the indentation is lost.
  • text --clear is Android in practice. It sizes the deletion from the element’s text, and on iOS that is the accessibility label, not the field’s contents, so the deletion is the wrong length. There is also no way to read back what was typed.
  • key covers a different set per platform. Android-only: back, menu, search, center, app_switch / recents, volume_up, volume_down, mute. iOS-only: lock, side_button, siri, apple_pay. An unknown key exits 2 listing what is available. Reach the back control by label instead of by key: that is portable, and on Android it also avoids the soft keyboard swallowing the press.
  • swipe --duration is ignored on iOS. idb paces a swipe in pixels per step, so verikun passes coordinates only. Auto-scroll still works; it cannot pace the gesture there.
  • suite --app does not reset app data on iOS. It degrades to a force-stop, so a test that depends on starting logged-out does not hold there — see Suites.
  • install replaces a differently-signed build on Android only. Android refuses to update a package across signing keys, which is routine on a shared device. verikun removes the installed build and installs again, warning on stderr that its app data is gone; a same-key install keeps its data. On iOS the install simply fails.
  • install allows a version downgrade on Android when the build is debuggable. verikun always installs with adb install -d; Android only honors that flag for a debuggable build (the common CI/test case), so a release-signed build still fails with INSTALL_FAILED_VERSION_DOWNGRADE.
  • A device pool is one platform. vk server --devices serves one platform per server, and vk suite --servers a,b exits 2 when the servers report different ones. Run a suite on both platforms by running it twice — see Suites.
  • doctor --fix is Android-only and an alias for device prep, so on a physical device it needs the serial named with --device. vk doctor --ios still checks the toolchain.
  • device prep needs an explicit --device on a physical phone. Naming the serial is the opt-in. An emulator is auto-selected, as it is for devices start|stop|restart, which likewise never power-cycles a physical device. On Android 9 the dnd knob is not scriptable and prep fails on it (#103).
  • companion is Android-only and exits 3 on iOS. It is on by default (VERIKUN_COMPANION=0 opts out) and makes the hierarchy read roughly ten times faster; iOS reads are already fast. On HyperOS (Android 15) it currently fails to start and every read silently takes the slow path (#87). See The Android companion.
ModifierAndroidiOS
PhysicalEmulatorSimulatorPhysical
--enabled / --not-enabled✅✅✅✅
--checked / --not-checked✅✅✅ derived✅ derived
--selected / --not-selected✅✅❌ 3❌ 3
--focused / --not-focused✅✅❌ 3❌ 3

idb reports no selected or focused state at all, so a filter on either could only ever match nothing; verikun refuses it with exit 3 instead of burning the wait window and reporting “no element matched”. checked is derived from the element type and value.

Which kind of selector to reach for is the same on both platforms: @id first, text: second, desc: never. See Selectors for what each kind maps to per platform.

This is the canonical per-key matrix. Device state covers how the snapshot-and-restore works and what each value domain accepts.

KeyAndroidiOS
PhysicalEmulatorSimulatorPhysical
animations✅✅❌ 3 — nothing disables UIKit animation❌ 3
airplane⚠️ refused over wireless adb✅❌ 3 — no radio❌ 3
dark✅✅✅❌ 3
font-scale✅✅⚠️ nearest Dynamic Type category❌ 3
rotation✅✅❌ 3 — nothing rotates it❌ 3
stay-awake✅✅⊘ no-op — simulators do not sleep❌ 3
screen-timeout✅✅⊘ no-op — simulators do not sleep❌ 3
dnd✅✅❌ 3 — Focus is not scriptable❌ 3
doze✅✅⊘ no-op — no Doze equivalent❌ 3
  • A physical iOS device supports none of them. simctl drives simulators only and idb covers interaction, not preferences. Each key refuses with the manual equivalent named, so vk device prep is Android-only in practice.
  • vk device caps --ios reports the simulator answer either way. The capability table is static, so on a physical device caps says dark is supported and set exits 3. Trust this page, or set, over caps there.
  • font-scale on iOS maps to the nearest Dynamic Type category. The category applied is printed to stderr; 1.3 can land at an effective ratio near 1.35.

An unsupported key exits 3 before any device I/O. For vk ai and vk suite it is caught when the plan is validated, so a suite asking for rotation on iOS fails before the first tap rather than half-way through a half-modified device.

FeatureAndroidiOS
PhysicalEmulatorSimulatorPhysical
Auto-wait on selectors✅✅✅✅
Modal-barrier settle on reads✅✅⊘ not needed⊘ not needed
Auto-scroll into view✅✅⚠️ orientation-blind⚠️ orientation-blind
offscreen marker⚠️ rarely fires⚠️ rarely fires✅✅
Screenshot downscaling✅✅✅✅
JUnit + HTML reports✅✅✅✅
Device log in the archive✅✅✅❌ log capture refuses
Password redaction✅✅❌ flag never set❌ flag never set
Failure screenshot + hierarchy✅✅✅✅
Failure screenshot over --server❌ hierarchy only❌ hierarchy only❌ hierarchy only❌ hierarchy only
Device claims (auto-pick a free device)✅✅✅✅
vk server failover — unreachable device✅✅✅✅
vk server failover — device cannot serve an install✅✅❌ probe only❌ probe only
vk server pool degrade / rejoin sweep✅✅✅✅
vk server adb-server recycle✅ macOS host only✅ macOS host only⊘ no adb⊘ no adb
vk server --log-file✅✅✅✅
  • The adb-server recycle needs a macOS host. A long-running adb server leaks USB handles until devices drop mid-run, and the server restarts it while idle. Detecting it reads the macOS kernel’s guard-violation log, so on a Linux host the server never recycles and vk doctor stays quiet. iOS has no adb to recycle. See Remote devices & CI.
  • Failover on iOS moves only for an unreachable device. Telling “this device cannot take the build” from “this build is broken” relies on adb’s INSTALL_FAILED_* vocabulary, which idb does not share, so a full simulator does not trigger a move: the install fails. See When the bound device fails.
  • Device claims are host-side and identical everywhere. Over --server the claim is held by the server process on the host where the devices are. See Device claims.
  • The modal-barrier settle is Android-only. Its dumper skips a sheet’s contents until they are on screen; iOS has them in the first read. See Auto-wait.
  • offscreen is mostly an iOS signal. Android’s dumper drops nodes it considers invisible and clips the rest to the display, so a fully off-screen element is usually not in the tree at all. Do not write an Android test that expects offscreen to fire.
  • Auto-scroll is orientation-blind on iOS. idb gives no orientation signal, so the viewport is treated as a square of the longest edge: exact along the axis a list scrolls, permissive across it.
  • Password redaction does not fire on iOS. Redaction keys off the element’s password flag, and idb reports a Flutter obscureText field as plain text, so the typed value lands in the report unredacted (#44). Do not rely on redaction cross-platform — see Reports & test runs.
Android iOS
Required adb (platform-tools) xcrun and idb and idb_companion
Install Android SDK platform-tools brew install idb-companion + pip install fb-idb
Override the binary path ADB IDB
Extra for a physical device USB debugging Developer mode, plus a reachable idb_companion
Check it vk doctor vk doctor --ios

idb is required to drive iOS at all, simulator or not. simctl covers screenshots, launch, stop and logs on a simulator; the hierarchy and every interaction come from idb. Full setup: iOS setup.

The Android columns and the iOS simulator column are measured, by running the built CLI against the repository’s Flutter fixture app on real hardware and simulators. The iOS physical device column is read from the source rather than measured, so treat it as reliable for what is refused and report anything that disagrees. The measured findings, with the hardware each was observed on, live in example/flutter-app/README.md.