
Senior Mobile
- 303 installs
- 454 repo stars
- Updated July 21, 2026
- borghei/claude-skills
senior-mobile is an agent skill that guides building native or cross-platform iOS and Android apps with navigation, offline handling, push notifications, and store-ready polish for developers shipping production mobile c
About
senior-mobile is an agent skill from borghei/claude-skills that applies senior-level mobile engineering practices when building native or cross-platform iOS and Android applications. The skill covers solid navigation architecture, offline data handling, push notification integration, performance tuning, and store-ready UI polish so apps meet App Store and Google Play expectations before submission. Developers reach for senior-mobile when scaffolding a new mobile codebase, hardening offline-first flows, wiring FCM or APNs notifications, or reviewing navigation and state management patterns across Swift, Kotlin, or cross-platform frameworks. senior-mobile spans multiple build concerns—frontend structure, client resilience, and pre-release quality—making it a multi-phase companion during mobile product development rather than a one-shot code generator. Invoke when asked to architect mobile navigation, implement offline sync, add push notifications, or polish an app for store release.
- Structures navigation and screen lifecycle correctly
- Handles offline, caching, and background tasks
- Applies platform HIG and performance best practices
- Integrates push, deep links, and secure storage
- Prepares builds for App Store and Play review norms
Senior Mobile by the numbers
- 303 all-time installs (skills.sh)
- Ranked #371 of 1,038 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 6, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill senior-mobileAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 303 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
How do you build production-ready mobile apps?
Build native or cross-platform iOS and Android apps with solid navigation, offline handling, push notifications, and store-ready polish.
Who is it for?
Developers building native or cross-platform iOS and Android apps who need senior guidance on navigation, offline sync, and push notifications.
Skip if: Web-only frontend projects or backend API teams without a mobile client codebase to implement.
When should I use this skill?
User asks to build iOS or Android apps, implement mobile navigation, offline handling, push notifications, or store-ready mobile polish.
What you get
Mobile app architecture with navigation, offline handling, push notification integration, and store-ready UI patterns for iOS and Android.
- Mobile navigation architecture
- Offline and push notification implementation plan
Files
Senior Mobile Developer
Expert mobile application development across iOS, Android, React Native, and Flutter.
Keywords
mobile, ios, android, react-native, flutter, swift, kotlin, swiftui, jetpack-compose, expo-router, zustand, app-store, performance, offline-first
---
Quick Start
# Scaffold a React Native project
python scripts/mobile_scaffold.py --platform react-native --name MyApp
# Build for production
python scripts/build.py --platform ios --env production
# Generate App Store metadata
python scripts/store_metadata.py --screenshots ./screenshots
# Profile rendering performance
python scripts/profile.py --platform android --output report.html---
Tools
| Script | Purpose |
|---|---|
scripts/mobile_scaffold.py | Scaffold project for react-native, ios, android, or flutter |
scripts/build.py | Build automation with environment and platform flags |
scripts/store_metadata.py | Generate App Store / Play Store listing metadata |
scripts/profile.py | Profile rendering, memory, and startup performance |
---
Platform Decision Matrix
| Aspect | Native iOS | Native Android | React Native | Flutter |
|---|---|---|---|---|
| Language | Swift | Kotlin | TypeScript | Dart |
| UI Framework | SwiftUI/UIKit | Compose/XML | React | Widgets |
| Performance | Best | Best | Good | Very Good |
| Code Sharing | None | None | ~80% | ~95% |
| Best For | iOS-only, hardware-heavy | Android-only, hardware-heavy | Web team, shared logic | Maximum code sharing |
---
Workflow 1: Scaffold a React Native App (Expo Router)
1. Generate project -- python scripts/mobile_scaffold.py --platform react-native --name MyApp 2. Verify directory structure matches this layout:
src/
├── app/ # Expo Router file-based routes
│ ├── (tabs)/ # Tab navigation group
│ ├── auth/ # Auth screens
│ └── _layout.tsx # Root layout
├── components/
│ ├── ui/ # Reusable primitives (Button, Input, Card)
│ └── features/ # Domain components (ProductCard, UserAvatar)
├── hooks/ # Custom hooks (useAuth, useApi)
├── services/ # API clients and storage
├── stores/ # Zustand state stores
└── utils/ # Helpers3. Configure navigation in app/_layout.tsx with Stack and Tabs. 4. Set up state management with Zustand + AsyncStorage persistence. 5. Validate -- Run the app on both iOS simulator and Android emulator. Confirm navigation and state persistence work.
Workflow 2: Build a SwiftUI Feature (iOS)
1. Create the View using NavigationStack, @StateObject for ViewModel binding, and .task for async data loading. 2. Create the ViewModel as @MainActor class with @Published properties. Inject services via protocol for testability. 3. Wire data flow: View observes ViewModel -> ViewModel calls Service -> Service returns data -> ViewModel updates @Published -> View re-renders. 4. Add search/refresh: .searchable(text:) for filtering, .refreshable for pull-to-refresh. 5. Validate -- Run in Xcode previews first, then simulator. Confirm async loading, error states, and empty states all render correctly.
Example: SwiftUI ViewModel Pattern
@MainActor
class ProductListViewModel: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
private let service: ProductServiceProtocol
init(service: ProductServiceProtocol = ProductService()) {
self.service = service
}
func loadProducts() async {
isLoading = true
error = nil
do {
products = try await service.fetchProducts()
} catch {
self.error = error
}
isLoading = false
}
}Workflow 3: Build a Jetpack Compose Feature (Android)
1. Create the Composable screen with Scaffold, TopAppBar, and state collection via collectAsStateWithLifecycle(). 2. Handle UI states with a sealed interface: Loading, Success<T>, Error. 3. Create the ViewModel with @HiltViewModel, MutableStateFlow, and repository injection. 4. Build list UI using LazyColumn with key parameter for stable identity and Arrangement.spacedBy() for spacing. 5. Validate -- Run on emulator. Confirm state transitions (loading -> success, loading -> error -> retry) work correctly.
Example: Compose UiState Pattern
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String) : UiState<Nothing>
}
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<Product>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Product>>> = _uiState.asStateFlow()
fun loadProducts() {
viewModelScope.launch {
_uiState.value = UiState.Loading
repository.getProducts()
.catch { e -> _uiState.value = UiState.Error(e.message ?: "Unknown error") }
.collect { products -> _uiState.value = UiState.Success(products) }
}
}
}Workflow 4: Optimize Mobile Performance
1. Profile -- python scripts/profile.py --platform <ios|android> --output report.html 2. Apply React Native optimizations:
- Use
FlatListwithkeyExtractor,initialNumToRender=10,windowSize=5,removeClippedSubviews=true - Memoize components with
React.memoand handlers withuseCallback - Supply
getItemLayoutfor fixed-height rows to skip measurement
3. Apply native iOS optimizations:
- Implement
prefetchItemsAtfor image pre-loading in collection views
4. Apply native Android optimizations:
- Set
setHasFixedSize(true)andsetItemViewCacheSize(20)on RecyclerViews
5. Validate -- Re-run profiler and confirm frame drops reduced and startup time improved.
Workflow 5: Submit to App Store / Play Store
1. Generate metadata -- python scripts/store_metadata.py --screenshots ./screenshots 2. Build release -- python scripts/build.py --platform ios --env production 3. Review the generated listing (title, description, keywords, screenshots). 4. Upload via Xcode (iOS) or Play Console (Android). 5. Validate -- Monitor review status and address any rejection feedback.
---
Reference Materials
| Document | Path |
|---|---|
| React Native Guide | references/react_native_guide.md |
| iOS Patterns | references/ios_patterns.md |
| Android Patterns | references/android_patterns.md |
| App Store Guide | references/app_store_guide.md |
| Full Code Examples | REFERENCE.md |
---
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| App crashes on launch after adding a new dependency | Incompatible native module version or missing pod install / gradle sync | Run npx pod-install (iOS) or cd android && ./gradlew clean (Android). Verify dependency version compatibility in the changelog. |
| FlatList renders blank or flickers | Missing keyExtractor, unstable keys, or inline renderItem causing full re-renders | Add a stable keyExtractor, wrap renderItem in useCallback, and supply getItemLayout for fixed-height rows. |
| iOS build fails with "signing" error | Provisioning profile mismatch or expired certificate | Open Xcode > Signing & Capabilities, select the correct team and profile. Run security find-identity -v -p codesigning to verify certificates. |
| Android build OOM during dexing | Insufficient JVM heap for large projects | Add org.gradle.jvmargs=-Xmx4096m to gradle.properties. Enable dexOptions { javaMaxHeapSize "4g" } in build.gradle. |
| App Store rejection for missing privacy manifest | Apple requires PrivacyInfo.xcprivacy for apps using required reason APIs (UserDefaults, file timestamp, etc.) | Add a PrivacyInfo.xcprivacy file declaring each required reason API. Run store_metadata_generator.py to review privacy label guidance. |
| Slow cold start time (>3 seconds) | Too many synchronous operations on the main thread at launch, large bundle size, or unoptimized images | Defer non-critical initialization, lazy-load modules, compress images, and use app_performance_analyzer.py to identify bottlenecks. |
| Hot reload / fast refresh stops working | Syntax error in a module boundary, anonymous default export, or class component state | Check terminal for error messages, ensure named exports, and restart the Metro bundler or Flutter daemon with a cache clear. |
---
Success Criteria
- App startup time under 2 seconds on cold launch (measured on mid-range devices, both iOS and Android).
- Crash-free rate above 99.5% across all supported OS versions, tracked via Crashlytics or Sentry.
- Frame rendering at 60 fps (16ms per frame) for scrolling lists and animations, with zero jank frames during typical user flows.
- Bundle size under 50 MB for the initial download (excluding on-demand resources), verified before each release.
- Performance analyzer score of 75+ (Grade B or above) when running
app_performance_analyzer.pyagainst the project. - Zero critical issues and fewer than 5 warnings reported by the performance analyzer before submitting to app stores.
- App Store / Play Store approval on first submission with complete metadata, correct privacy labels, and proper age rating, validated using
store_metadata_generator.py.
---
Scope & Limitations
This skill covers:
- Scaffolding production-ready mobile projects for React Native (Expo Router), Flutter, iOS native (SwiftUI), and Android native (Jetpack Compose).
- Static performance analysis including image asset sizing, re-render detection, memory leak patterns, and bundle size estimation.
- App Store and Play Store metadata generation including titles, keywords, privacy labels, age ratings, and submission checklists.
- Platform-specific architecture patterns (MVVM, state management, navigation).
This skill does NOT cover:
- Backend API development or server-side logic (see
senior-backendandsenior-fullstackskills). - CI/CD pipeline configuration for mobile builds and automated distribution (see
senior-devopsandrelease-orchestratorskills). - UI/UX design systems, accessibility auditing, or design token management (see
senior-frontendanddesign-auditorskills). - Runtime profiling with native tools (Xcode Instruments, Android Studio Profiler) -- the analyzer performs static code analysis only, not live device profiling.
---
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
senior-frontend | Shared component patterns, styling conventions, and responsive design principles for React Native web targets | Frontend design tokens and component APIs feed into mobile UI components |
senior-backend | API contract definitions, authentication flows, and data models consumed by mobile clients | Backend OpenAPI specs define mobile service layer interfaces |
senior-devops | Build pipelines, code signing automation, and deployment workflows for mobile releases | Mobile build artifacts flow into CI/CD pipelines for TestFlight / Play Console distribution |
senior-qa | Test strategy alignment, device matrix coverage, and E2E testing patterns for mobile screens | QA test plans drive device coverage; mobile scaffold includes test directory structure |
senior-security | Secure storage patterns (Keychain/Keystore), certificate pinning, and data encryption for mobile apps | Security requirements inform Keychain helper implementation and network client configuration |
release-orchestrator | Version bumping, changelog generation, and coordinated release across iOS and Android | Release metadata and version info flow from orchestrator into store submission workflow |
---
Tool Reference
mobile_scaffold.py
Purpose: Scaffold a production-ready mobile project with proper directory structure, navigation setup, state management, and base configuration files.
Usage:
python scripts/mobile_scaffold.py <name> --platform <platform> [options]Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name | positional | Yes | -- | Project name, used as the directory name |
--platform, -p | choice | Yes | -- | Target platform: android-native, flutter, ios-native, react-native |
--typescript, -t | flag | No | False (auto-enabled for react-native) | Use TypeScript (React Native only) |
--state, -s | string | No | none | State management library. React Native: zustand, redux, jotai, none. Flutter: riverpod, bloc, provider, none. Not applicable for native platforms. |
--output-dir, -o | path | No | . (current directory) | Parent directory for the generated project |
--json | flag | No | False | Output result as JSON instead of human-readable summary |
Example:
# Scaffold a React Native app with Zustand state management
python scripts/mobile_scaffold.py MyApp --platform react-native --state zustand
# Scaffold a Flutter app with Riverpod, output as JSON
python scripts/mobile_scaffold.py my-flutter-app --platform flutter --state riverpod --json
# Scaffold an iOS native app in a specific directory
python scripts/mobile_scaffold.py HealthTracker --platform ios-native --output-dir ~/ProjectsOutput Formats:
- Human-readable (default): Prints the project name, platform, state management choice, created directory path, and a list of all generated files.
- JSON (`--json`): Returns a JSON object with
project_name,platform,typescript,state_management,output_directory,files_created, andgenerated_atfields.
---
store_metadata_generator.py
Purpose: Generate structured metadata for App Store (iOS) and Google Play Store (Android) submissions, including title variants, keywords, category recommendations, privacy labels, age rating guidance, and submission checklists.
Usage:
python scripts/store_metadata_generator.py --app-name <name> --category <category> --features <features> [options]Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
--app-name | string | Yes | -- | The app name for store listings |
--category | choice | Yes | -- | Primary app category. Choices: business, education, entertainment, finance, food, games, health, lifestyle, music, navigation, news, photo, productivity, shopping, social, sports, travel, utilities, weather |
--features | string | Yes | -- | Comma-separated list of features (e.g., "offline,sync,biometric"). Recognized features expand into keywords and trigger privacy/age-rating guidance. |
--description | string | No | "" | Short app description used in generated store copy |
--json | flag | No | False | Output results as JSON |
Example:
# Generate metadata for a health app
python scripts/store_metadata_generator.py --app-name "FitTrack" --category health --features "workout,tracking,social" --description "Track your workouts"
# JSON output for CI integration
python scripts/store_metadata_generator.py --app-name "BudgetPal" --category finance --features "payment,offline,biometric,push" --jsonOutput Formats:
- Human-readable (default): Formatted report with sections for Title Variants, Keywords (with iOS 100-char field), Store Categories, Privacy Labels / Data Safety, Age Rating Guidance, and Submission Checklist.
- JSON (`--json`): Full metadata object including
titles,keywords,categories,descriptions,privacy_labels,age_rating,screenshot_specs, andsubmission_checklist.
---
app_performance_analyzer.py
Purpose: Analyze a mobile project directory for common performance issues including oversized image assets, re-render patterns, memory leak patterns, bundle size estimation, and platform-specific anti-patterns.
Usage:
python scripts/app_performance_analyzer.py <project_dir> [options]Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
project_dir | positional | Yes | -- | Path to the mobile project directory to analyze |
--platform, -p | choice | No | Auto-detected | Target platform: react-native, flutter, ios-native, android-native. Auto-detected from project files if omitted. |
--json | flag | No | False | Output results as JSON |
Example:
# Analyze with auto-detected platform
python scripts/app_performance_analyzer.py ./my-app
# Analyze a React Native project explicitly
python scripts/app_performance_analyzer.py ./my-app --platform react-native
# JSON output for CI pipeline integration
python scripts/app_performance_analyzer.py ./my-app --platform flutter --jsonOutput Formats:
- Human-readable (default): Performance score (0-100 with letter grade), issue summary (critical/warning/info counts), bundle size estimate, detailed issues grouped by category, and platform-specific recommendations.
- JSON (`--json`): Full report object including
performance_score,summary,bundle_estimate(source code size, asset size, file counts),issues_by_category, and the flatissuesarray withcategory,severity,file,line, andmessageper issue.
Senior Mobile Developer -- Reference Code Examples
Extended code examples for the senior-mobile skill. See SKILL.md for workflows and quick start.
---
React Native: Animated Button Component
import { View, Text, Pressable, StyleSheet, ActivityIndicator } from 'react-native';
import Animated, {
useAnimatedStyle,
withSpring,
useSharedValue,
} from 'react-native-reanimated';
interface ButtonProps {
title: string;
variant?: 'primary' | 'secondary';
loading?: boolean;
disabled?: boolean;
onPress: () => void;
}
export function Button({
title,
variant = 'primary',
loading,
disabled,
onPress,
}: ButtonProps) {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Pressable
onPress={onPress}
onPressIn={() => { scale.value = withSpring(0.95); }}
onPressOut={() => { scale.value = withSpring(1); }}
disabled={disabled || loading}
>
<Animated.View
style={[styles.button, styles[variant], disabled && styles.disabled, animatedStyle]}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={[styles.text, styles[`${variant}Text`]]}>{title}</Text>
)}
</Animated.View>
</Pressable>
);
}
const styles = StyleSheet.create({
button: { paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
primary: { backgroundColor: '#007AFF' },
secondary: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#007AFF' },
disabled: { opacity: 0.5 },
text: { fontSize: 16, fontWeight: '600' },
primaryText: { color: '#fff' },
secondaryText: { color: '#007AFF' },
});React Native: Expo Router Navigation Setup
// app/_layout.tsx
import { Stack } from 'expo-router';
import { AuthProvider } from '@/contexts/AuthContext';
export default function RootLayout() {
return (
<AuthProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="auth" />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
</AuthProvider>
);
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => <Ionicons name="home" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => <Ionicons name="person" size={size} color={color} />,
}}
/>
</Tabs>
);
}React Native: Zustand Auth Store with Persistence
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState {
user: { id: string; email: string; name: string } | null;
token: string | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
refreshToken: () => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
user: null,
token: null,
isLoading: false,
login: async (email, password) => {
set({ isLoading: true });
try {
const response = await api.post('/auth/login', { email, password });
set({ user: response.data.user, token: response.data.token, isLoading: false });
} catch (error) {
set({ isLoading: false });
throw error;
}
},
logout: () => set({ user: null, token: null }),
refreshToken: async () => {
const { token } = get();
if (!token) return;
const response = await api.post('/auth/refresh', { token });
set({ token: response.data.token });
},
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({ user: state.user, token: state.token }),
}
)
);iOS: SwiftUI Product List with Search and Pull-to-Refresh
import SwiftUI
struct ProductListView: View {
@StateObject private var viewModel = ProductListViewModel()
@State private var searchText = ""
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
ErrorView(error: error, onRetry: viewModel.loadProducts)
} else {
List(viewModel.filteredProducts(searchText)) { product in
NavigationLink(value: product) {
ProductRow(product: product)
}
}
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
}
}
.navigationTitle("Products")
.searchable(text: $searchText)
.refreshable { await viewModel.loadProducts() }
}
.task { await viewModel.loadProducts() }
}
}
struct ProductRow: View {
let product: Product
var body: some View {
HStack(spacing: 12) {
AsyncImage(url: product.imageURL) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: {
Color.gray.opacity(0.3)
}
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 4) {
Text(product.name).font(.headline)
Text(product.formattedPrice).font(.subheadline).foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 4)
}
}Android: Jetpack Compose Product List
@Composable
fun ProductListScreen(
viewModel: ProductListViewModel = hiltViewModel(),
onProductClick: (Product) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = { TopAppBar(title = { Text("Products") }) }
) { padding ->
when (val state = uiState) {
is UiState.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is UiState.Error -> {
ErrorContent(message = state.message, onRetry = { viewModel.loadProducts() })
}
is UiState.Success -> {
LazyColumn(
modifier = Modifier.padding(padding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.data, key = { it.id }) { product ->
ProductCard(product = product, onClick = { onProductClick(product) })
}
}
}
}
}
}
@Composable
fun ProductCard(product: Product, onClick: () -> Unit, modifier: Modifier = Modifier) {
Card(modifier = modifier.fillMaxWidth(), onClick = onClick) {
Row(modifier = Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
AsyncImage(
model = product.imageUrl,
contentDescription = null,
modifier = Modifier.size(60.dp).clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
Column {
Text(text = product.name, style = MaterialTheme.typography.titleMedium)
Text(text = product.formattedPrice, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}React Native: FlatList Performance Configuration
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>iOS: Collection View Prefetching
func collectionView(
_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [IndexPath]
) {
for indexPath in indexPaths {
let product = products[indexPath.row]
imageLoader.prefetch(url: product.imageURL)
}
}Android: RecyclerView Optimization
recyclerView.apply {
setHasFixedSize(true)
setItemViewCacheSize(20)
recycledViewPool.setMaxRecycledViews(0, 20)
}iOS & Android Native Development Patterns
Comprehensive reference for native mobile development covering architecture, UI frameworks, concurrency, platform UX guidelines, and CI/CD.
---
iOS: SwiftUI vs UIKit Decision
When to Use SwiftUI
- Green-field projects targeting iOS 16+
- Teams with Swift experience wanting declarative UI
- Apps with dynamic layouts that adapt to content
- Rapid prototyping and design iteration
- Apps leveraging Combine/async-await for reactive data flow
When to Use UIKit
- Apps requiring iOS 13-15 support
- Complex custom animations and gesture handling
- Integration with legacy Objective-C codebases
- Fine-grained control over view lifecycle and layout
- Features not yet available in SwiftUI (some UIKit-only APIs)
Hybrid Approach (Recommended for most projects)
// Use SwiftUI for new screens, wrap UIKit when needed
// SwiftUI hosting UIKit
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
return MKMapView()
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// Update map region
}
}
// UIKit hosting SwiftUI
let swiftUIView = UIHostingController(rootView: ProfileView())
navigationController?.pushViewController(swiftUIView, animated: true)SwiftUI vs UIKit Comparison
| Aspect | SwiftUI | UIKit |
|---|---|---|
| Paradigm | Declarative | Imperative |
| Min iOS | 13 (practical: 16+) | All versions |
| Learning curve | Lower | Higher |
| Customization | Growing | Complete |
| Performance | Good (improving) | Excellent |
| Previews | Live previews | Storyboard/XIB |
| Accessibility | Built-in | Manual setup |
| Testing | Snapshot + unit | XCTest + UI tests |
---
iOS: MVVM-C Architecture
Overview
MVVM-C (Model-View-ViewModel-Coordinator) separates navigation from presentation logic.
┌──────────────┐
│ Coordinator │ - Navigation flow
│ │ - Dependency injection
└──────┬───────┘
│ creates
┌──────▼───────┐
│ View │ - SwiftUI View or UIViewController
│ │ - Displays data from ViewModel
└──────┬───────┘
│ observes
┌──────▼───────┐
│ ViewModel │ - Business logic
│ │ - Data transformation
└──────┬───────┘
│ uses
┌──────▼───────┐
│ Model │ - Data structures
│ + Services │ - Network, persistence
└──────────────┘Coordinator Pattern
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
func start()
}
class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
private let window: UIWindow
init(window: UIWindow) {
self.window = window
}
func start() {
let authCoordinator = AuthCoordinator()
authCoordinator.onLoginSuccess = { [weak self] in
self?.showMainFlow()
}
childCoordinators.append(authCoordinator)
window.rootViewController = authCoordinator.start()
window.makeKeyAndVisible()
}
private func showMainFlow() {
let mainCoordinator = MainTabCoordinator()
childCoordinators = [mainCoordinator]
window.rootViewController = mainCoordinator.start()
}
}ViewModel with async/await
@MainActor
class ProductListViewModel: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var state: ViewState = .idle
private let productService: ProductServiceProtocol
private var loadTask: Task<Void, Never>?
enum ViewState: Equatable {
case idle, loading, loaded, error(String)
}
init(productService: ProductServiceProtocol = ProductService()) {
self.productService = productService
}
func loadProducts() {
loadTask?.cancel()
loadTask = Task {
state = .loading
do {
products = try await productService.fetchProducts()
state = .loaded
} catch is CancellationError {
// Ignore cancellation
} catch {
state = .error(error.localizedDescription)
}
}
}
func refresh() async {
do {
products = try await productService.fetchProducts()
} catch {
state = .error(error.localizedDescription)
}
}
deinit {
loadTask?.cancel()
}
}---
iOS: Combine and async/await
Combine Publishers
import Combine
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published private(set) var results: [SearchResult] = []
private var cancellables = Set<AnyCancellable>()
private let searchService: SearchServiceProtocol
init(searchService: SearchServiceProtocol) {
self.searchService = searchService
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { $0.count >= 2 }
.sink { [weak self] query in
self?.performSearch(query)
}
.store(in: &cancellables)
}
private func performSearch(_ query: String) {
Task {
do {
results = try await searchService.search(query: query)
} catch {
results = []
}
}
}
}Structured Concurrency
// TaskGroup for parallel requests
func loadDashboard() async throws -> Dashboard {
async let profile = userService.getProfile()
async let notifications = notificationService.getRecent()
async let stats = analyticsService.getDailyStats()
return try await Dashboard(
profile: profile,
notifications: notifications,
stats: stats
)
}
// Task cancellation
class DownloadManager {
private var downloadTask: Task<Data, Error>?
func startDownload(url: URL) {
downloadTask = Task {
let (data, _) = try await URLSession.shared.data(from: url)
try Task.checkCancellation()
return data
}
}
func cancelDownload() {
downloadTask?.cancel()
}
}---
Android: Compose vs XML Decision
When to Use Jetpack Compose
- New projects targeting API 21+
- Teams ready for declarative UI paradigm
- Apps with dynamic, state-driven interfaces
- Faster iteration with live previews
- Material Design 3 compliance
When to Use XML Layouts
- Existing large codebases with XML infrastructure
- Apps needing RecyclerView with complex item types
- Teams not yet trained on Compose
- Libraries that expose XML-only custom views
Compose vs XML Comparison
| Aspect | Compose | XML |
|---|---|---|
| Paradigm | Declarative | Imperative |
| Min API | 21 | All |
| Tooling | Preview, Layout Inspector | Layout Editor |
| Performance | Good (improving) | Mature |
| Theming | Material3 built-in | Manual theme setup |
| Testing | ComposeTestRule | Espresso |
| Interop | Can embed XML views | Can embed Compose |
| Code sharing | Easy with KMP | Limited |
---
Android: MVVM with Hilt
Project Structure
app/
├── di/ # Hilt modules
│ ├── AppModule.kt
│ ├── NetworkModule.kt
│ └── DatabaseModule.kt
├── data/
│ ├── local/ # Room database
│ │ ├── AppDatabase.kt
│ │ ├── dao/
│ │ └── entity/
│ ├── remote/ # Retrofit API
│ │ ├── ApiService.kt
│ │ └── dto/
│ └── repository/ # Repository implementations
│ └── ProductRepositoryImpl.kt
├── domain/
│ ├── model/ # Domain models
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Business logic
├── presentation/
│ ├── navigation/
│ │ └── AppNavigation.kt
│ ├── theme/
│ │ └── AppTheme.kt
│ └── features/
│ ├── home/
│ │ ├── HomeScreen.kt
│ │ └── HomeViewModel.kt
│ └── detail/
│ ├── DetailScreen.kt
│ └── DetailViewModel.kt
└── App.kt # @HiltAndroidAppHilt Dependency Injection
// di/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService =
retrofit.create(ApiService::class.java)
}
// di/DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app_db")
.fallbackToDestructiveMigration()
.build()
@Provides
fun provideProductDao(db: AppDatabase): ProductDao = db.productDao()
}ViewModel with StateFlow
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val getProductsUseCase: GetProductsUseCase,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _uiState = MutableStateFlow(ProductListUiState())
val uiState: StateFlow<ProductListUiState> = _uiState.asStateFlow()
private val _events = Channel<ProductListEvent>()
val events: Flow<ProductListEvent> = _events.receiveAsFlow()
init {
loadProducts()
}
fun loadProducts() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
getProductsUseCase()
.catch { error ->
_uiState.update {
it.copy(isLoading = false, error = error.message)
}
}
.collect { products ->
_uiState.update {
it.copy(isLoading = false, products = products, error = null)
}
}
}
}
fun onProductClick(productId: String) {
viewModelScope.launch {
_events.send(ProductListEvent.NavigateToDetail(productId))
}
}
}
data class ProductListUiState(
val products: List<Product> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
)
sealed interface ProductListEvent {
data class NavigateToDetail(val productId: String) : ProductListEvent
}---
Android: Coroutines and Flow
Structured Concurrency
// Repository with Flow
class ProductRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val productDao: ProductDao,
) : ProductRepository {
override fun getProducts(): Flow<List<Product>> = flow {
// Emit cached data first
val cached = productDao.getAll().map { it.toDomain() }
if (cached.isNotEmpty()) emit(cached)
// Fetch from network
try {
val remote = apiService.getProducts().map { it.toDomain() }
productDao.insertAll(remote.map { it.toEntity() })
emit(remote)
} catch (e: Exception) {
if (cached.isEmpty()) throw e
}
}.flowOn(Dispatchers.IO)
override suspend fun getProduct(id: String): Product =
withContext(Dispatchers.IO) {
apiService.getProduct(id).toDomain()
}
}Parallel Execution
// Parallel network calls
suspend fun loadDashboard(): Dashboard = coroutineScope {
val profileDeferred = async { userRepository.getProfile() }
val statsDeferred = async { analyticsRepository.getStats() }
val notificationsDeferred = async { notificationRepository.getRecent() }
Dashboard(
profile = profileDeferred.await(),
stats = statsDeferred.await(),
notifications = notificationsDeferred.await(),
)
}Flow Operators
// Search with debounce
class SearchViewModel @Inject constructor(
private val searchRepository: SearchRepository,
) : ViewModel() {
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
val results: StateFlow<List<SearchResult>> = _query
.debounce(300)
.distinctUntilChanged()
.filter { it.length >= 2 }
.flatMapLatest { query ->
searchRepository.search(query)
.catch { emit(emptyList()) }
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
fun onQueryChange(query: String) {
_query.value = query
}
}---
Platform-Specific UX Guidelines
iOS (Human Interface Guidelines)
| Principle | Implementation |
|---|---|
| Navigation | Tab bar for 3-5 top-level destinations; NavigationStack for hierarchical |
| Gestures | Swipe back, pull to refresh, long press for context menus |
| Typography | SF Pro, Dynamic Type support required for accessibility |
| Colors | Support Light/Dark mode; use system colors for adaptability |
| Haptics | Use for confirmations, selections, errors (UIFeedbackGenerator) |
| Spacing | 16pt margins, 8pt grid system |
| Safe areas | Always respect safe area insets (notch, home indicator) |
| Sheets | Use .sheet() for non-blocking content, .fullScreenCover() for blocking |
Android (Material Design 3)
| Principle | Implementation |
|---|---|
| Navigation | Bottom nav for 3-5 destinations; NavDrawer for 5+ |
| Gestures | Swipe to dismiss, pull to refresh, FAB for primary action |
| Typography | Roboto / Google Sans; Material type scale |
| Colors | Dynamic color (Material You) on Android 12+; fallback palette |
| Motion | Shared element transitions, container transforms |
| Spacing | 16dp margins, 4dp grid system |
| Edge-to-edge | Enable edge-to-edge display with WindowInsets handling |
| Predictive back | Support predictive back gesture on Android 14+ |
Cross-Platform Consistency Tips
- Use platform-native navigation patterns (do NOT use iOS-style tabs on Android)
- Respect platform back button behavior (hardware back on Android)
- Match platform date/time pickers
- Use platform-specific sharing intents
- Match platform notification behavior and permissions flow
---
CI/CD for Native Apps
Fastlane Configuration
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Run tests"
lane :test do
run_tests(
scheme: "MyApp",
devices: ["iPhone 15 Pro"],
clean: true,
)
end
desc "Build and upload to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "MyApp.xcodeproj")
build_app(
scheme: "MyApp",
export_method: "app-store",
)
upload_to_testflight(skip_waiting_for_build_processing: true)
slack(message: "New iOS beta uploaded to TestFlight!")
end
desc "Deploy to App Store"
lane :release do
build_app(scheme: "MyApp", export_method: "app-store")
upload_to_app_store(
force: true,
skip_screenshots: true,
submit_for_review: true,
)
end
end
# android/fastlane/Fastfile
platform :android do
desc "Run tests"
lane :test do
gradle(task: "test")
end
desc "Build and upload to Play Store internal track"
lane :beta do
gradle(task: "bundleRelease")
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab",
)
end
desc "Promote to production"
lane :release do
upload_to_play_store(
track: "production",
track_promote_to: "production",
)
end
endXcode Cloud
# ci_scripts/ci_post_clone.sh
#!/bin/sh
# Install dependencies after Xcode Cloud clones the repo
brew install swiftlintXcode Cloud workflow configuration is done via Xcode GUI:
- Start condition: Push to
mainbranch or PR - Actions: Build, Test, Archive
- Post-actions: Distribute to TestFlight
Firebase App Distribution
# .github/workflows/android-ci.yml
name: Android CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew assembleRelease
- name: Run tests
run: ./gradlew testReleaseUnitTest
- name: Upload to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIALS }}
groups: testers
file: app/build/outputs/apk/release/app-release.apkGitHub Actions for iOS
# .github/workflows/ios-ci.yml
name: iOS CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.2.app
- name: Install dependencies
run: |
gem install fastlane
bundle install
- name: Run tests
run: fastlane ios test
- name: Build for TestFlight
if: github.ref == 'refs/heads/main'
run: fastlane ios beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}CI/CD Pipeline Summary
| Stage | iOS | Android |
|---|---|---|
| Lint | SwiftLint | ktlint / detekt |
| Unit tests | XCTest | JUnit + Mockk |
| UI tests | XCUITest | Espresso / Compose UI Test |
| Build | xcodebuild / Fastlane | Gradle (AAB) |
| Sign | Match / manual certs | Keystore |
| Distribute beta | TestFlight | Firebase App Distribution |
| Distribute prod | App Store Connect | Google Play Console |
| Monitor | Xcode Organizer / Sentry | Crashlytics / Sentry |
Mobile Security Best Practices
Comprehensive security reference covering secure storage, network security, authentication, code protection, and compliance with OWASP Mobile Top 10.
---
Secure Storage
iOS: Keychain Services
The Keychain is the only secure storage on iOS. Never store secrets in UserDefaults, plist files, or plain files.
import Security
enum KeychainError: Error {
case duplicateItem, itemNotFound, unexpectedStatus(OSStatus)
}
struct KeychainManager {
static func save(key: String, data: Data, accessibility: CFString = kSecAttrAccessibleWhenUnlockedThisDeviceOnly) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: accessibility,
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
// Update existing
let updateQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
]
let attributes: [String: Any] = [kSecValueData as String: data]
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary)
guard updateStatus == errSecSuccess else {
throw KeychainError.unexpectedStatus(updateStatus)
}
} else if status != errSecSuccess {
throw KeychainError.unexpectedStatus(status)
}
}
static func read(key: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else {
throw KeychainError.itemNotFound
}
return data
}
static func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}Keychain Accessibility Levels:
| Level | When Available | Backup |
|---|---|---|
kSecAttrAccessibleWhenUnlocked | Device unlocked | Yes |
kSecAttrAccessibleWhenUnlockedThisDeviceOnly | Device unlocked | No |
kSecAttrAccessibleAfterFirstUnlock | After first unlock | Yes |
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly | After first unlock | No |
Android: EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class SecureStorage(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
fun saveToken(token: String) {
sharedPreferences.edit().putString("auth_token", token).apply()
}
fun getToken(): String? {
return sharedPreferences.getString("auth_token", null)
}
fun clear() {
sharedPreferences.edit().clear().apply()
}
}Android: Keystore System
For cryptographic keys, use the Android Keystore:
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.GCMParameterSpec
class CryptoManager {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
private fun getOrCreateKey(alias: String): javax.crypto.SecretKey {
keyStore.getKey(alias, null)?.let { return it as javax.crypto.SecretKey }
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false)
.build()
)
return keyGenerator.generateKey()
}
fun encrypt(data: ByteArray, alias: String = "default_key"): Pair<ByteArray, ByteArray> {
val key = getOrCreateKey(alias)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
return cipher.iv to cipher.doFinal(data)
}
fun decrypt(iv: ByteArray, encryptedData: ByteArray, alias: String = "default_key"): ByteArray {
val key = getOrCreateKey(alias)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
return cipher.doFinal(encryptedData)
}
}React Native Secure Storage
// Use react-native-keychain (wraps Keychain + Android Keystore)
import * as Keychain from 'react-native-keychain';
// Save credentials
await Keychain.setGenericPassword('username', 'authToken', {
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
// Read credentials
const credentials = await Keychain.getGenericPassword();
if (credentials) {
console.log(credentials.password); // authToken
}
// Delete
await Keychain.resetGenericPassword();---
Certificate Pinning
Why Certificate Pinning
Prevents man-in-the-middle attacks by validating the server's certificate against a known set of certificates or public keys, even when the device trusts a rogue CA.
iOS: URLSession Pinning
class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
private let pinnedCertificates: [SecCertificate]
init(certificateNames: [String]) {
pinnedCertificates = certificateNames.compactMap { name in
guard let url = Bundle.main.url(forResource: name, withExtension: "cer"),
let data = try? Data(contentsOf: url),
let cert = SecCertificateCreateWithData(nil, data as CFData)
else { return nil }
return cert
}
}
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Set pinned certificates as trusted anchors
SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray)
var error: CFError?
if SecTrustEvaluateWithError(serverTrust, &error) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}Android: OkHttp Certificate Pinning
val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup
.build()
)
.build()React Native (TrustKit or react-native-ssl-pinning)
import { fetch as pinnedFetch } from 'react-native-ssl-pinning';
const response = await pinnedFetch('https://api.example.com/data', {
method: 'GET',
sslPinning: {
certs: ['my_cert'], // .cer files in native assets
},
headers: {
'Content-Type': 'application/json',
},
});Certificate Pinning Best Practices
- Pin the public key (not the certificate) for easier rotation
- Include backup pins (at least 2) to avoid lockout during rotation
- Set a max-age so pins expire if not updated
- Have a remote kill switch to disable pinning if pins become invalid
- Test certificate rotation in staging before production
- Monitor pinning failures via server-side logging
---
Code Obfuscation
iOS
- Bitcode: Deprecated as of Xcode 14; no longer needed
- Symbol stripping: Enabled by default in release builds
- String encryption: Use third-party tools (iXGuard, SwiftShield)
# Build Settings (already default for Release)
STRIP_INSTALLED_PRODUCT = YES
STRIP_STYLE = all
DEPLOYMENT_POSTPROCESSING = YESAndroid: R8/ProGuard
# proguard-rules.pro
# Keep model classes (used with Gson/Moshi)
-keep class com.example.myapp.data.remote.dto.** { *; }
-keep class com.example.myapp.domain.model.** { *; }
# Keep Hilt-generated code
-keep class dagger.hilt.** { *; }
-keep class javax.inject.** { *; }
# Obfuscate everything else
-repackageclasses ''
-allowaccessmodification
-optimizations !code/simplification/arithmetic
# Remove logging
-assumenosideeffects class android.util.Log {
public static int d(...);
public static int v(...);
public static int i(...);
}React Native: Hermes Bytecode
Hermes compiles JS to bytecode, providing basic obfuscation:
// app.json
{
"expo": {
"jsEngine": "hermes"
}
}For additional protection:
- Use
react-native-obfuscating-transformerfor JS obfuscation - Enable ProGuard/R8 for the Android Java/Kotlin layer
- Strip debug symbols in release builds
---
Biometric Authentication
iOS: LocalAuthentication
import LocalAuthentication
class BiometricManager {
enum BiometricType {
case none, touchID, faceID, opticID
}
static var biometricType: BiometricType {
let context = LAContext()
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else {
return .none
}
switch context.biometryType {
case .touchID: return .touchID
case .faceID: return .faceID
case .opticID: return .opticID
default: return .none
}
}
static func authenticate(reason: String) async throws -> Bool {
let context = LAContext()
context.localizedCancelTitle = "Use Password"
return try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
)
}
}
// Usage
Task {
do {
let success = try await BiometricManager.authenticate(
reason: "Authenticate to access your account"
)
if success { /* unlock */ }
} catch {
// Fall back to password
}
}Info.plist requirement:
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to securely authenticate you.</string>Android: BiometricPrompt
import androidx.biometric.BiometricPrompt
class BiometricHelper(
private val activity: FragmentActivity,
private val onSuccess: () -> Unit,
private val onError: (String) -> Unit,
) {
private val executor = ContextCompat.getMainExecutor(activity)
private val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onSuccess()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onError(errString.toString())
}
override fun onAuthenticationFailed() {
onError("Authentication failed")
}
}
fun authenticate() {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication")
.setSubtitle("Verify your identity")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
.build()
BiometricPrompt(activity, executor, callback).authenticate(promptInfo)
}
}React Native: expo-local-authentication
import * as LocalAuthentication from 'expo-local-authentication';
async function authenticateWithBiometrics(): Promise<boolean> {
// Check hardware support
const hasHardware = await LocalAuthentication.hasHardwareAsync();
if (!hasHardware) return false;
// Check if biometrics are enrolled
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!isEnrolled) return false;
// Authenticate
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Verify your identity',
cancelLabel: 'Use Password',
disableDeviceFallback: false,
});
return result.success;
}---
Network Security Configuration
Android: network_security_config.xml
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Disable cleartext traffic globally -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- Pin certificates for your API domain -->
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2026-12-31">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
<!-- Allow cleartext for local development only -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config><!-- AndroidManifest.xml -->
<application
android:networkSecurityConfig="@xml/network_security_config"
...>iOS: App Transport Security (ATS)
ATS is enabled by default. Avoid disabling it globally.
<!-- Info.plist - only add exceptions for specific domains -->
<key>NSAppTransportSecurity</key>
<dict>
<!-- Do NOT set NSAllowsArbitraryLoads to true -->
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.example.com</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>Secure API Communication Checklist
- [ ] All traffic over HTTPS (TLS 1.2 minimum, prefer 1.3)
- [ ] Certificate pinning for primary API domains
- [ ] Token-based authentication (JWT with short expiry)
- [ ] Refresh tokens stored in secure storage (Keychain / Keystore)
- [ ] Request signing for sensitive operations
- [ ] Rate limiting awareness (handle 429 responses gracefully)
- [ ] No sensitive data in URL query parameters (use POST body or headers)
- [ ] Timeouts configured to prevent hanging connections
---
OWASP Mobile Top 10 (2024)
M1: Improper Credential Usage
Risk: Hardcoded credentials, insecure token storage, weak authentication.
Mitigations:
- Never hardcode API keys, secrets, or passwords in source code
- Store credentials in Keychain (iOS) or Keystore (Android)
- Use short-lived tokens with refresh mechanism
- Implement token revocation on logout
- Use certificate pinning for credential transmission
M2: Inadequate Supply Chain Security
Risk: Vulnerable or malicious third-party libraries.
Mitigations:
- Audit dependencies regularly (
npm audit,bundle audit,dependabot) - Lock dependency versions (lockfiles)
- Use only well-maintained, widely-adopted libraries
- Review library permissions and code for suspicious behavior
- Enable integrity checks (npm
package-lock.jsonintegrity field)
M3: Insecure Authentication/Authorization
Risk: Weak login flows, missing session management, client-side auth bypasses.
Mitigations:
- Perform all authorization checks server-side
- Implement proper session timeout and refresh
- Use multi-factor authentication for sensitive operations
- Lock accounts after failed attempts
- Never trust client-side validation alone
M4: Insufficient Input/Output Validation
Risk: Injection attacks, XSS via WebViews, data corruption.
Mitigations:
- Validate and sanitize all user inputs
- Use parameterized queries for local databases (Room, Core Data)
- Sanitize HTML content before rendering in WebViews
- Validate deep link parameters before processing
- Implement input length limits
M5: Insecure Communication
Risk: Cleartext traffic, missing certificate validation, data interception.
Mitigations:
- Enforce HTTPS everywhere (ATS on iOS, network_security_config on Android)
- Implement certificate pinning
- Do not disable SSL/TLS validation (even in development)
- Avoid transmitting sensitive data in URLs
- Use end-to-end encryption for highly sensitive data
M6: Inadequate Privacy Controls
Risk: Excessive data collection, missing consent, data leakage.
Mitigations:
- Collect only necessary data (data minimization)
- Implement proper consent flows (GDPR, CCPA)
- Provide data deletion capability
- Clear sensitive data from clipboard, screenshots, and logs
- Disable screenshots for sensitive screens:
// iOS: Hide content in app switcher
func sceneWillResignActive(_ scene: UIScene) {
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
blurView.tag = 999
window?.addSubview(blurView)
blurView.frame = window?.bounds ?? .zero
}
func sceneDidBecomeActive(_ scene: UIScene) {
window?.viewWithTag(999)?.removeFromSuperview()
}// Android: Prevent screenshots
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)M7: Insufficient Binary Protections
Risk: Reverse engineering, code tampering, repackaging.
Mitigations:
- Enable code obfuscation (R8/ProGuard on Android)
- Strip debug symbols in release builds
- Implement runtime integrity checks (jailbreak/root detection)
- Use tamper detection libraries
- Avoid storing business logic exclusively on the client
M8: Security Misconfiguration
Risk: Debug features in production, exposed admin endpoints, default credentials.
Mitigations:
- Remove all debug logs and test code before release
- Disable WebView debugging in production
- Review exported components and content providers (Android)
- Set
android:debuggable="false"in release builds - Review URL schemes for hijacking potential
M9: Insecure Data Storage
Risk: Sensitive data in plaintext files, logs, backups, or caches.
Mitigations:
- Use encrypted storage for all sensitive data
- Exclude sensitive files from device backups
- Clear sensitive data from memory when no longer needed
- Disable keyboard caching for sensitive input fields
- Review application logs for sensitive data exposure
// iOS: Exclude from backup
var url = getDocumentsDirectory().appendingPathComponent("sensitive.dat")
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
try url.setResourceValues(resourceValues)// Android: Exclude from backup
// AndroidManifest.xml
android:allowBackup="false"
android:fullBackupContent="@xml/backup_rules"
// res/xml/backup_rules.xml
<full-backup-content>
<exclude domain="sharedpref" path="secure_prefs.xml" />
<exclude domain="database" path="sensitive.db" />
</full-backup-content>M10: Insufficient Cryptography
Risk: Weak algorithms, hardcoded keys, improper key management.
Mitigations:
- Use platform cryptography (CommonCrypto on iOS, Android Keystore)
- Never implement custom cryptographic algorithms
- Use AES-256-GCM for symmetric encryption
- Use RSA-2048+ or ECDSA for asymmetric encryption
- Store cryptographic keys in hardware-backed storage (Secure Enclave / StrongBox)
- Never hardcode encryption keys in source code
- Rotate keys periodically
---
Security Audit Checklist
Pre-Release Security Review
- [ ] No hardcoded secrets in source code (scan with tools like
gitleaks) - [ ] All API keys stored in environment variables / secure config
- [ ] Certificate pinning implemented and tested
- [ ] Biometric authentication tested on real devices
- [ ] Sensitive screens prevent screenshots
- [ ] Debug logging removed from release builds
- [ ] ProGuard/R8 enabled for Android release
- [ ] ATS not globally disabled on iOS
- [ ] Network security config restricts cleartext traffic
- [ ] Secure storage used for tokens and credentials
- [ ] Input validation on all user-provided data
- [ ] Deep link parameters validated and sanitized
- [ ] Backup exclusions configured for sensitive data
- [ ] Third-party dependencies audited for vulnerabilities
- [ ] Root/jailbreak detection implemented (if required)
- [ ] SSL/TLS minimum version set to 1.2
- [ ] Session timeout and token refresh implemented
- [ ] Privacy policy and data handling documented
- [ ] Penetration testing completed for critical flows
React Native Patterns & Best Practices
Comprehensive reference for production React Native development covering navigation, state management, performance, testing, native modules, and OTA updates.
---
Navigation Patterns
Stack Navigation (Expo Router)
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack
screenOptions={{
headerStyle: { backgroundColor: '#f5f5f5' },
headerTintColor: '#333',
animation: 'slide_from_right',
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="auth" options={{ headerShown: false }} />
<Stack.Screen name="[id]" options={{ title: 'Details' }} />
<Stack.Screen
name="modal"
options={{ presentation: 'modal', headerShown: true }}
/>
</Stack>
);
}Tab Navigation
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Platform } from 'react-native';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#007AFF',
tabBarInactiveTintColor: '#8E8E93',
tabBarStyle: {
height: Platform.OS === 'ios' ? 88 : 60,
paddingBottom: Platform.OS === 'ios' ? 28 : 8,
},
headerShown: true,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="search"
options={{
title: 'Search',
tabBarIcon: ({ color, size }) => (
<Ionicons name="search" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}Drawer Navigation
// Using @react-navigation/drawer
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function DrawerNavigator() {
return (
<Drawer.Navigator
screenOptions={{
drawerPosition: 'left',
drawerType: 'front',
headerShown: true,
drawerStyle: { width: 280 },
}}
>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Settings" component={SettingsScreen} />
<Drawer.Screen name="About" component={AboutScreen} />
</Drawer.Navigator>
);
}Deep Linking Configuration
// app.json or app.config.ts
{
"expo": {
"scheme": "myapp",
"plugins": ["expo-router"],
"experiments": {
"typedRoutes": true
}
}
}
// Handling deep links in Expo Router:
// myapp://profile/123 -> app/profile/[id].tsx
// https://myapp.com/profile/123 -> requires universal links configNavigation Guards / Auth Protection
// app/_layout.tsx with auth guard
import { useEffect } from 'react';
import { useRouter, useSegments } from 'expo-router';
import { useAuthStore } from '@/stores/authStore';
function useProtectedRoute() {
const segments = useSegments();
const router = useRouter();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
useEffect(() => {
const inAuthGroup = segments[0] === 'auth';
if (!isAuthenticated && !inAuthGroup) {
router.replace('/auth/login');
} else if (isAuthenticated && inAuthGroup) {
router.replace('/');
}
}, [isAuthenticated, segments]);
}---
State Management Comparison
Zustand (Recommended for most projects)
| Aspect | Details |
|---|---|
| Bundle size | ~1 KB |
| Learning curve | Minimal - familiar hooks API |
| Boilerplate | Very low |
| DevTools | Zustand/devtools middleware |
| Best for | Small-medium apps, rapid prototyping |
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item],
})),
removeItem: (id) => set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}),
{
name: 'cart-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);Redux Toolkit
| Aspect | Details |
|---|---|
| Bundle size | ~11 KB (+ React-Redux) |
| Learning curve | Moderate - actions, reducers, thunks |
| Boilerplate | Medium (much less than classic Redux) |
| DevTools | Excellent (Redux DevTools, Flipper) |
| Best for | Large apps, complex data flows, teams |
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit';
export const fetchProducts = createAsyncThunk(
'products/fetch',
async (_, { rejectWithValue }) => {
try {
const response = await api.get('/products');
return response.data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const productsSlice = createSlice({
name: 'products',
initialState: { items: [], loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => { state.loading = true; })
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});Jotai (Atomic approach)
| Aspect | Details |
|---|---|
| Bundle size | ~3 KB |
| Learning curve | Low - atom-based thinking |
| Boilerplate | Very low |
| DevTools | Jotai DevTools |
| Best for | Derived state, fine-grained reactivity |
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import AsyncStorage from '@react-native-async-storage/async-storage';
const storage = createJSONStorage(() => AsyncStorage);
// Base atoms
const userAtom = atomWithStorage('user', null, storage);
const themeAtom = atomWithStorage('theme', 'light', storage);
// Derived atom
const isDarkAtom = atom((get) => get(themeAtom) === 'dark');
// Async atom
const productsAtom = atom(async () => {
const response = await fetch('https://api.example.com/products');
return response.json();
});MobX
| Aspect | Details |
|---|---|
| Bundle size | ~16 KB |
| Learning curve | Moderate - observable/observer patterns |
| Boilerplate | Low with decorators |
| DevTools | MobX DevTools |
| Best for | OOP-style state, complex domains |
State Management Decision Matrix
| Factor | Zustand | Redux | Jotai | MobX |
|---|---|---|---|---|
| App complexity | S-M | M-L | S-M | M-L |
| Team size | 1-5 | 5+ | 1-5 | 3-10 |
| TS support | Good | Excellent | Good | Good |
| Persistence | Easy | Moderate | Easy | Moderate |
| Server state | TanStack Query | RTK Query | TanStack Query | Built-in |
---
Performance Optimization Checklist
Rendering Performance
- [ ] Use
React.memo()for components that receive stable props - [ ] Use
useCallback()for event handlers passed as props - [ ] Use
useMemo()for expensive computations - [ ] Use
FlatListorFlashListinstead ofScrollView+.map()for lists - [ ] Set
keyExtractoron all FlatList components - [ ] Provide
getItemLayoutwhen item heights are fixed - [ ] Use
windowSize,initialNumToRender,maxToRenderPerBatchon FlatList - [ ] Avoid inline style objects (use
StyleSheet.create) - [ ] Avoid inline arrow functions in render (move to component body with useCallback)
- [ ] Use
removeClippedSubviews={true}for long lists
Image Optimization
- [ ] Use WebP format over PNG/JPEG where possible
- [ ] Implement progressive loading with placeholder images
- [ ] Use
expo-imageorreact-native-fast-imagefor caching - [ ] Set explicit
widthandheighton images - [ ] Use
resizeMode="cover"or"contain"appropriately - [ ] Lazy-load off-screen images
Animation Performance
- [ ] Use
react-native-reanimatedfor JS-driven animations (runs on UI thread) - [ ] Use
LayoutAnimationfor simple layout transitions - [ ] Avoid animating
width/height- usetransforminstead - [ ] Set
useNativeDriver: truewhen using Animated API - [ ] Batch state updates that trigger animations
Memory Management
- [ ] Clean up event listeners in useEffect cleanup
- [ ] Cancel subscriptions, timers, and intervals on unmount
- [ ] Avoid memory leaks from uncancelled async operations
- [ ] Use AbortController for fetch requests
- [ ] Dispose of large data structures when screens unmount
Bundle Size
- [ ] Use
import x from 'lodash/x'instead ofimport { x } from 'lodash' - [ ] Remove unused dependencies
- [ ] Replace
moment.jswithdate-fnsordayjs - [ ] Use tree-shakeable libraries
- [ ] Analyze bundle with
npx react-native-bundle-visualizer
Startup Performance
- [ ] Use Hermes engine (enabled by default in Expo SDK 50+)
- [ ] Lazy-load screens with
React.lazy()+Suspense - [ ] Defer non-critical initialization
- [ ] Use
SplashScreen.preventAutoHideAsync()to control splash timing
---
Testing Strategy
Unit Testing (Jest)
// __tests__/stores/cartStore.test.ts
import { useCartStore } from '@/stores/cartStore';
import { act, renderHook } from '@testing-library/react-hooks';
describe('CartStore', () => {
beforeEach(() => {
useCartStore.setState({ items: [] });
});
it('adds item to cart', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Widget', price: 9.99 });
});
expect(result.current.items).toHaveLength(1);
expect(result.current.items[0].name).toBe('Widget');
});
});Component Testing (React Native Testing Library)
// __tests__/components/Button.test.tsx
import { render, fireEvent, screen } from '@testing-library/react-native';
import { Button } from '@/components/ui/Button';
describe('Button', () => {
it('renders title correctly', () => {
render(<Button title="Submit" onPress={() => {}} />);
expect(screen.getByText('Submit')).toBeTruthy();
});
it('calls onPress when pressed', () => {
const onPress = jest.fn();
render(<Button title="Submit" onPress={onPress} />);
fireEvent.press(screen.getByText('Submit'));
expect(onPress).toHaveBeenCalledTimes(1);
});
it('does not call onPress when disabled', () => {
const onPress = jest.fn();
render(<Button title="Submit" onPress={onPress} disabled />);
fireEvent.press(screen.getByText('Submit'));
expect(onPress).not.toHaveBeenCalled();
});
});E2E Testing (Detox)
// e2e/login.test.ts
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should show login screen', async () => {
await expect(element(by.text('Login'))).toBeVisible();
});
it('should login with valid credentials', async () => {
await element(by.id('email-input')).typeText('user@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await expect(element(by.text('Welcome Home'))).toBeVisible();
});
});Testing Coverage Targets
| Layer | Target | Tool |
|---|---|---|
| Unit (utils, hooks, stores) | 80%+ | Jest |
| Component | 70%+ | React Native Testing Library |
| Integration | Critical paths | Jest + RNTL |
| E2E | Happy paths | Detox or Maestro |
| Visual regression | Key screens | Storybook + Chromatic |
---
Native Module Integration
Expo Modules (Recommended)
// modules/my-native-module/index.ts
import { requireNativeModule } from 'expo-modules-core';
const MyModule = requireNativeModule('MyNativeModule');
export function doNativeWork(input: string): Promise<string> {
return MyModule.doWork(input);
}Turbo Modules (React Native 0.68+)
// NativeMyModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
multiply(a: number, b: number): Promise<number>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('MyModule');Common Native Integrations
| Feature | Package | Notes |
|---|---|---|
| Camera | expo-camera | Permissions required |
| File System | expo-file-system | Read/write local files |
| Notifications | expo-notifications | Push + local |
| Location | expo-location | Foreground + background |
| Biometrics | expo-local-authentication | Face ID / Fingerprint |
| Haptics | expo-haptics | Tactile feedback |
| In-App Purchases | react-native-iap | Both stores |
| Maps | react-native-maps | Google Maps + Apple Maps |
---
CodePush / OTA Updates
EAS Update (Expo)
# Install EAS CLI
npm install -g eas-cli
# Configure
eas update:configure
# Create update
eas update --branch production --message "Bug fix for login"
# Check update status
eas update:listUpdate Strategy
| Channel | Branch | Auto-update | Use case |
|---|---|---|---|
| production | main | On app launch | Stable releases |
| staging | develop | Immediate | QA testing |
| preview | feature/* | Manual | Feature review |
CodePush (Microsoft) - Alternative
import codePush from 'react-native-code-push';
const codePushOptions = {
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.ON_NEXT_RESUME,
minimumBackgroundDuration: 60 * 5, // 5 minutes
};
function App() {
return <MainNavigator />;
}
export default codePush(codePushOptions)(App);OTA Update Best Practices
1. Never OTA native code changes - only JS bundle + assets 2. Use staged rollouts - 10% -> 50% -> 100% 3. Include rollback mechanism - revert to previous bundle on crash 4. Monitor crash rates after each update 5. Keep updates small - diff-based updates are faster 6. Test on real devices before pushing to production 7. Set minimum app version - ensure native compatibility 8. Use update channels for staging vs production
---
Project Configuration Best Practices
ESLint Configuration
{
"extends": [
"expo",
"@react-native",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"react-native/no-inline-styles": "warn",
"react-native/no-unused-styles": "error",
"react-hooks/exhaustive-deps": "warn",
"no-console": "warn"
}
}Environment Variables
// app.config.ts
export default {
expo: {
extra: {
apiUrl: process.env.API_URL ?? 'https://api.example.com',
environment: process.env.APP_ENV ?? 'development',
},
},
};
// Usage
import Constants from 'expo-constants';
const { apiUrl } = Constants.expoConfig.extra;Error Boundaries
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<View style={styles.container}>
<Text style={styles.title}>Something went wrong</Text>
<Text style={styles.message}>{error.message}</Text>
<Button title="Try Again" onPress={resetErrorBoundary} />
</View>
);
}
// Wrap your app
<ErrorBoundary FallbackComponent={ErrorFallback}>
<App />
</ErrorBoundary>---
API Layer Pattern
Typed API Client
// services/api.ts
import { useAuthStore } from '@/stores/authStore';
const BASE_URL = 'https://api.example.com';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
async function request<T>(
endpoint: string,
method: HttpMethod = 'GET',
body?: unknown,
signal?: AbortSignal,
): Promise<T> {
const token = useAuthStore.getState().token;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${BASE_URL}${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal,
});
if (response.status === 401) {
useAuthStore.getState().logout();
throw new Error('Session expired');
}
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
export const api = {
get: <T>(endpoint: string, signal?: AbortSignal) =>
request<T>(endpoint, 'GET', undefined, signal),
post: <T>(endpoint: string, body: unknown) =>
request<T>(endpoint, 'POST', body),
put: <T>(endpoint: string, body: unknown) =>
request<T>(endpoint, 'PUT', body),
delete: <T>(endpoint: string) =>
request<T>(endpoint, 'DELETE'),
};React Query / TanStack Query Integration
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/services/api';
export function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: ({ signal }) => api.get<Product[]>('/products', signal),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (product: CreateProductDTO) =>
api.post<Product>('/products', product),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}#!/usr/bin/env python3
"""
Mobile App Performance Analyzer
================================
CLI tool that analyzes a mobile project for common performance issues.
Checks performed:
- Large image assets (unoptimized PNGs/JPGs)
- Unnecessary re-render patterns (React Native)
- Memory leak patterns (missing cleanup, uncancelled subscriptions)
- Bundle size estimation
- Platform-specific anti-patterns
Supported platforms: react-native, flutter, ios-native, android-native
Usage:
python app_performance_analyzer.py /path/to/project
python app_performance_analyzer.py /path/to/project --platform react-native
python app_performance_analyzer.py /path/to/project --json
Dependencies: Python 3.8+ standard library only.
"""
import argparse
import json
import os
import re
import sys
from collections import defaultdict
from datetime import datetime
from pathlib import Path
# ---------------------------------------------------------------------------
# Severity levels
# ---------------------------------------------------------------------------
SEVERITY_CRITICAL = "critical"
SEVERITY_WARNING = "warning"
SEVERITY_INFO = "info"
SEVERITY_WEIGHT = {
SEVERITY_CRITICAL: 3,
SEVERITY_WARNING: 2,
SEVERITY_INFO: 1,
}
# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------
def detect_platform(project_dir: Path) -> str:
"""Auto-detect the mobile platform based on project files."""
if (project_dir / "pubspec.yaml").exists():
return "flutter"
if (project_dir / "package.json").exists():
pkg_file = project_dir / "package.json"
try:
content = pkg_file.read_text()
if "react-native" in content or "expo" in content:
return "react-native"
except Exception:
pass
if (project_dir / "app" / "build.gradle").exists() or (project_dir / "app" / "build.gradle.kts").exists():
return "android-native"
# Look for .xcodeproj or Swift files
for child in project_dir.iterdir():
if child.suffix == ".xcodeproj" or child.suffix == ".xcworkspace":
return "ios-native"
swift_files = list(project_dir.rglob("*.swift"))
if swift_files:
return "ios-native"
return "unknown"
# ---------------------------------------------------------------------------
# Image asset analysis
# ---------------------------------------------------------------------------
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".gif", ".webp"}
# Thresholds in bytes
IMAGE_SIZE_WARNING = 500 * 1024 # 500 KB
IMAGE_SIZE_CRITICAL = 1024 * 1024 # 1 MB
def analyze_images(project_dir: Path) -> list:
"""Find oversized image assets."""
issues = []
total_image_bytes = 0
image_count = 0
for root, dirs, files in os.walk(project_dir):
# Skip build and dependency directories
dirs[:] = [d for d in dirs if d not in {
"node_modules", ".gradle", "build", "Pods",
"DerivedData", ".dart_tool", ".expo", ".git",
"__pycache__", ".next",
}]
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in IMAGE_EXTENSIONS:
continue
fpath = Path(root) / fname
try:
size = fpath.stat().st_size
except OSError:
continue
image_count += 1
total_image_bytes += size
rel = fpath.relative_to(project_dir)
if size >= IMAGE_SIZE_CRITICAL:
issues.append({
"category": "image_assets",
"severity": SEVERITY_CRITICAL,
"file": str(rel),
"message": f"Very large image ({_fmt_bytes(size)}). Consider compressing or converting to WebP.",
"size_bytes": size,
})
elif size >= IMAGE_SIZE_WARNING:
issues.append({
"category": "image_assets",
"severity": SEVERITY_WARNING,
"file": str(rel),
"message": f"Large image ({_fmt_bytes(size)}). Consider optimizing.",
"size_bytes": size,
})
# Summary
if total_image_bytes > 5 * 1024 * 1024:
issues.append({
"category": "image_assets",
"severity": SEVERITY_WARNING,
"file": None,
"message": f"Total image assets: {_fmt_bytes(total_image_bytes)} across {image_count} files. Consider using a CDN or lazy loading.",
"size_bytes": total_image_bytes,
})
return issues
def _fmt_bytes(size: int) -> str:
if size >= 1024 * 1024:
return f"{size / (1024 * 1024):.1f} MB"
elif size >= 1024:
return f"{size / 1024:.1f} KB"
return f"{size} B"
# ---------------------------------------------------------------------------
# React Native specific checks
# ---------------------------------------------------------------------------
# Patterns that suggest re-render issues
RN_RERENDER_PATTERNS = [
(r"<FlatList[^>]*(?!keyExtractor)", "FlatList without keyExtractor causes full re-renders"),
(r"style=\{\{", "Inline style objects create new references each render - use StyleSheet.create"),
(r"onPress=\{\(\)\s*=>", "Inline arrow function in onPress creates new reference each render - use useCallback"),
(r"\.map\(.*=>\s*<", "Using .map() to render lists instead of FlatList/SectionList may cause perf issues with large data"),
]
RN_MEMORY_PATTERNS = [
(r"addEventListener\(", "Event listener added - ensure it is removed in cleanup/useEffect return"),
(r"setInterval\(", "setInterval detected - ensure clearInterval on unmount"),
(r"setTimeout\(", "setTimeout detected - ensure clearTimeout on unmount"),
(r"new WebSocket\(", "WebSocket created - ensure close() on component unmount"),
(r"Animated\.loop\(", "Animated.loop detected - ensure animation is stopped on unmount"),
]
RN_PERF_PATTERNS = [
(r"console\.(log|warn|error|info|debug)\(", "console.log statements left in code - remove for production"),
(r"import\s+\{[^}]+\}\s+from\s+['\"]lodash['\"]", "Full lodash import - use lodash/specific-function for smaller bundles"),
(r"import\s+moment\s+from", "moment.js is large (300KB+) - consider date-fns or dayjs"),
(r"JSON\.parse\(JSON\.stringify\(", "Deep clone via JSON - consider structuredClone or specific copy"),
]
def analyze_react_native(project_dir: Path) -> list:
"""React Native specific performance analysis."""
issues = []
source_extensions = {".js", ".jsx", ".ts", ".tsx"}
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in {"node_modules", ".expo", ".git", "build", "__tests__"}]
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in source_extensions:
continue
fpath = Path(root) / fname
rel = str(fpath.relative_to(project_dir))
try:
content = fpath.read_text(errors="replace")
except OSError:
continue
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
for pattern, msg in RN_RERENDER_PATTERNS:
if re.search(pattern, line):
issues.append({
"category": "re_renders",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": msg,
})
for pattern, msg in RN_MEMORY_PATTERNS:
if re.search(pattern, line):
issues.append({
"category": "memory_leaks",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": msg,
})
for pattern, msg in RN_PERF_PATTERNS:
if re.search(pattern, line):
issues.append({
"category": "performance",
"severity": SEVERITY_INFO if "console" in pattern else SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": msg,
})
# FlatList optimization analysis (multi-line: check full component props)
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in {"node_modules", ".expo", ".git", "build", "__tests__"}]
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in source_extensions:
continue
fpath = Path(root) / fname
rel = str(fpath.relative_to(project_dir))
try:
content = fpath.read_text(errors="replace")
except OSError:
continue
# Find FlatList usages and check for optimization props
flatlist_matches = list(re.finditer(r"<FlatList\b", content))
for match in flatlist_matches:
start = match.start()
# Extract a reasonable chunk after <FlatList to capture its props
chunk_end = min(start + 1500, len(content))
chunk = content[start:chunk_end]
# Find closing > or /> to scope the props
close = re.search(r"/?>", chunk)
if close:
chunk = chunk[: close.end()]
line_num = content[:start].count("\n") + 1
if "getItemLayout" not in chunk:
issues.append({
"category": "flatlist_optimization",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": "FlatList missing getItemLayout - add it for fixed-height items to skip measurement.",
})
if "windowSize" not in chunk:
issues.append({
"category": "flatlist_optimization",
"severity": SEVERITY_INFO,
"file": rel,
"line": line_num,
"message": "FlatList missing windowSize prop - set windowSize={5} to reduce off-screen rendering.",
})
if "keyExtractor" not in chunk:
issues.append({
"category": "flatlist_optimization",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": "FlatList missing keyExtractor - required for efficient re-rendering.",
})
# Check for missing useCallback/useMemo in components with FlatList
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in {"node_modules", ".expo", ".git", "build", "__tests__"}]
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in source_extensions:
continue
fpath = Path(root) / fname
rel = str(fpath.relative_to(project_dir))
try:
content = fpath.read_text(errors="replace")
except OSError:
continue
# If file contains FlatList but no useCallback, flag it
if "<FlatList" in content:
if "useCallback" not in content:
issues.append({
"category": "re_renders",
"severity": SEVERITY_WARNING,
"file": rel,
"message": "File uses FlatList but does not import useCallback - wrap renderItem and handlers in useCallback.",
})
if "useMemo" not in content and "React.memo" not in content:
issues.append({
"category": "re_renders",
"severity": SEVERITY_INFO,
"file": rel,
"message": "File uses FlatList but does not use useMemo or React.memo - consider memoizing list items.",
})
# Check package.json for heavy dependencies
pkg_path = project_dir / "package.json"
if pkg_path.exists():
try:
pkg = json.loads(pkg_path.read_text())
deps = {}
deps.update(pkg.get("dependencies", {}))
deps.update(pkg.get("devDependencies", {}))
heavy_deps = {
"moment": "moment is ~300KB. Use date-fns or dayjs instead.",
"lodash": "Full lodash is ~70KB. Use lodash-es or specific imports.",
"axios": "axios is ~13KB. fetch() is built-in for React Native.",
"react-native-elements": "Heavy UI lib. Consider react-native-paper or custom components.",
}
for dep, msg in heavy_deps.items():
if dep in deps:
issues.append({
"category": "bundle_size",
"severity": SEVERITY_INFO,
"file": "package.json",
"message": msg,
})
except (json.JSONDecodeError, OSError):
pass
return issues
# ---------------------------------------------------------------------------
# Flutter specific checks
# ---------------------------------------------------------------------------
FLUTTER_PERF_PATTERNS = [
(r"setState\(\s*\(\)\s*\{", "setState can cause full widget rebuild - consider using ValueNotifier or Riverpod for granular updates"),
(r"print\(", "print() statements left in code - remove for production or use debugPrint"),
(r"Column\(\s*children:\s*\[.*ListView", "ListView inside Column without Expanded/shrinkWrap causes layout issues"),
(r"Image\.network\(", "Image.network without caching - consider cached_network_image package"),
]
FLUTTER_MEMORY_PATTERNS = [
(r"StreamSubscription", "StreamSubscription detected - ensure cancel() in dispose()"),
(r"AnimationController", "AnimationController detected - ensure dispose() is called"),
(r"TextEditingController", "TextEditingController detected - ensure dispose() is called"),
(r"ScrollController", "ScrollController detected - ensure dispose() is called"),
(r"FocusNode", "FocusNode detected - ensure dispose() is called"),
]
def analyze_flutter(project_dir: Path) -> list:
"""Flutter specific performance analysis."""
issues = []
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in {".dart_tool", "build", ".git", ".idea"}]
for fname in files:
if not fname.endswith(".dart"):
continue
fpath = Path(root) / fname
rel = str(fpath.relative_to(project_dir))
try:
content = fpath.read_text(errors="replace")
except OSError:
continue
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
for pattern, msg in FLUTTER_PERF_PATTERNS:
if re.search(pattern, line):
issues.append({
"category": "performance",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": msg,
})
for pattern, msg in FLUTTER_MEMORY_PATTERNS:
if re.search(pattern, line):
issues.append({
"category": "memory_leaks",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": msg,
})
# Check pubspec for heavy packages
pubspec = project_dir / "pubspec.yaml"
if pubspec.exists():
try:
content = pubspec.read_text()
if "flutter_svg" not in content and re.search(r"\.svg", content):
issues.append({
"category": "performance",
"severity": SEVERITY_INFO,
"file": "pubspec.yaml",
"message": "SVG files detected but flutter_svg not in dependencies.",
})
except OSError:
pass
return issues
# ---------------------------------------------------------------------------
# iOS Native checks
# ---------------------------------------------------------------------------
IOS_PATTERNS = [
(r"DispatchQueue\.main\.async", "Check that heavy work is not dispatched to main queue"),
(r"UIImage\(named:", "UIImage(named:) caches images in memory - use UIImage(contentsOfFile:) for large images"),
(r"NotificationCenter\.default\.addObserver", "Observer added - ensure removeObserver in deinit"),
(r"Timer\.scheduledTimer", "Timer created - ensure invalidate() in deinit/disappear"),
(r"print\(", "print() statements left in code - remove for production"),
(r"force\s+try", "Force try detected - handle errors gracefully"),
(r"as!", "Force cast detected - use optional binding (as?) instead"),
]
def analyze_ios(project_dir: Path) -> list:
"""iOS native performance analysis."""
issues = []
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in {"Pods", "DerivedData", ".git", "build", "xcuserdata"}]
for fname in files:
if not fname.endswith(".swift"):
continue
fpath = Path(root) / fname
rel = str(fpath.relative_to(project_dir))
try:
content = fpath.read_text(errors="replace")
except OSError:
continue
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
for pattern, msg in IOS_PATTERNS:
if re.search(pattern, line):
issues.append({
"category": "performance",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": msg,
})
return issues
# ---------------------------------------------------------------------------
# Android Native checks
# ---------------------------------------------------------------------------
ANDROID_PATTERNS = [
(r"runOnUiThread", "Heavy work on UI thread detected - use coroutines or background thread"),
(r"Log\.(d|v|i|w|e)\(", "Log statements left in code - remove for production builds or use Timber"),
(r"GlobalScope\.launch", "GlobalScope causes potential leaks - use viewModelScope or lifecycleScope"),
(r"Thread\(\)", "Raw Thread usage - prefer Coroutines for structured concurrency"),
(r"BitmapFactory\.decodeFile", "BitmapFactory without inSampleSize may cause OOM - use Coil or Glide"),
(r"registerReceiver\(", "BroadcastReceiver registered - ensure unregisterReceiver in onDestroy/onPause"),
]
def analyze_android(project_dir: Path) -> list:
"""Android native performance analysis."""
issues = []
kt_extensions = {".kt", ".java"}
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in {".gradle", "build", ".git", ".idea"}]
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in kt_extensions:
continue
fpath = Path(root) / fname
rel = str(fpath.relative_to(project_dir))
try:
content = fpath.read_text(errors="replace")
except OSError:
continue
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
for pattern, msg in ANDROID_PATTERNS:
if re.search(pattern, line):
issues.append({
"category": "performance",
"severity": SEVERITY_WARNING,
"file": rel,
"line": line_num,
"message": msg,
})
return issues
# ---------------------------------------------------------------------------
# Bundle size estimation
# ---------------------------------------------------------------------------
SKIP_DIRS = {
"node_modules", ".gradle", "build", "Pods", "DerivedData",
".dart_tool", ".expo", ".git", "__pycache__", ".next", ".idea",
"xcuserdata",
}
CODE_EXTENSIONS = {
".js", ".jsx", ".ts", ".tsx", ".dart", ".swift", ".kt", ".java",
".m", ".mm", ".h", ".c", ".cpp",
}
def estimate_bundle_size(project_dir: Path) -> dict:
"""Estimate the source code size (not final bundle, but a rough indicator)."""
total_code_bytes = 0
total_asset_bytes = 0
total_other_bytes = 0
code_files = 0
asset_files = 0
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for fname in files:
fpath = Path(root) / fname
try:
size = fpath.stat().st_size
except OSError:
continue
ext = os.path.splitext(fname)[1].lower()
if ext in CODE_EXTENSIONS:
total_code_bytes += size
code_files += 1
elif ext in IMAGE_EXTENSIONS or ext in {".mp4", ".mp3", ".wav", ".ttf", ".otf", ".json"}:
total_asset_bytes += size
asset_files += 1
else:
total_other_bytes += size
return {
"source_code_size": _fmt_bytes(total_code_bytes),
"source_code_bytes": total_code_bytes,
"source_code_files": code_files,
"asset_size": _fmt_bytes(total_asset_bytes),
"asset_bytes": total_asset_bytes,
"asset_files": asset_files,
"other_size": _fmt_bytes(total_other_bytes),
"other_bytes": total_other_bytes,
"total_size": _fmt_bytes(total_code_bytes + total_asset_bytes + total_other_bytes),
"total_bytes": total_code_bytes + total_asset_bytes + total_other_bytes,
}
# ---------------------------------------------------------------------------
# Report generation
# ---------------------------------------------------------------------------
def compute_score(issues: list) -> int:
"""Compute a performance score from 0-100."""
base = 100
for issue in issues:
weight = SEVERITY_WEIGHT.get(issue.get("severity", SEVERITY_INFO), 1)
base -= weight
return max(0, min(100, base))
def generate_report(project_dir: Path, platform: str, json_output: bool):
"""Run all analyses and produce a report."""
all_issues = []
# Image analysis (all platforms)
all_issues.extend(analyze_images(project_dir))
# Platform-specific analysis
if platform == "react-native":
all_issues.extend(analyze_react_native(project_dir))
elif platform == "flutter":
all_issues.extend(analyze_flutter(project_dir))
elif platform == "ios-native":
all_issues.extend(analyze_ios(project_dir))
elif platform == "android-native":
all_issues.extend(analyze_android(project_dir))
# Bundle size estimation
bundle_info = estimate_bundle_size(project_dir)
# Categorize
by_category = defaultdict(list)
by_severity = defaultdict(int)
for issue in all_issues:
by_category[issue["category"]].append(issue)
by_severity[issue["severity"]] += 1
score = compute_score(all_issues)
result = {
"project": str(project_dir),
"platform": platform,
"analyzed_at": datetime.utcnow().isoformat() + "Z",
"performance_score": score,
"summary": {
"total_issues": len(all_issues),
"critical": by_severity.get(SEVERITY_CRITICAL, 0),
"warnings": by_severity.get(SEVERITY_WARNING, 0),
"info": by_severity.get(SEVERITY_INFO, 0),
},
"bundle_estimate": bundle_info,
"issues_by_category": {k: v for k, v in sorted(by_category.items())},
"issues": all_issues,
}
if json_output:
print(json.dumps(result, indent=2))
else:
_print_human_report(result)
def _print_human_report(result: dict):
"""Pretty-print the analysis report."""
print("=" * 70)
print(" MOBILE APP PERFORMANCE ANALYSIS")
print("=" * 70)
print(f" Project: {result['project']}")
print(f" Platform: {result['platform']}")
print(f" Analyzed: {result['analyzed_at']}")
print()
score = result["performance_score"]
grade = "A" if score >= 90 else "B" if score >= 75 else "C" if score >= 60 else "D" if score >= 40 else "F"
print(f" Performance Score: {score}/100 (Grade: {grade})")
print()
s = result["summary"]
print(f" Issues Found: {s['total_issues']}")
if s["critical"]:
print(f" Critical: {s['critical']}")
if s["warnings"]:
print(f" Warnings: {s['warnings']}")
if s["info"]:
print(f" Info: {s['info']}")
print()
b = result["bundle_estimate"]
print(" Bundle Size Estimate:")
print(f" Source code: {b['source_code_size']} ({b['source_code_files']} files)")
print(f" Assets: {b['asset_size']} ({b['asset_files']} files)")
print(f" Total: {b['total_size']}")
print()
if result["issues"]:
print("-" * 70)
print(" DETAILED ISSUES")
print("-" * 70)
for category, issues in result["issues_by_category"].items():
cat_label = category.replace("_", " ").title()
print(f"\n [{cat_label}] ({len(issues)} issues)")
for issue in issues:
sev = issue["severity"].upper()
loc = issue.get("file", "project")
line = issue.get("line")
loc_str = f"{loc}:{line}" if line else loc
print(f" [{sev:8s}] {loc_str}")
print(f" {issue['message']}")
else:
print(" No performance issues detected. Great job!")
print()
print("=" * 70)
print(" RECOMMENDATIONS")
print("=" * 70)
if result["summary"]["critical"] > 0:
print(" 1. Address all CRITICAL issues immediately")
if result["bundle_estimate"]["asset_bytes"] > 5 * 1024 * 1024:
print(" 2. Optimize image assets - total asset size exceeds 5 MB")
if result["summary"]["warnings"] > 10:
print(" 3. Review WARNING issues - many potential performance regressions")
if result["platform"] == "react-native":
print(" - Use React.memo() for expensive components")
print(" - Use useCallback/useMemo for reference stability")
print(" - Use FlatList with keyExtractor for all lists")
print(" - Remove console.log statements before production")
elif result["platform"] == "flutter":
print(" - Use const constructors where possible")
print(" - Prefer ValueNotifier/Riverpod over setState for granular rebuilds")
print(" - Dispose all controllers in dispose()")
print(" - Use cached_network_image for network images")
elif result["platform"] == "ios-native":
print(" - Profile with Instruments (Time Profiler, Allocations)")
print(" - Remove all print() statements for release")
print(" - Avoid force unwrapping and force casting")
elif result["platform"] == "android-native":
print(" - Use viewModelScope/lifecycleScope instead of GlobalScope")
print(" - Profile with Android Studio Profiler")
print(" - Use Coil/Glide for image loading")
print()
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def parse_args(argv=None):
parser = argparse.ArgumentParser(
prog="app_performance_analyzer",
description="Analyze a mobile app project for performance issues.",
epilog="Example: python app_performance_analyzer.py ./my-app --platform react-native",
)
parser.add_argument(
"project_dir",
help="Path to the mobile project directory",
)
parser.add_argument(
"--platform", "-p",
choices=["react-native", "flutter", "ios-native", "android-native"],
default=None,
help="Target platform (auto-detected if omitted)",
)
parser.add_argument(
"--json",
action="store_true",
default=False,
help="Output results as JSON",
)
return parser.parse_args(argv)
def main(argv=None):
args = parse_args(argv)
project_dir = Path(args.project_dir).resolve()
if not project_dir.is_dir():
sys.stderr.write(f"Error: '{project_dir}' is not a directory.\n")
sys.exit(1)
platform = args.platform
if platform is None:
platform = detect_platform(project_dir)
if platform == "unknown":
sys.stderr.write(
"Error: Could not auto-detect platform. "
"Use --platform to specify.\n"
)
sys.exit(1)
if not args.json:
sys.stderr.write(f"Auto-detected platform: {platform}\n")
generate_report(project_dir, platform, args.json)
if __name__ == "__main__":
main()
Related skills
FAQ
What platforms does senior-mobile cover?
senior-mobile guides native and cross-platform iOS and Android development, including navigation, offline handling, push notifications, and store-ready polish for production mobile clients.
Does senior-mobile handle backend APIs?
senior-mobile focuses on mobile client architecture, offline behavior, and UI polish. Backend API design and server infrastructure require separate backend or DevOps skills.