# Engineering Mobile App Builder

> Build native and cross-platform mobile applications for iOS and Android with optimized performance and platform integration. Use when you need SwiftUI or Jetpack Compose development, React Native or Flutter cross-platform apps, offline-first architecture, biometric authentication, push notifications, deep linking, app startup optimization, or mobile-specific UX patterns and gesture handling.

- **Type:** Skill
- **Install:** `agentstack add skill-peterhdd-agent-skills-engineering-mobile-app-builder`
- **Verified:** Pending review
- **Seller:** [PeterHdd](https://agentstack.voostack.com/s/peterhdd)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [PeterHdd](https://github.com/PeterHdd)
- **Source:** https://github.com/PeterHdd/agent-skills/tree/main/skills/engineering-mobile-app-builder

## Install

```sh
agentstack add skill-peterhdd-agent-skills-engineering-mobile-app-builder
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Mobile Development Guide

## Overview
This guide covers native iOS/Android development (SwiftUI, Jetpack Compose) and cross-platform frameworks (React Native, Flutter) with patterns for offline-first architecture, platform integrations, and performance optimization. Use it when choosing a platform strategy, building mobile UI, or integrating device capabilities.

## Platform Selection Guide

- Use **native** (SwiftUI/Compose) when the app requires deep platform integration (widgets, extensions, AR, custom camera pipelines).
- Use **React Native or Flutter** when shipping to both platforms with a small team and the app is primarily data display and forms.
- For iOS, use SwiftUI with `@Observable` (iOS 17+) or `@StateObject`/`@ObservedObject` (iOS 15+); fall back to UIKit only for unsupported features.
- For Android, use Jetpack Compose with Hilt for DI and `StateFlow` for reactive state; avoid XML layouts in new screens.
- For navigation, use `NavigationStack` (iOS) or Navigation Compose (Android) with typed routes.

## Performance and UX Rules

- Cold start must be under 2 seconds on mid-range devices; defer initialization not needed for first frame. If cold start exceeds 1.5s, profile with Instruments (iOS) or `reportFullyDrawn()` (Android) and move heavy work to background.
- Maintain 60fps scrolling on devices two generations behind current. If frame drops occur: on iOS, check for off-main-thread image decoding and use `prefetchDataSource`; on Android, enable `compositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed` and check for unnecessary recomposition with Layout Inspector.
- Animations: use SwiftUI `.animation()` / Compose `animateFloatAsState`; keep durations 200-350ms. Never animate layout properties (frame size, padding) — animate opacity and offset only.
- Offline-first: local database (Core Data, Room, SQLite/Drift) as single source of truth, background server sync. If the app has >3 entity types with relationships, use Core Data (iOS) or Room (Android) — never raw SQLite.
- Profile battery with Xcode Energy Diagnostics or Android Battery Historian; fix any operation keeping CPU awake >2s without user interaction.
- Batch network requests; use background sessions (URLSession / WorkManager) for large transfers. If payload >1MB, use background transfer; if >10MB, add resumable upload support.
- Incremental sync with timestamps or change tokens instead of full-collection fetches. If collection has >500 items, paginate sync with cursor; never fetch all.
- Lazy-load images with caching (Kingfisher/SDWebImage on iOS, Coil on Android); downscale to display size. If list shows >20 images, implement memory cache cap (50MB iOS, 100MB Android) and disk cache cap (200MB).
- App binary size: target 3 permissions needed, request them progressively as features are used — never batch-request on launch.

## Code Examples

See [SwiftUI Guide](references/swiftui.md) for a full product list with NavigationStack, search, pagination, and pull-to-refresh.

See [Jetpack Compose Guide](references/compose.md) for a full product list with Hilt, StateFlow, LazyColumn, and debounced search.

See [React Native Guide](references/react-native.md) for a full product list with FlatList, react-query infinite scrolling, and platform styling.

See [Flutter Guide](references/flutter.md) for a full product list with Riverpod, ListView.builder, debounced search, pagination, and pull-to-refresh.

See [Offline-First Architecture](references/offline-first.md) for offline queue systems (NetInfo + AsyncStorage, Core Data + CloudKit, Room + WorkManager), conflict resolution (LWW, field-level merge, CRDT), and optimistic UI with rollback.

See [Performance Patterns](references/performance.md) for Hermes engine config, FlatList optimization (getItemLayout, windowSize), SwiftUI lazy stacks and image caching, Compose recomposition control with Coil, battery optimization, memory leak prevention, and cold start optimization.

See [State Management](references/state-management.md) for Zustand with MMKV persistence, TanStack Query optimistic mutations, SwiftUI @Observable with environment injection, Compose ViewModel + StateFlow + SavedStateHandle, navigation deep linking, auth state machines, and form validation.

See [Native APIs](references/native-apis.md) for push notifications (APNs, FCM, react-native-firebase), camera/photo (CameraX, AVFoundation, vision-camera), biometric auth (FaceID/TouchID, BiometricPrompt), background tasks (BGTaskScheduler, WorkManager), and non-transactional entitlement architecture notes for app-store subscriptions.

## Platform-Specific Gotchas

### iOS Background Task Limits
- `BGAppRefreshTask`: System grants ~30 seconds of execution. If the task does not call `setTaskCompleted` within that window, the system kills it and deprioritizes future requests.
- `BGProcessingTask`: Up to 3 minutes, but only runs when device is charging and on Wi-Fi. Do not rely on this for time-sensitive sync.
- After a push notification wakes the app (`content-available: 1`), you get ~30 seconds. If the work takes longer, start a `URLSession` background download instead.
- The system learns user behavior. If the user never opens your app at 8am, your 8am background refresh will not run. Design for missed refreshes — always do a full catch-up sync on foreground.

### Android Background Restrictions
- **Doze mode** (Android 6+): After screen off + stationary, network access is batched into maintenance windows (~every 15 min, increasing to ~1 hour). `WorkManager` with `NetworkType.CONNECTED` constraint will defer until the next window. For urgent messages, use FCM high-priority messages (limited to ~10/day before throttled).
- **App Standby Buckets** (Android 9+): Apps are ranked Active → Working Set → Frequent → Rare → Restricted. Rare/Restricted apps get severely limited jobs and alarms. If your app is in Restricted bucket, `WorkManager` periodic work may run only once per 24 hours.
- **Exact alarms** (Android 12+): `setExactAndAllowWhileIdle()` requires `SCHEDULE_EXACT_ALARM` permission. Users can revoke it in Settings. Always check `canScheduleExactAlarms()` before scheduling and fall back to inexact.
- **Foreground service types** (Android 14+): Must declare `foregroundServiceType` in manifest. Types: `camera`, `location`, `mediaPlayback`, `dataSync`, etc. Using wrong type → crash. `dataSync` type is limited to 6 hours.

### Flutter-Specific Gotchas
- **`const` constructors**: Always use `const` for stateless widgets and unchanging widget subtrees. Without `const`, Flutter rebuilds the entire subtree on parent rebuild. This is the #1 Flutter performance issue.
- **`Keys`**: Use `ValueKey` on list items when the list can reorder, insert, or delete. Without keys, Flutter reuses state incorrectly — user types in TextField A, deletes item, and the text appears in item B.
- **Platform channels**: Calls between Dart and native (iOS/Android) are async and serialized. If you call a platform channel in a tight loop (>100 calls/sec), batch the data into a single call. Use `BasicMessageChannel` with binary codec for large payloads.
- **Isolates**: Dart is single-threaded. For CPU work >16ms (JSON parsing large responses, image processing), use `Isolate.run()` (Dart 2.19+) or `compute()`. Never parse >100KB JSON on the main isolate.
- **Image caching**: Flutter's default `Image` widget caches decoded images in memory with no limit. For lists with >50 images, use `cached_network_image` with `memCacheHeight`/`memCacheWidth` to downscale, or the memory will grow unbounded.
- **State restoration**: On Android, the system kills background apps aggressively. Use `RestorationMixin` or persist critical state to disk. If the user fills a 10-field form, switches to another app, and comes back to a blank form — that is a bug.

### Compose-Specific Gotchas
- **Recomposition**: Any lambda that captures a mutable value triggers recomposition of its parent. Pass `remember`-ed lambdas or use `derivedStateOf` for expensive computations. Use Layout Inspector > "Show Recomposition Counts" to find hot spots.
- **Stability**: Compose skips recomposition for `@Stable` or `@Immutable` types. If your data class uses `List` (unstable), Compose recomposes every time. Use `kotlinx.collections.immutable.ImmutableList` or annotate with `@Immutable` if you guarantee immutability.
- **`LazyColumn` item keys**: Always provide `key` in `items(key = { it.id })`. Without keys, scroll position breaks on list mutations and animations fail.

## Offline Sync Decision Rules

### Conflict Resolution Strategy
- **Last-Write-Wins (LWW)**: Use when data is user-owned and rarely edited concurrently (user profile, settings, personal notes). Simple: compare timestamps, latest wins. Risk: silent data loss if two devices edit simultaneously.
- **Field-level merge**: Use when different fields of the same record may be edited on different devices (e.g., user edits `name` on phone, `email` on tablet). Merge non-conflicting field changes, flag conflicting fields for manual resolution. Requires tracking per-field timestamps.
- **Operational Transform / CRDT**: Use only for real-time collaborative editing (shared documents, collaborative whiteboards). Complex to implement — use a library (Yjs, Automerge). Never build custom OT/CRDT.
- **Queue-and-retry**: Use for write-only operations (form submissions, analytics events, chat messages). Queue locally, send when online, retry with idempotency keys. No conflict possible because each operation is independent.

### Sync Architecture Decision Rules
- If the app has 1% at any stage, pause and investigate.

## Anti-Patterns

- Never use `@ObservedObject` for state owned by the current view — use `@StateObject` (iOS 15) or `@State` with `@Observable` (iOS 17+). `@ObservedObject` is for injected dependencies only.
- Never use `mutableStateOf` outside a ViewModel in Compose — all mutable state lives in ViewModels, UI observes via `collectAsStateWithLifecycle()`.
- Never call `setState` on a disposed widget in Flutter — always check `mounted` before async state updates. In Riverpod, use `ref.onDispose()` to cancel async work.
- Never use `FlatList` without `keyExtractor` and `getItemLayout` in React Native — without these, scrolling performance degrades dramatically on lists >50 items.
- Never store navigation state in a global store — use the platform navigation stack. Global nav state causes back-button bugs and deep link failures.
- Never block the main thread with synchronous database reads — use `withContext(Dispatchers.IO)` (Android) / background actors (iOS) / `InteractionManager.runAfterInteractions` (React Native) / `Isolate.run()` (Flutter).
- Never parse large JSON (>100KB) on the main thread in Flutter — use `compute()` or `Isolate.run()`. On Android, never do network or disk I/O on the main thread — `StrictMode` will catch this in debug builds.
- Never use platform-specific code without a fallback. If a plugin is iOS-only, the Android build must not crash — it should degrade gracefully or show "not available on this platform."

## Workflow

### Step 1: Platform Strategy and Setup
- Choose native vs cross-platform: if app needs AR, custom camera, or widgets, go native. If app is data display + forms with 80% coverage.
- Set up Fastlane (iOS) or Gradle Play Publisher (Android) for automated store submission.

## Self-Verification Protocol
After completing any mobile implementation, verify before submitting for review:
- Run the app on a real device (not just simulator). Simulators hide performance, memory, and gesture issues.
- Test the core flow offline: enable airplane mode, perform the action, re-enable network, verify sync. If the app crashes or loses data offline, it is not ready.
- Profile memory on a mid-range device. If peak memory exceeds 200MB, investigate before shipping.
- Measure cold start time on the oldest supported device. If >2s, defer initialization until after first frame.
- Verify all touch targets are >=44pt (iOS) / >=48dp (Android) by tapping every interactive element with a finger, not a mouse cursor.
- Check Dark Mode on every screen. If any text is unreadable or any element is invisible, fix before merge.
- Test with Dynamic Type (iOS) / Font Scale 200% (Android). If text truncates or overlaps, fix the layout.
- Run the app with VoiceOver (iOS) / TalkBack (Android) on the core flow. Every interactive element must be announced meaningfully.
- Check that no sensitive data (tokens, passwords, PII) appears in logs. Run `adb logcat` (Android) or Console.app (iOS) during the flow.

## Failure Recovery
- **App crashes on launch**: Check crash log for the exact line. Common causes: force-unwrapped nil (Swift), uninitialized lateinit (Kotlin), missing native module (React Native). If the crash is in a third-party library, pin the previous version and file an issue.
- **Build fails after Xcode/Gradle update**: Clean derived data (`rm -rf ~/Library/Developer/Xcode/DerivedData`) or Gradle cache (`./gradlew clean`). If still failing, check release notes for breaking changes in build tools. Pin the toolchain version in the project until the issue is resolved.
- **UI renders differently on device vs simulator**: The simulator uses the Mac GPU. Test on a real device. For layout issues, check for hardcoded pixel values (use pt/dp instead) and safe area insets.
- **Performance drops after adding a feature**: Profile with Instruments (iOS) or Android Studio Profiler. Check for: main thread blocking, excessive recomposition/re-renders, large image loading without caching, or leaked subscriptions/observers.
- **Push notifications not arriving**: Verify: (1) valid APNs/FCM token, (2) correct bundle ID/package name, (3) certificate/key not expired, (4) device not in low-power mode suppressing background activity. Test with a manual push via `curl` to APNs/FCM before debugging app code.
- **App rejected by App Store / Play Store**: Read the rejection reason exactly. Common: missing privacy policy URL, incomplete App Privacy details (iOS), missing data safety form (Android), or background location usage without justification. Fix the metadata — do not guess.

## Existing App Orientation
When taking over or joining an existing mobile project:
1. **Build and run** (15 min) — Clone, install deps, build, run on a real device. If it fails, fix the build first.
2. **Identify the architecture pattern** (10 min) — MVC, MVVM, MVI, or unstructured? Check: where does state live? How does data flow from API to UI?
3. **Map the navigation graph** (10 min) — List all screens and how they connect. Check for deep link handlers. Note any navigation library in use.
4. **Check the dependency list** (5 min) — Podfile/SPM (iOS), build.gradle (Android), package.json (React Native). Flag outdated or abandoned dependencies.
5. **Run existing tests** (5 min) — Note coverage, test types, and which flows are untested.
6. **Check for platform-specific debt** (5 min) — Deprecated APIs (check Xcode warnings, Android lint), missing permissions declarations, hardcoded strings (localization readiness).
7. **Profile the app** (10 min) — Cold start time, memory usage, scroll performance on the heaviest list screen. These numbers are your baseline.

## Scripts

### `scripts/check_app_size.sh`
Analyze a mobile app build output directory for size issues. Takes a build output directory as argument, finds app binaries/bundles (.app, .apk, .aab, .ipa), reports total size, largest files, and asset breakdown by category. Warns if total exceeds common thresholds (50MB for iOS, 150MB for Android).

```bash
scripts/check_app_size.sh ./build/outputs/apk/release
scripts/check_app_size.sh --threshold-ios 40 --top-files 10 ./DerivedData/Build/Products/Release-iphoneos
```

### `scripts/check_permissions.py`
Extract and audit permissions from AndroidManifest.xml or Info.plist. Identifies requested permissions, flags potentially dangerous ones (CAMERA, LOCAT

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [PeterHdd](https://github.com/PeterHdd)
- **Source:** [PeterHdd/agent-skills](https://github.com/PeterHdd/agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-peterhdd-agent-skills-engineering-mobile-app-builder
- Seller: https://agentstack.voostack.com/s/peterhdd
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
