Mobile Developer Examples
Externalized from the agent definition per the few-shot-examples rule (#1587).
Mobile Developer — Worked Examples
Externalized from the agent definition per the few-shot-examples rule (#1587).
Process Sample Blocks
These full sample implementations back the capability summaries in the agent's "## Your Process" section. Each illustrates the architecture, pattern, or pipeline the agent produces.
1. Architecture Design
iOS — Clean Architecture with SwiftUI
// MARK: - Domain Layer
protocol UserRepository {
func fetchUser(id: String) async throws -> User
func updateProfile(_ profile: UserProfile) async throws
}
struct FetchUserUseCase {
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func execute(id: String) async throws -> User {
try await repository.fetchUser(id: id)
}
}
// MARK: - ViewModel
@MainActor
final class ProfileViewModel: ObservableObject {
@Published private(set) var state: ViewState<User> = .idle
private let fetchUser: FetchUserUseCase
init(fetchUser: FetchUserUseCase) {
self.fetchUser = fetchUser
}
func load(id: String) async {
state = .loading
do {
let user = try await fetchUser.execute(id: id)
state = .loaded(user)
} catch {
state = .error(error.localizedDescription)
}
}
}
// MARK: - View
struct ProfileView: View {
@StateObject private var viewModel: ProfileViewModel
var body: some View {
Group {
switch viewModel.state {
case .idle, .loading:
ProgressView()
case .loaded(let user):
UserDetailView(user: user)
case .error(let message):
ErrorView(message: message)
}
}
.task { await viewModel.load(id: userId) }
}
}
Android — MVVM with Jetpack Compose
// ViewModel with StateFlow
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val userId: String = checkNotNull(savedStateHandle["userId"])
private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
init { loadUser() }
private fun loadUser() {
viewModelScope.launch {
getUserUseCase(userId)
.onSuccess { _uiState.value = UiState.Success(it) }
.onFailure { _uiState.value = UiState.Error(it.message ?: "Unknown error") }
}
}
}
// Composable screen
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold { padding ->
when (val state = uiState) {
is UiState.Loading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
is UiState.Success -> UserDetail(
user = state.data,
modifier = Modifier.padding(padding)
)
is UiState.Error -> ErrorMessage(
message = state.message,
onRetry = viewModel::loadUser
)
}
}
}
2. React Native Components and Native Bridge
Shared component with platform-specific behavior
// components/HapticButton.tsx
import React from 'react';
import { Platform, Pressable, StyleSheet, Text, ViewStyle } from 'react-native';
import * as Haptics from 'expo-haptics';
interface HapticButtonProps {
label: string;
onPress: () => void;
variant?: 'primary' | 'destructive';
style?: ViewStyle;
}
export function HapticButton({ label, onPress, variant = 'primary', style }: HapticButtonProps) {
const handlePress = async () => {
if (Platform.OS !== 'web') {
await Haptics.impactAsync(
variant === 'destructive'
? Haptics.ImpactFeedbackStyle.Heavy
: Haptics.ImpactFeedbackStyle.Light,
);
}
onPress();
};
return (
<Pressable
onPress={handlePress}
style={({ pressed }) => [
styles.button,
variant === 'destructive' && styles.destructive,
pressed && styles.pressed,
style,
]}
accessibilityRole="button"
accessibilityLabel={label}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#007AFF',
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 12,
alignItems: 'center',
minHeight: 44,
},
destructive: { backgroundColor: '#FF3B30' },
pressed: { opacity: 0.75 },
label: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
});
Native module bridge (Camera with custom processing)
// modules/CameraModule.ts — TypeScript interface over native module
import { NativeModules, NativeEventEmitter, Platform } from 'react-native';
const { NativeCameraModule } = NativeModules;
export interface ScanResult {
barcode: string;
format: 'QR_CODE' | 'EAN_13' | 'CODE_128';
confidence: number;
}
export const CameraModule = {
startScanning: (): Promise<void> => NativeCameraModule.startScanning(),
stopScanning: (): Promise<void> => NativeCameraModule.stopScanning(),
setTorchEnabled: (enabled: boolean): Promise<void> =>
NativeCameraModule.setTorchEnabled(enabled),
};
// Event emitter for scan results
const emitter = new NativeEventEmitter(NativeCameraModule);
export function onScanResult(callback: (result: ScanResult) => void) {
const subscription = emitter.addListener('onBarcodeScan', callback);
return () => subscription.remove();
}
// iOS native module (NativeCameraModule.swift)
import AVFoundation
import React
@objc(NativeCameraModule)
class NativeCameraModule: RCTEventEmitter, AVCaptureMetadataOutputObjectsDelegate {
private var session: AVCaptureSession?
override func supportedEvents() -> [String]! {
return ["onBarcodeScan"]
}
@objc func startScanning(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.setupAndStartSession()
resolve(nil)
}
}
func metadataOutput(
_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection
) {
guard let barcode = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
let value = barcode.stringValue
else { return }
sendEvent(withName: "onBarcodeScan", body: [
"barcode": value,
"format": barcode.type.rawValue,
"confidence": 0.95,
])
}
private func setupAndStartSession() { /* ... */ }
}
3. Flutter Widgets and State Management
// Flutter: BLoC pattern with Freezed data classes
// lib/features/cart/bloc/cart_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'cart_bloc.freezed.dart';
@freezed
class CartEvent with _$CartEvent {
const factory CartEvent.addItem(CartItem item) = _AddItem;
const factory CartEvent.removeItem(String itemId) = _RemoveItem;
const factory CartEvent.checkout() = _Checkout;
}
@freezed
class CartState with _$CartState {
const factory CartState.idle(List<CartItem> items) = _Idle;
const factory CartState.processing(List<CartItem> items) = _Processing;
const factory CartState.checkoutSuccess(String orderId) = _CheckoutSuccess;
const factory CartState.error(String message, List<CartItem> items) = _Error;
}
class CartBloc extends Bloc<CartEvent, CartState> {
final CartRepository _repository;
CartBloc(this._repository) : super(const CartState.idle([])) {
on<CartEvent>((event, emit) async {
await event.when(
addItem: (item) => _onAddItem(item, emit),
removeItem: (id) => _onRemoveItem(id, emit),
checkout: () => _onCheckout(emit),
);
});
}
Future<void> _onAddItem(_AddItem event, Emitter<CartState> emit) async {
final current = state.mapOrNull(idle: (s) => s.items) ?? [];
emit(CartState.idle([...current, event.item]));
}
Future<void> _onCheckout(Emitter<CartState> emit) async {
final items = state.mapOrNull(idle: (s) => s.items) ?? [];
emit(CartState.processing(items));
try {
final orderId = await _repository.checkout(items);
emit(CartState.checkoutSuccess(orderId));
} catch (e) {
emit(CartState.error(e.toString(), items));
}
}
Future<void> _onRemoveItem(_RemoveItem event, Emitter<CartState> emit) async {
final current = state.mapOrNull(idle: (s) => s.items) ?? [];
emit(CartState.idle(current.where((i) => i.id != event.itemId).toList()));
}
}
// Flutter widget consuming the BLoC
class CartScreen extends StatelessWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CartBloc, CartState>(
builder: (context, state) => state.when(
idle: (items) => CartItemList(
items: items,
onRemove: (id) => context.read<CartBloc>().add(CartEvent.removeItem(id)),
onCheckout: () => context.read<CartBloc>().add(const CartEvent.checkout()),
),
processing: (items) => const Center(child: CircularProgressIndicator()),
checkoutSuccess: (orderId) => OrderConfirmation(orderId: orderId),
error: (msg, items) => ErrorBanner(message: msg),
),
);
}
}
4. Offline-First Data Sync
// iOS: Core Data + CloudKit sync
import CoreData
import CloudKit
class SyncManager {
private let container: NSPersistentCloudKitContainer
init() {
container = NSPersistentCloudKitContainer(name: "AppModel")
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
let description = container.persistentStoreDescriptions.first!
description.setOption(true as NSNumber,
forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores { _, error in
if let error { fatalError("Store failed: \(error)") }
}
}
func saveContext() {
let context = container.viewContext
guard context.hasChanges else { return }
do { try context.save() } catch {
print("Save failed: \(error)")
}
}
}
// Android: Room + WorkManager background sync
@Entity(tableName = "tasks")
data class TaskEntity(
@PrimaryKey val id: String,
val title: String,
val completed: Boolean,
val syncStatus: SyncStatus = SyncStatus.PENDING,
val updatedAt: Long = System.currentTimeMillis(),
)
enum class SyncStatus { SYNCED, PENDING, CONFLICT }
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repository: TaskRepository,
private val apiService: ApiService,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
val pending = repository.getPendingTasks()
pending.forEach { task ->
apiService.upsertTask(task.toDto())
repository.markSynced(task.id)
}
val remote = apiService.fetchTasks()
repository.mergeRemote(remote)
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
companion object {
fun schedule(workManager: WorkManager) {
val request = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(Constraints(NetworkType.CONNECTED))
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
workManager.enqueueUniquePeriodicWork(
"task_sync", ExistingPeriodicWorkPolicy.KEEP, request
)
}
}
}
5. Device Farm and E2E Testing
// React Native: Detox E2E tests
// e2e/checkout.test.ts
import { device, element, by, expect as detoxExpect } from 'detox';
describe('Checkout Flow', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('completes purchase from product detail', async () => {
await element(by.id('product-list-item-0')).tap();
await detoxExpect(element(by.id('product-detail-screen'))).toBeVisible();
await element(by.id('add-to-cart-button')).tap();
await element(by.id('cart-tab')).tap();
await detoxExpect(element(by.id('cart-item-0'))).toBeVisible();
await element(by.id('checkout-button')).tap();
// Fill shipping form
await element(by.id('address-input')).typeText('123 Main St');
await element(by.id('city-input')).typeText('Springfield');
await element(by.id('continue-button')).tap();
// Confirm order
await detoxExpect(element(by.id('order-confirmation-screen'))).toBeVisible();
await detoxExpect(element(by.id('order-id-label'))).toBeVisible();
});
it('handles network failure gracefully', async () => {
await device.setStatusBar({ networkActivity: false });
// Simulate offline state
await device.setURLBlacklist(['.*api.myapp.com.*']);
await element(by.id('checkout-button')).tap();
await detoxExpect(element(by.id('offline-banner'))).toBeVisible();
await device.setURLBlacklist([]);
});
});
# Firebase Test Lab via GitHub Actions
# .github/workflows/device-farm.yml
name: Device Farm Tests
on:
pull_request:
branches: [main]
jobs:
android-device-farm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build debug APK
run: ./gradlew assembleDebug assembleAndroidTest
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Run on Firebase Test Lab
run: |
gcloud firebase test android run \
--type instrumentation \
--app app/build/outputs/apk/debug/app-debug.apk \
--test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
--device model=Pixel7,version=33,locale=en,orientation=portrait \
--device model=SamsungS21,version=31,locale=en,orientation=portrait \
--device model=redfin,version=30,locale=en,orientation=portrait \
--timeout 10m \
--results-bucket gs://my-test-results \
--results-dir "${{ github.run_id }}"
ios-device-farm:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Build for testing
run: |
xcodebuild build-for-testing \
-scheme MyApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath DerivedData
- name: Run on BrowserStack
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
run: |
zip -r TestApp.zip DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app
curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \
-X POST "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/build" \
-F "[email protected]" \
-F 'data={"devices":["iPhone 15-17","iPhone 13-16"],"project":"MyApp","networkProfile":"4g-lte-good"}'
6. CI/CD with Fastlane and EAS Build
Fastlane for native apps
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Run tests"
lane :test do
run_tests(
scheme: "MyApp",
devices: ["iPhone 15 Pro", "iPhone SE (3rd generation)"],
code_coverage: true,
output_directory: "fastlane/test_output",
)
end
desc "Build and submit to TestFlight"
lane :beta do
ensure_git_status_clean
increment_build_number(
build_number: latest_testflight_build_number + 1,
)
build_app(
scheme: "MyApp",
export_method: "app-store",
include_bitcode: false,
)
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: changelog_from_git_commits(
commits_count: 10,
pretty: "- %s",
),
)
slack(
message: "iOS beta #{lane_context[SharedValues::VERSION_NUMBER]} submitted to TestFlight",
channel: "#mobile-releases",
)
end
desc "Release to App Store"
lane :release do
deliver(
submit_for_review: true,
automatic_release: false,
force: true,
skip_screenshots: true,
submission_information: {
add_id_info_uses_idfa: false,
export_compliance_uses_encryption: false,
},
)
end
end
platform :android do
desc "Build and upload to Play Store internal track"
lane :beta do
gradle(
task: "bundle",
build_type: "Release",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASSWORD"],
},
)
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab",
)
end
end
EAS Build for React Native
// eas.json
{
"cli": { "version": ">= 10.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true },
"android": { "buildType": "apk" }
},
"preview": {
"distribution": "internal",
"channel": "preview",
"ios": {
"enterpriseProvisioning": "adhoc"
}
},
"production": {
"autoIncrement": true,
"channel": "production",
"ios": { "buildConfiguration": "Release" },
"android": {
"buildType": "app-bundle",
"gradleCommand": ":app:bundleRelease"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "[email protected]",
"ascAppId": "1234567890",
"appleTeamId": "XXXXXXXXXX"
},
"android": {
"serviceAccountKeyPath": "./service-account.json",
"track": "production",
"releaseStatus": "completed"
}
}
}
}
# .github/workflows/eas-build.yml — EAS Build in CI
name: EAS Build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Build preview (PR)
if: github.event_name == 'pull_request'
run: eas build --platform all --profile preview --non-interactive
- name: Build and submit production (main)
if: github.ref == 'refs/heads/main'
run: |
eas build --platform all --profile production --non-interactive
eas submit --platform all --profile production --non-interactive
Bitrise pipeline for Flutter
# bitrise.yml
format_version: '11'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
workflows:
flutter_ci:
steps:
- activate-ssh-key@4: {}
- git-clone@8: {}
- flutter-installer@0:
inputs:
- version: stable
- cache-pull@2: {}
- script@1:
title: Flutter pub get
inputs:
- content: flutter pub get
- flutter-test@1:
inputs:
- project_location: .
- platform: both
- script@1:
title: Build Android release
inputs:
- content: |
flutter build appbundle \
--release \
--build-number=$BITRISE_BUILD_NUMBER
- script@1:
title: Build iOS release
inputs:
- content: |
flutter build ipa \
--release \
--build-number=$BITRISE_BUILD_NUMBER \
--export-options-plist=ios/ExportOptions.plist
- deploy-to-itunesconnect-application-loader@1:
inputs:
- ipa_path: build/ios/ipa/MyApp.ipa
- apple_id: $APPLE_ID
- password: $APP_SPECIFIC_PASSWORD
- google-play@1:
inputs:
- service_account_json_key_path: $BITRISEIO_SERVICE_ACCOUNT_JSON_KEY_URL
- package_name: com.mycompany.myapp
- app_path: build/app/outputs/bundle/release/app-release.aab
- track: internal
- cache-push@2: {}
Few-Shot Examples
Example 1: Architecture Review
Prompt: "Review this iOS app — it's getting slow and the code is hard to maintain."
Response:
After reviewing the codebase, I see three root causes for both slowness and maintainability issues.
Problem 1 — Massive ViewControllers: `HomeViewController` is 1,400 lines with network calls, data transformation, and UI logic interleaved. Extract a ViewModel:
@MainActor
final class HomeViewModel: ObservableObject {
@Published private(set) var items: [FeedItem] = []
@Published private(set) var isLoading = false
private let fetchFeed: FetchFeedUseCase
func refresh() async {
isLoading = true
defer { isLoading = false }
items = (try? await fetchFeed.execute()) ?? items
}
}
Problem 2 — URLSession on main thread: `URLSession.shared.dataTask` completions update UI without dispatching to main. Use `async/await` which handles threading correctly.
Problem 3 — No image caching: `UIImage(contentsOfFile:)` is called in `cellForRowAt`, decoding images on the main thread per scroll. Use `SDWebImage` or an `NSCache`-backed async loader.
Example 2: CI/CD Setup
Prompt: "How do we automate our React Native releases to both stores?"
Response:
Use EAS Build for building and EAS Submit for store delivery. The setup takes about a day but eliminates all manual steps:
# Install EAS CLI
npm install -g eas-cli
# Initialize
eas build:configure
eas submit:configure
# Add to CI — this handles both stores in one command
eas build --platform all --profile production --non-interactive
eas submit --platform all --profile production --non-interactive
Configure `eas.json` with three profiles: `development` (simulator/APK for local testing), `preview` (ad-hoc/internal for QA), and `production` (App Store/Play Store). Store credentials live in EAS secrets, not in your repo.
For Android, the service account JSON from Google Play Console gives EAS permission to upload. For iOS, Expo manages provisioning profiles automatically via App Store Connect API key — no more certificate sharing between team members.
Example 3: Performance Profiling
Prompt: "Our Flutter list screen drops frames on older Android devices."
Response:
Frame drops in Flutter lists almost always come from expensive `build()` methods, synchronous image decoding, or rebuilding too much of the widget tree per scroll.
Step 1 — Enable performance overlay:
MaterialApp(
showPerformanceOverlay: true, // Red bars = jank
...
)
Step 2 — Profile with DevTools: Run `flutter run --profile` and open Dart DevTools. Sort the flame chart by self time to find the expensive widget.
Most common fix — const constructors and ListView.builder:
// BEFORE: Rebuilds all items on any state change
ListView(children: items.map((i) => ItemWidget(item: i)).toList())
// AFTER: Only builds visible items
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemWidget(item: items[index]),
)
// ALSO: Mark stateless widgets const
const ItemWidget({super.key, required this.item});
If images are the issue: Use `cached_network_image` with `memCacheWidth` to resize on decode rather than storing full-resolution images in memory.