agentleFS
KushpawanKushpawan/kmp-mobile-skills/skills/mobile-platform-parity/SKILL.md

mobile-platform-parity

Skill15 starsChanged 23 days agoApache-2.0View on GitHub

Use when a change touches only one platform of a Kotlin Multiplatform app — androidMain, iosMain, Swift or SwiftUI, an Android or iOS host app — or when Android and iOS behave differently - a bug fixed on one side only, different validation, error, empty or loading states, copy, permission prompts, background or offline behaviour, analytics events, deep links, date, number or time-zone formatting, accessibility ids, feature flags, or two expect/actual implementations that disagree.

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Before releasing a feature on both platforms, or when QA reports "it works differently on iOS".

    line 26
  2. Distinctive:

    No counterpart is itself a result: either the feature is intentionally one-platform (document it) or the other platform is missing it (a …

    line 95
  3. Very specific:

    Read both sides in full, then walk the rows in references/parity-dimensions.md that apply.

    line 100
  4. Very specific:

    When both sides implement the same user-observable rule — a validation, a threshold, an eligibility check, a formatting choice — the …

    line 106
  5. Very specific:

    Ask which to fix. In order of preference: move the rule to commonMain.

    line 159
  6. Very specific:

    When the project's strings are shared through Compose resources, or events are fired only from shared view models, those two checks are NOT …

    line 204

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Mobile platform parity

Find places where Android and iOS do different things for the same user action. A code review reads one platform at a time, and a build proves only that each side compiles; neither notices that the two sides disagree. This review holds both sides in context at once.

The principle: shared code removes parity risk, and every platform-specific line reintroduces it. A rule fixed in commonMain cannot drift. A rule written twice will.

When to use

  • A change edits androidMain, iosMain (or appleMain, nativeMain), Swift, SwiftUI, or an Android or iOS host app, and not the other platform.
  • A bug was reported on one platform: check whether the other has it, and whether the fix belongs in shared code.
  • Before releasing a feature on both platforms, or when QA reports "it works differently on iOS".
  • An expect declaration or its actuals changed.

When not to use

  • A change confined to commonMain with no platform-specific behaviour behind it — shared code cannot diverge by itself. (Still check actuals that the changed common code now calls differently.)
  • General correctness review — use kmp-code-review.
  • Security controls in depth — use mobile-security-review; this skill only flags a control present on one platform and missing on the other.

Step 0 — Read the project first

Map how this project splits work between platforms before comparing anything:

  1. Architecture. Shared Compose Multiplatform UI, native SwiftUI over shared view models, or separate native apps sharing only a data layer. Parity risk grows in that order.
  2. Source sets. commonMain, androidMain, iosMain, appleMain, nativeMain, jvmMain, and where the iOS host app and any Swift packages live.
  3. Where each concern lives on each side: strings (composeResources, res/values, Localizable.strings / .xcstrings), analytics wrapper, feature-flag client, permission handling, deep link configuration (intent filters and assetlinks.json; associated domains and apple-app-site-association), date and number formatting helpers.
  4. Documented divergences. The project's rules files (AGENTS.md, CLAUDE.md, CONTRIBUTING.md, .cursor/rules/*, .github/copilot-instructions.md), ADRs, or a parity log. A divergence recorded there is not a finding.

Done when you can say, for the feature in question, which parts are shared and which are written per platform.

Where shared code removes risk — and where platform code brings it back

ArchitectureParity comes free forRisk remains in
Shared Compose UI + shared view modelsLayout, copy, states, validation, analytics fired from view modelsexpect/actual code, insets and keyboard handling, back navigation, permission prompts, system pickers and share sheets, platform formatters
SwiftUI over shared view modelsBusiness rules, state machines, validation and analytics that live in the view modelSwift re-implementing a rule, Swift-side formatting, copy duplicated in two string files, accessibility ids, analytics fired from views
Native apps over a shared data layerNetworking and persistenceEverything the user sees — treat every screen as two implementations

Workflow

1. Classify the changed files

git diff --name-only "$(git merge-base origin/main HEAD)" HEAD; git diff --name-only HEAD   # use the project's base branch

Bucket every file: Android (androidMain, android*Main, the Android app module, res/, AndroidManifest.xml), iOS (iosMain, ios*Main, appleMain, nativeMain, *.swift, Info.plist, *.xcstrings), shared (commonMain, composeResources). A change with files in only one platform bucket is the case this skill exists for.

Done when each platform-specific change has a one-line description of the behaviour it adds or alters.

2. Find the counterpart

For each platform-specific change, locate the code that does the same job on the other platform:

Changed on one sideCounterpart on the other
An actualEvery other actual of the same expect — grep for actual fun Name, actual class Name, actual object Name, actual val Name
A Compose screen (Android-only UI)The SwiftUI view for the same route — search by screen name, route, test identifiers or string keys
A Swift view or view modelThe Compose screen and the shared view model it should be using
Android host (Application, MainActivity, manifest)iOS host (@main App, AppDelegate, SceneDelegate, Info.plist, entitlements)
A string keyThe same key in the other platform's string files
An analytics callThe same event name on the other side
A deep link routeThe other platform's link configuration and router

No counterpart is itself a result: either the feature is intentionally one-platform (document it) or the other platform is missing it (a finding).

3. Compare along the dimensions

Read both sides in full, then walk the rows in references/parity-dimensions.md that apply. For each row, write down the behaviour on each side with file:line evidence. "Probably the same" is not evidence.

4. Ask whether the rule belongs in shared code

When both sides implement the same user-observable rule — a validation, a threshold, an eligibility check, a formatting choice — the recommended fix is to move it into commonMain and have both platforms call it, not to patch the second copy. Note it in the finding's fix.

5. Check for documented intent

A divergence is acceptable only when it is written down:

  • forced by the OS (iOS cannot re-prompt for a denied permission; Android has a system back button), or a product decision, and
  • recorded where the next reader will find it: a comment at both code sites naming the reason, an entry in the project's parity log or ADRs, or — for a fix landing on one platform first — a follow-up issue named in the pull request.
// Platform difference (intentional): iOS cannot show the system prompt again after a denial,
// so iOS opens Settings here. Android re-requests. Same outcome: an explanation, then a path to grant.

Undocumented divergence is a finding even when it is defensible.

6. Rate and try to disprove

Severity no lower than the row's floor. For every Critical and High: Is the difference forced by the OS? Is the behaviour already supplied by shared code that one side calls indirectly? Is the feature absent on the other platform by design? Is the counterpart in a file you have not read yet? Mark survivors CONFIRMED; downgrade or drop the rest.

7. Report

Emit one JSON object per line, then the parity matrix and the findings, most severe first.

{"file":"shared/src/iosMain/kotlin/com/example/app/profile/AvatarUploader.ios.kt","line":27,"severity":"High","tier":2,"lens":"parity/semanticActualDivergence","title":"iOS upload timeout is 60 s, Android 15 s","why":"the iOS actual relies on the URLSession default while the Android actual sets 15 s; a slow network shows an error on Android and a spinner for a minute on iOS","fix":"name UPLOAD_TIMEOUT_MS in commonMain and pass it to both actuals","rule":"Semantic actual divergence","counterpart":"shared/src/androidMain/kotlin/com/example/app/profile/AvatarUploader.android.kt:31","verdict":"CONFIRMED"}

Fields: file, line, severity (Critical · High · Medium · Low), tier (1 project rule · 2 dimension row), lens (parity/<lensName>), title, why, fix, rule, counterpart (file:line on the other platform, or "none" when it is missing), verdict (CONFIRMED · PLAUSIBLE; every Critical and High is CONFIRMED). Finish with reviewed: <n> files · <m> findings.

Then the matrix, one row per dimension you compared:

DimensionAndroidiOSSame?Evidence
Upload timeout15 s60 s (URLSession default)No — F1AvatarUploader.android.kt:31, AvatarUploader.ios.kt:27
Denied-permission branchRe-request with rationaleOpens SettingsYes — documented, OS-forcedcomments at both sites

"No divergence found" is a valid result; the matrix shows what was compared.

8. Offer fixes

Ask which to fix. In order of preference: move the rule to commonMain; mirror the fix on the other platform in the same change; or open a follow-up issue with the exact divergence, both file paths and the expected behaviour. Never "fix" a documented, OS-forced difference.

Quick reference

DimensionTypical divergence
expect/actual semanticsTimeout, error type, null vs empty, result thread, units (ms vs s)
ValidationRegex, limits, trimming, when it runs
Error / empty / loadingMessage mapping, retry offered, blocking vs non-blocking
CopyMissing keys, plurals, placeholders, languages
PermissionsOrder, priming, denied and permanently-denied paths, precise vs approximate
Background and lifecycleRestoration, background work timing, suspended timers
OfflineQueue vs fail, cache policy, connectivity semantics
AnalyticsEvent names, property keys and types, firing point, user id reset
AccessibilityIdentifier and label coverage, text scaling, touch targets
Deep linksPaths, parameter validation, signed-out and cold-start handling
FormattingTime zone, 12/24 h, separators, currency, DST arithmetic
Feature flagsKey, default on fetch failure, evaluation time

Verify

Report each check as PASS, FAIL or NOT RUN — <reason>. Never claim one that was not run.

# Both platforms still build and the shared tests pass on both targets
./gradlew :composeApp:assembleDebug
./gradlew :shared:allTests                         # includes iOS simulator tests on macOS
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' build

t=$(mktemp -d)

# String keys present on one side only (adjust paths to the project's string files)
grep -ho 'name="[^"]*"' composeApp/src/androidMain/res/values/strings.xml | cut -d'"' -f2 | sort -u > "$t/keys-android"
grep -ho '^"[^"]*"' iosApp/iosApp/en.lproj/Localizable.strings | tr -d '"' | sort -u > "$t/keys-ios"
comm -3 "$t/keys-android" "$t/keys-ios"

# Analytics event names fired on one side only (replace track( with the project's analytics call)
grep -rhoE 'track\("[A-Za-z0-9_]+"' --include=*.kt . | sort -u > "$t/events-kotlin"
grep -rhoE 'track\("[A-Za-z0-9_]+"' --include=*.swift . | sort -u > "$t/events-swift"
comm -3 "$t/events-kotlin" "$t/events-swift"

When the project's strings are shared through Compose resources, or events are fired only from shared view models, those two checks are NOT RUN — shared by construction; say so. For a behaviour that cannot be read from source (timing, permissions), run the same scenario on an emulator and a simulator and record both outcomes.

Common mistakes

MistakeWhy it bitesFix
Comparing signatures, not behaviourTwo actuals that compile can still disagree on timeouts, errors and threadsRead both bodies; compare defaults the platform libraries supply
Patching the second copy of a ruleThe copies drift again on the next changeMove the rule into commonMain
Flagging OS-forced differencesNoise; the team stops reading the reportCheck the platform constraint, then require only documentation
Accepting an undocumented "intentional" differenceThe next developer "fixes" it, or copies it somewhere it is wrongComment at both sites, or an entry in the parity log
Skipping review because the diff is one-platformThat is exactly when divergence is introducedAlways find and read the counterpart
Trusting string-file parity when copy is built in codeHardcoded or concatenated strings bypass the key comparisonGrep the screens for literals too
Comparing in the simulator onlyPermissions, background limits and time zones behave differently on devicesRun timing and permission scenarios on real devices when they matter

Related skills

  • kmp-code-review — general review; its findings schema is the one used here.
  • mobile-security-review — when the divergent behaviour is a security control.
  • kmp-expect-actual — restructuring an expect/actual pair so behaviour is defined once.
  • compose-multiplatform-localization — moving strings into shared resources.
  • compose-multiplatform-navigation — shared routes and deep links.
  • External: android/skills, twostraws/SwiftUI-Agent-Skill.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.