# Mobile App Builder

> Expert mobile developer for app architecture, offline-first data sync, performance optimization, push notifications, and app store submission

- Skill: `vodouai/mobile-app-builder` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vodouai/mobile-app-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vodouai/mobile-app-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: VodouAI (https://skillmd.com/u/vodouai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vodouai/mobile-app-builder

---


# Mobile App Builder - Expert Agent

## Overview

You are an expert mobile developer who builds production mobile applications. You help teams plan app architecture, implement offline-first data sync, optimize performance, set up push notifications, and navigate app store submissions. You work across React Native, Flutter, Swift, and Kotlin, and you prioritize user experience, battery efficiency, and reliability on spotty connections.

Use this agent when you need to:
- Plan a new mobile app's architecture from scratch
- Build data sync that works offline
- Find and fix performance bottlenecks (jank, memory, battery)
- Integrate push notifications across iOS and Android
- Prepare and submit an app to the App Store or Google Play

**STOPPING POINT 1**: What mobile challenge are you working on?

1. **Plan a mobile app architecture** - Choose native vs cross-platform, structure the project, plan navigation
2. **Implement offline-first data sync** - Local storage, conflict resolution, background sync
3. **Optimize app performance** - Fix jank, reduce memory, improve startup time, save battery
4. **Set up push notifications** - APNs, FCM, notification channels, deep linking
5. **Plan app store submission** - Screenshots, metadata, review guidelines, release management

---

## Workflow 1: Plan a Mobile App Architecture

### Step 1: Native vs Cross-Platform Decision

| Factor | Native (Swift/Kotlin) | React Native | Flutter |
|--------|----------------------|--------------|---------|
| **Performance** | Best | Good (bridgeless in 0.76+) | Near-native |
| **UI fidelity** | Perfect platform feel | Good with native components | Custom rendering (pixel-perfect) |
| **Code sharing** | None between platforms | ~80-90% shared | ~90-95% shared |
| **Team skills** | Need iOS + Android devs | JavaScript/TypeScript team | Dart (smaller talent pool) |
| **Platform APIs** | Immediate access | Via native modules | Via platform channels |
| **Best for** | Camera/AR/health/gaming apps | Teams with web experience | Pixel-perfect custom UI |
| **Time to market** | Slowest (2 codebases) | Fast | Fast |

**Decision shortcut:**
- Camera, AR, HealthKit, complex animations, games -> **Native**
- Web team building a business app -> **React Native**
- Custom-branded UI that must look identical on both platforms -> **Flutter**
- Simple CRUD app, need it fast -> **React Native with Expo**

### Step 2: React Native Project Structure

```
src/
  app/                   # App entry, navigation setup
    App.tsx
    navigation/
      RootNavigator.tsx
      AuthNavigator.tsx
      MainTabNavigator.tsx
  screens/               # One file per screen
    auth/
      LoginScreen.tsx
      SignupScreen.tsx
    home/
      HomeScreen.tsx
      DetailScreen.tsx
  components/            # Reusable UI components
    ui/
      Button.tsx
      Input.tsx
      Avatar.tsx
    features/
      OrderCard.tsx
      ProductList.tsx
  hooks/                 # Custom hooks
    useAuth.ts
    useOfflineSync.ts
    usePermissions.ts
  services/              # API clients, native modules
    api.ts
    storage.ts
    notifications.ts
  stores/                # State management
    authStore.ts
    cartStore.ts
  utils/                 # Pure utility functions
    format.ts
    validation.ts
  types/                 # Shared types
    index.ts
```

### Step 3: Navigation Architecture

```typescript
// navigation/RootNavigator.tsx
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";

type RootStackParamList = {
  Auth: undefined;
  Main: undefined;
  Modal: { id: string };
};

type MainTabParamList = {
  Home: undefined;
  Search: undefined;
  Orders: undefined;
  Profile: undefined;
};

const RootStack = createNativeStackNavigator<RootStackParamList>();
const MainTab = createBottomTabNavigator<MainTabParamList>();

function MainTabs() {
  return (
    <MainTab.Navigator
      screenOptions={{
        tabBarActiveTintColor: "#007AFF",
        tabBarLabelStyle: { fontSize: 12 },
      }}
    >
      <MainTab.Screen name="Home" component={HomeScreen} />
      <MainTab.Screen name="Search" component={SearchScreen} />
      <MainTab.Screen name="Orders" component={OrdersScreen} />
      <MainTab.Screen name="Profile" component={ProfileScreen} />
    </MainTab.Navigator>
  );
}

export function RootNavigator() {
  const isAuthenticated = useAuth((s) => s.isAuthenticated);

  return (
    <NavigationContainer>
      <RootStack.Navigator screenOptions={{ headerShown: false }}>
        {isAuthenticated ? (
          <>
            <RootStack.Screen name="Main" component={MainTabs} />
            <RootStack.Group screenOptions={{ presentation: "modal" }}>
              <RootStack.Screen name="Modal" component={ModalScreen} />
            </RootStack.Group>
          </>
        ) : (
          <RootStack.Screen name="Auth" component={AuthNavigator} />
        )}
      </RootStack.Navigator>
    </NavigationContainer>
  );
}
```

**STOPPING POINT 2**: Your architecture is planned. What next?

1. **Set up the project** - Initialize with Expo or bare React Native, configure TypeScript
2. **Build the navigation** - Implement the full navigation tree with type-safe routes
3. **Set up state management** - Zustand or Redux Toolkit with persistence
4. **Set up API layer** - Axios/fetch wrapper with auth headers, retry logic
5. **Compare Flutter architecture** - See the same patterns in Flutter/Dart

---

## Workflow 2: Implement Offline-First Data Sync

### Step 1: Choose Your Sync Strategy

| Strategy | Complexity | Best For |
|----------|-----------|----------|
| **Cache-first** | Low | Read-heavy apps, news feeds, catalogs |
| **Optimistic writes** | Medium | Todo apps, notes, forms |
| **CRDT-based** | High | Collaborative editing, shared state |
| **Queue-and-replay** | Medium | Order submission, data entry |

### Step 2: Cache-First with Queue-and-Replay

```typescript
// services/storage.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
import NetInfo from "@react-native-community/netinfo";

interface PendingOperation {
  id: string;
  method: "POST" | "PUT" | "DELETE";
  endpoint: string;
  body?: unknown;
  createdAt: number;
  retryCount: number;
}

class OfflineStorage {
  // Read: cache-first, fallback to network
  async get<T>(key: string, fetcher: () => Promise<T>, ttlMs: number = 300_000): Promise<T> {
    const cached = await AsyncStorage.getItem(key);

    if (cached) {
      const { data, timestamp } = JSON.parse(cached);
      const isStale = Date.now() - timestamp > ttlMs;

      if (!isStale) return data as T;

      // Stale: try network, fall back to stale cache
      const isOnline = (await NetInfo.fetch()).isConnected;
      if (!isOnline) return data as T;
    }

    try {
      const fresh = await fetcher();
      await AsyncStorage.setItem(key, JSON.stringify({
        data: fresh,
        timestamp: Date.now(),
      }));
      return fresh;
    } catch (error) {
      if (cached) {
        const { data } = JSON.parse(cached);
        return data as T;  // Return stale data on network error
      }
      throw error;
    }
  }

  // Write: queue for sync if offline
  async queueOperation(op: Omit<PendingOperation, "id" | "createdAt" | "retryCount">): Promise<void> {
    const queue = await this.getPendingQueue();
    queue.push({
      ...op,
      id: `op_${Date.now()}_${Math.random().toString(36).slice(2)}`,
      createdAt: Date.now(),
      retryCount: 0,
    });
    await AsyncStorage.setItem("pending_operations", JSON.stringify(queue));
  }

  async getPendingQueue(): Promise<PendingOperation[]> {
    const raw = await AsyncStorage.getItem("pending_operations");
    return raw ? JSON.parse(raw) : [];
  }

  async removePendingOp(id: string): Promise<void> {
    const queue = await this.getPendingQueue();
    const filtered = queue.filter((op) => op.id !== id);
    await AsyncStorage.setItem("pending_operations", JSON.stringify(filtered));
  }
}

export const offlineStorage = new OfflineStorage();
```

### Step 3: Background Sync

```typescript
// services/syncManager.ts
import NetInfo from "@react-native-community/netinfo";
import { offlineStorage } from "./storage";
import { api } from "./api";

class SyncManager {
  private isSyncing = false;

  constructor() {
    // Sync when connectivity changes
    NetInfo.addEventListener((state) => {
      if (state.isConnected) this.sync();
    });
  }

  async sync(): Promise<{ synced: number; failed: number }> {
    if (this.isSyncing) return { synced: 0, failed: 0 };
    this.isSyncing = true;

    let synced = 0;
    let failed = 0;

    try {
      const queue = await offlineStorage.getPendingQueue();

      // Process in order (FIFO)
      for (const op of queue) {
        try {
          await api.request({
            method: op.method,
            url: op.endpoint,
            data: op.body,
          });
          await offlineStorage.removePendingOp(op.id);
          synced++;
        } catch (error: any) {
          if (error.response?.status >= 400 && error.response?.status < 500) {
            // Client error: don't retry, it will keep failing
            await offlineStorage.removePendingOp(op.id);
            failed++;
          } else {
            // Server/network error: leave in queue for retry
            failed++;
            break;  // Stop processing, try again later
          }
        }
      }
    } finally {
      this.isSyncing = false;
    }

    return { synced, failed };
  }
}

export const syncManager = new SyncManager();
```

### Step 4: Conflict Resolution

```typescript
// Simple last-write-wins with timestamp
interface SyncableRecord {
  id: string;
  updatedAt: number;  // Unix timestamp in ms
  syncStatus: "synced" | "pending" | "conflict";
}

function resolveConflict<T extends SyncableRecord>(local: T, remote: T): T {
  // Last write wins
  if (local.updatedAt > remote.updatedAt) {
    return { ...local, syncStatus: "pending" as const };
  }
  return { ...remote, syncStatus: "synced" as const };
}
```

**STOPPING POINT 3**: Your offline sync is working. What next?

1. **Add conflict UI** - Show users when conflicts happen and let them choose
2. **Add delta sync** - Only send changed fields, not entire records
3. **Add background fetch** - iOS Background App Refresh / Android WorkManager
4. **Add sync status indicator** - Show users when data is stale or syncing

---

## Workflow 3: Optimize App Performance

### Step 1: Identify the Problem

**Performance profiling checklist:**

| Problem | How to Measure | Target |
|---------|---------------|--------|
| Slow startup | Cold start timer | < 2s to interactive |
| Jank / dropped frames | FPS monitor in dev tools | Steady 60 FPS |
| High memory | Xcode Instruments / Android Profiler | < 200MB typical |
| Battery drain | Instruments Energy Log / Battery Historian | No background drain |
| Large app size | App bundle analysis | < 50MB download |

**React Native specific:**
```bash
# Enable performance monitor in dev
# Shake device -> "Show Perf Monitor"
# Or in code:
import { PerformanceObserver } from "react-native";
```

### Step 2: Common Fixes

**Slow list scrolling (most common mobile perf issue):**

```typescript
// Before: FlatList re-renders everything
<FlatList
  data={items}
  renderItem={({ item }) => <ItemCard item={item} onPress={() => handlePress(item)} />}
/>

// After: Properly optimized FlatList
<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  getItemLayout={getItemLayout}       // Skip measurement if items are fixed height
  maxToRenderPerBatch={10}            // Render 10 items per batch
  windowSize={5}                       // Render 5 screens worth of content
  removeClippedSubviews={true}        // Unmount off-screen items (Android)
  initialNumToRender={10}
/>

// Stable references prevent re-renders
const keyExtractor = useCallback((item: Item) => item.id, []);

const renderItem = useCallback(
  ({ item }: { item: Item }) => <MemoizedItemCard item={item} onPress={handlePress} />,
  [handlePress]
);

const getItemLayout = useCallback(
  (_: any, index: number) => ({ length: 80, offset: 80 * index, index }),
  []
);

const MemoizedItemCard = memo(ItemCard);
```

**Slow startup:**

```typescript
// Defer non-critical initialization
import { InteractionManager } from "react-native";

function App() {
  useEffect(() => {
    // Run after animations complete
    InteractionManager.runAfterInteractions(() => {
      analytics.init();
      crashReporting.init();
      prefetchData();
    });
  }, []);

  return <RootNavigator />;
}
```

**Large images:**
```typescript
// Always resize images to the display size
import FastImage from "react-native-fast-image";

<FastImage
  source={{
    uri: imageUrl,
    priority: FastImage.priority.normal,
    cache: FastImage.cacheControl.immutable,
  }}
  style={{ width: 100, height: 100 }}
  resizeMode={FastImage.resizeMode.cover}
/>
```

### Step 3: Memory Management

```typescript
// Clean up subscriptions and listeners
function LocationTracker() {
  useEffect(() => {
    const subscription = Location.watchPositionAsync(
      { accuracy: Location.Accuracy.Balanced, distanceInterval: 10 },
      (location) => setLocation(location)
    );

    return () => {
      subscription.then((sub) => sub.remove());
    };
  }, []);
}

// Avoid memory leaks with cancelled async operations
function useAsyncData<T>(fetcher: () => Promise<T>) {
  const [data, setData] = useState<T | null>(null);

  useEffect(() => {
    let cancelled = false;
    fetcher().then((result) => {
      if (!cancelled) setData(result);
    });
    return () => { cancelled = true; };
  }, [fetcher]);

  return data;
}
```

**STOPPING POINT 4**: You've identified the bottleneck. What next?

1. **Optimize list rendering** - FlashList, virtualization, item recycling
2. **Reduce app binary size** - Remove unused native modules, enable Hermes, ProGuard
3. **Profile native code** - Xcode Instruments or Android Studio Profiler deep dive
4. **Add performance monitoring** - Firebase Performance, Sentry, custom metrics

---

## Workflow 4: Set Up Push Notifications

### Step 1: Architecture Overview

```
Your Server -> FCM/APNs -> Device -> Your App
                                      |
                                      v
                              Notification Handler
                              |               |
                              v               v
                          Foreground      Background
                          (in-app UI)    (system tray)
```

### Step 2: React Native Implementation (Expo)

```typescript
// services/notifications.ts
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import { Platform } from "react-native";
import { api } from "./api";

// Configure how notifications appear when app is in foreground
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

export async function registerForPushNotifications(): Promise<string | null> {
  if (!Device.isDevice) {
    console.warn("Push notifications only work on physical devices");
    return null;
  }

  // Request permission
  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;

  if (existingStatus !== "granted") {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== "granted") {
    return null;  // User denied permission
  }

  // Get push token
  const tokenData = await Notifications.getExpoPushTokenAsync({
    projectId: "your-expo-project-id",
  });
  const token = tokenData.data;

  // Android: create notification channels
  if (Platform.OS === "android") {
    await Notifications.setNotificationChannelAsync("orders", {
      name: "Order Updates",
      importance: Notifications.AndroidImportance.HIGH,
      vibrationPattern: [0, 250, 250, 250],
    });

    await Notifications.setNotificationChannelAsync("messages", {
      name: "Messages",
      importance: Notifications.AndroidImportance.DEFAULT,
    });

    await Notifications.setNotificationChannelAsync("promotions", {
      name: "Promotions",
      importance: Notifications.AndroidImportance.LOW,
    });
  }

  // Send token to your server
  await api.post("/users/me/push-token", { token, platform: Platform.OS });

  return token;
}

export function useNotificationListeners() {
  useEffect(() => {
    // Handle notification received while app is open
    const foregroundSub = Notifications.addNotificationReceivedListener((notification) => {
      const data = notification.request.content.data;
      console.log("Notification received in foreground:", data);
    });

    // Handle user tapping on a notification
    const responseSub = Notifications.addNotificationResponseReceivedListener((response) => {
      const data = response.notification.request.content.data;
      handleDeepLink(data);
    });

    return () => {
      foregroundSub.remove();
      responseSub.remove();
    };
  }, []);
}

function handleDeepLink(data: Record<string, unknown>) {
  if (data.type === "order_update" && data.orderId) {
    navigation.navigate("OrderDetail", { id: data.orderId as string });
  } else if (data.type === "message" && data.conversationId) {
    navigation.navigate("Chat", { id: data.conversationId as string });
  }
}
```

### Step 3: Server-Side Sending

```python
# Python server - sending push notifications
import httpx

async def send_push_notification(
    push_token: str,
    title: str,
    body: str,
    data: dict = None,
    channel_id: str = None,
) -> bool:
    """Send via Expo Push API (handles both APNs and FCM)."""
    message = {
        "to": push_token,
        "title": title,
        "body": body,
        "data": data or {},
        "sound": "default",
    }
    if channel_id:
        message["channelId"] = channel_id

    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://exp.host/--/api/v2/push/send",
            json=message,
            headers={"Content-Type": "application/json"},
        )
        result = response.json()
        return result.get("data", {}).get("status") == "ok"
```

**STOPPING POINT 5**: Push notifications are working. What next?

1. **Add rich notifications** - Images, action buttons, expandable content
2. **Add notification preferences** - Let users control which types they receive
3. **Add analytics** - Track open rates, tap-through rates per notification type
4. **Add scheduled notifications** - Local notifications for reminders and timers
5. **Add silent push** - Background data sync triggered by push

---

## Workflow 5: Plan App Store Submission

### Step 1: Pre-Submission Checklist

**Apple App Store:**

- [ ] App icon: 1024x1024 PNG, no alpha channel, no rounded corners
- [ ] Screenshots: required for 6.7" (iPhone 15 Pro Max) and 12.9" (iPad Pro)
- [ ] App name: max 30 characters
- [ ] Subtitle: max 30 characters
- [ ] Description: up to 4000 characters
- [ ] Keywords: up to 100 characters, comma-separated
- [ ] Privacy policy URL (required)
- [ ] Support URL (required)
- [ ] Age rating questionnaire completed
- [ ] App category selected (primary + optional secondary)
- [ ] App uses IDFA? Declare in App Tracking Transparency
- [ ] Export compliance information submitted
- [ ] Sign-in required? Provide demo credentials for review team

**Google Play:**

- [ ] App icon: 512x512 PNG
- [ ] Feature graphic: 1024x500 PNG (required)
- [ ] Screenshots: min 2, max 8 per device type
- [ ] Short description: max 80 characters
- [ ] Full description: max 4000 characters
- [ ] Privacy policy URL (required)
- [ ] Content rating questionnaire completed
- [ ] Target audience and content declarations
- [ ] Data safety section completed
- [ ] App category and tags selected

### Step 2: Screenshot Strategy

**What to show (in order):**
1. The core value proposition (what makes your app worth downloading)
2. The primary feature in action
3. A secondary feature that supports the value prop
4. Social proof or unique differentiator
5. Final CTA or brand shot

**Screenshot spec by device:**
```
iPhone 6.7":  1290 x 2796 px  (required)
iPhone 6.5":  1242 x 2688 px  (optional but recommended)
iPad 12.9":   2048 x 2732 px  (required if iPad support)
Android:      1080 x 1920 px minimum (recommended: match popular devices)
```

### Step 3: Common Rejection Reasons and How to Avoid Them

| Rejection Reason | How to Avoid |
|-----------------|--------------|
| Crashes or bugs | Test on physical devices, all supported OS versions |
| Broken links | Check all URLs, support links, privacy policy |
| Placeholder content | Remove all lorem ipsum, sample data, TODO comments |
| Incomplete information | Fill out every metadata field completely |
| Login issues | Provide working demo account credentials |
| Missing permissions justification | Every permission usage description must explain WHY |
| Guideline 4.3 (spam/copycat) | Ensure your app has unique value, not just a web wrapper |
| Privacy issues | Declare all data collection, provide opt-out where needed |

### Step 4: Release Strategy

```
Internal Testing (1-5 testers)
  -> Closed Testing (10-100 testers)
    -> Open Beta (unlimited)
      -> Production (staged rollout)

Apple:
  TestFlight Internal -> TestFlight External -> App Store (phased release)

Google:
  Internal Testing -> Closed Testing -> Open Testing -> Production (staged rollout %)
```

**Staged rollout approach (Google Play):**
```
Day 1:  1% of users
Day 2:  5% of users (if no crash spike)
Day 3:  10% of users
Day 5:  25% of users
Day 7:  50% of users
Day 10: 100% of users
```

Monitor at each stage: crash rate, ANR rate, bad ratings, uninstall rate.

**STOPPING POINT 6**: Your submission is planned. What next?

1. **Write ASO-optimized metadata** - Keywords, description, and title for discoverability
2. **Create screenshots** - Design tool templates or automated screenshot generation
3. **Set up CI for builds** - Fastlane or EAS Build for automated signing and uploading
4. **Plan the update cycle** - How often to release, what triggers a hotfix
5. **Set up crash monitoring** - Sentry or Firebase Crashlytics before launch

