Mobile Development Prompt Templates

AI prompt templates for mobile app development. Build native and cross-platform mobile applications.

Overview

Mobile development prompts help you build native iOS and Android apps or cross-platform solutions using React Native, Flutter, or other frameworks. These templates address common mobile challenges like responsive layouts, platform-specific APIs, offline storage, and app store deployment.

Best Practices

1

Specify the target platform (iOS, Android, or cross-platform) and framework (SwiftUI, Kotlin, React Native, Flutter)

2

Mention minimum OS versions and device types you need to support

3

Include details about native features needed (camera, GPS, push notifications, biometrics)

4

Describe existing navigation patterns and state management approach if integrating with an existing app

5

Note any performance constraints or offline requirements

Prompt Templates

1. React Native Component Builder

Create a React Native component for [COMPONENT_PURPOSE].

Requirements:
- Platform: [ios/android/both]
- Navigation library: [NAVIGATION_LIB]
- State management: [STATE_APPROACH]
- Must support: [FEATURES_LIST]

Existing types/interfaces to use:
[EXISTING_TYPES]

Design specifications:
[DESIGN_SPECS]

Provide the component with proper TypeScript types, handle both platforms where behavior differs, and include loading/error states.
Create a React Native component for a product detail screen.

Requirements:
- Platform: both
- Navigation library: React Navigation 6
- State management: Redux Toolkit with RTK Query
- Must support: image gallery, size selector, add to cart, wishlist toggle

Existing types/interfaces to use:
interface Product { id: string; name: string; price: number; images: string[]; sizes: Size[]; inStock: boolean; }
interface Size { id: string; label: string; available: boolean; }

Design specifications:
- Sticky header with back button and share icon
- Full-width image carousel with pagination dots
- Price in large bold text, original price struck through if on sale
- Size chips that gray out when unavailable
- Fixed bottom bar with Add to Cart button
```tsx
import React, { useState, useCallback } from 'react';
import {
 View,
 Text,
 ScrollView,
 Image,
 TouchableOpacity,
 StyleSheet,
 Dimensions,
 Platform,
 Share,
 ActivityIndicator,
} from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useGetProductQuery, useAddToCartMutation, useToggleWishlistMutation } from '../store/api';
import { useAppSelector } from '../store/hooks';
import Icon from 'react-native-vector-icons/Feather';

const { width: SCREEN_WIDTH } = Dimensions.get('window');

type RootStackParamList = {
 ProductDetail: { productId: string };
};

type ProductDetailRouteProp = RouteProp<RootStackParamList, 'ProductDetail'>;

export const ProductDetailScreen: React.FC = () => {
 const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
 const route = useRoute<ProductDetailRouteProp>();
 const { productId } = route.params;

 const [selectedSize, setSelectedSize] = useState<string | null>(null);
 const [currentImageIndex, setCurrentImageIndex] = useState(0);

 const { data: product, isLoading, error } = useGetProductQuery(productId);
 const [addToCart, { isLoading: isAddingToCart }] = useAddToCartMutation();
 const [toggleWishlist] = useToggleWishlistMutation();
 
 const wishlistItems = useAppSelector(state => state.wishlist.items);
 const isInWishlist = wishlistItems.includes(productId);

 const handleShare = useCallback(async () => {
 if (!product) return;
 try {
 await Share.share({
 message: `Check out ${product.name}`,
 url: `https://myapp.com/products/${productId}`,
 });
 } catch (err) {
 console.error('Share failed:', err);
 }
 }, [product, productId]);

 const handleAddToCart = useCallback(async () => {
 if (!selectedSize || !product?.inStock) return;
 try {
 await addToCart({ productId, sizeId: selectedSize }).unwrap();
 // Show success feedback
 } catch (err) {
 // Handle error
 }
 }, [selectedSize, product, addToCart, productId]);

 const handleScroll = useCallback((event: any) => {
 const index = Math.round(event.nativeEvent.contentOffset.x / SCREEN_WIDTH);
 setCurrentImageIndex(index);
 }, []);

 if (isLoading) {
 return (
 <View style={styles.centered}>
 <ActivityIndicator size="large" color="#000" />
 </View>
 );
 }

 if (error || !product) {
 return (
 <View style={styles.centered}>
 <Text style={styles.errorText}>Failed to load product</Text>
 <TouchableOpacity onPress={() => navigation.goBack()}>
 <Text style={styles.linkText}>Go Back</Text>
 </TouchableOpacity>
 </View>
 );
 }

 return (
 <View style={styles.container}>
 {/* Sticky Header */}
 <View style={styles.header}>
 <TouchableOpacity onPress={() => navigation.goBack()} style={styles.headerButton}>
 <Icon name="arrow-left" size={24} color="#000" />
 </TouchableOpacity>
 <TouchableOpacity onPress={handleShare} style={styles.headerButton}>
 <Icon name="share" size={24} color="#000" />
 </TouchableOpacity>
 </View>

 <ScrollView showsVerticalScrollIndicator={false}>
 {/* Image Carousel */}
 <ScrollView
 horizontal
 pagingEnabled
 showsHorizontalScrollIndicator={false}
 onScroll={handleScroll}
 scrollEventThrottle={16}
 >
 {product.images.map((uri, index) => (
 <Image key={index} source={{ uri }} style={styles.productImage} resizeMode="cover" />
 ))}
 </ScrollView>

 {/* Pagination Dots */}
 <View style={styles.pagination}>
 {product.images.map((_, index) => (
 <View
 key={index}
 style={[styles.dot, currentImageIndex === index && styles.dotActive]}
 />
 ))}
 </View>

 <View style={styles.content}>
 {/* Title and Wishlist */}
 <View style={styles.titleRow}>
 <Text style={styles.productName}>{product.name}</Text>
 <TouchableOpacity onPress={() => toggleWishlist(productId)}>
 <Icon
 name={isInWishlist ? 'heart' : 'heart'}
 size={24}
 color={isInWishlist ? '#e53935' : '#ccc'}
 style={isInWishlist ? { fill: '#e53935' } : {}}
 />
 </TouchableOpacity>
 </View>

 {/* Price */}
 <Text style={styles.price}>${product.price.toFixed(2)}</Text>

 {/* Size Selector */}
 <Text style={styles.sectionTitle}>Select Size</Text>
 <View style={styles.sizeContainer}>
 {product.sizes.map(size => (
 <TouchableOpacity
 key={size.id}
 style={[
 styles.sizeChip,
 selectedSize === size.id && styles.sizeChipSelected,
 !size.available && styles.sizeChipDisabled,
 ]}
 onPress={() => size.available && setSelectedSize(size.id)}
 disabled={!size.available}
 >
 <Text
 style={[
 styles.sizeText,
 selectedSize === size.id && styles.sizeTextSelected,
 !size.available && styles.sizeTextDisabled,
 ]}
 >
 {size.label}
 </Text>
 </TouchableOpacity>
 ))}
 </View>
 </View>
 </ScrollView>

 {/* Fixed Bottom Bar */}
 <View style={styles.bottomBar}>
 <TouchableOpacity
 style={[
 styles.addToCartButton,
 (!selectedSize || !product.inStock) && styles.addToCartDisabled,
 ]}
 onPress={handleAddToCart}
 disabled={!selectedSize || !product.inStock || isAddingToCart}
 >
 {isAddingToCart ? (
 <ActivityIndicator color="#fff" />
 ) : (
 <Text style={styles.addToCartText}>
 {!product.inStock ? 'Out of Stock' : 'Add to Cart'}
 </Text>
 )}
 </TouchableOpacity>
 </View>
 </View>
 );
};

const styles = StyleSheet.create({
 container: { flex: 1, backgroundColor: '#fff' },
 centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
 errorText: { fontSize: 16, color: '#666', marginBottom: 12 },
 linkText: { fontSize: 16, color: '#007AFF' },
 header: {
 position: 'absolute',
 top: Platform.OS === 'ios' ? 50 : 20,
 left: 0,
 right: 0,
 flexDirection: 'row',
 justifyContent: 'space-between',
 paddingHorizontal: 16,
 zIndex: 10,
 },
 headerButton: {
 width: 40,
 height: 40,
 borderRadius: 20,
 backgroundColor: 'rgba(255,255,255,0.9)',
 justifyContent: 'center',
 alignItems: 'center',
 },
 productImage: { width: SCREEN_WIDTH, height: SCREEN_WIDTH * 1.2 },
 pagination: { flexDirection: 'row', justifyContent: 'center', marginTop: 12 },
 dot: { width: 8, height: 8, borderRadius: 4, backgroundColor: '#ccc', marginHorizontal: 4 },
 dotActive: { backgroundColor: '#000' },
 content: { padding: 16 },
 titleRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
 productName: { fontSize: 24, fontWeight: '600', flex: 1, marginRight: 12 },
 price: { fontSize: 28, fontWeight: '700', marginTop: 8 },
 sectionTitle: { fontSize: 16, fontWeight: '600', marginTop: 24, marginBottom: 12 },
 sizeContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
 sizeChip: {
 paddingVertical: 10,
 paddingHorizontal: 20,
 borderRadius: 8,
 borderWidth: 1,
 borderColor: '#ddd',
 },
 sizeChipSelected: { borderColor: '#000', backgroundColor: '#000' },
 sizeChipDisabled: { backgroundColor: '#f5f5f5', borderColor: '#eee' },
 sizeText: { fontSize: 14, color: '#000' },
 sizeTextSelected: { color: '#fff' },
 sizeTextDisabled: { color: '#bbb' },
 bottomBar: {
 padding: 16,
 paddingBottom: Platform.OS === 'ios' ? 34 : 16,
 borderTopWidth: 1,
 borderTopColor: '#eee',
 },
 addToCartButton: {
 backgroundColor: '#000',
 paddingVertical: 16,
 borderRadius: 12,
 alignItems: 'center',
 },
 addToCartDisabled: { backgroundColor: '#ccc' },
 addToCartText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
```

When building complete mobile screens that need to handle data fetching, user interactions, and platform-specific behaviors in React Native.

  • Use Platform.select() for more complex platform differences beyond just iOS/Android padding
  • Memoize callbacks with useCallback to prevent unnecessary re-renders in lists
  • Consider using react-native-reanimated for smoother animations on the image carousel

2. Native iOS Feature Implementation

Implement [FEATURE_NAME] for iOS using SwiftUI.

Feature requirements:
[REQUIREMENTS]

Target iOS version: [MIN_IOS_VERSION]
Device support: [DEVICES]

Existing app architecture:
- Navigation: [NAV_PATTERN]
- Data layer: [DATA_APPROACH]
- Existing models: [MODELS]

Handle permissions, errors, and loading states appropriately.
Implement biometric authentication for iOS using SwiftUI.

Feature requirements:
- Support Face ID and Touch ID
- Fallback to device passcode if biometrics unavailable
- Remember user preference for biometric login
- Show appropriate icons based on biometric type available

Target iOS version: 15.0
Device support: iPhone and iPad

Existing app architecture:
- Navigation: NavigationStack with path-based routing
- Data layer: SwiftData for persistence, async/await for networking
- Existing models: User, AuthToken stored in Keychain
```swift
import SwiftUI
import LocalAuthentication

// MARK: - Biometric Authentication Manager

@MainActor
class BiometricAuthManager: ObservableObject {
 @Published var biometricType: LABiometryType =.none
 @Published var isBiometricAvailable = false
 @Published var isAuthenticating = false
 @Published var errorMessage: String?
 
 @AppStorage("biometricLoginEnabled") var biometricLoginEnabled = false
 
 private let context = LAContext()
 
 init() {
 checkBiometricAvailability()
 }
 
 func checkBiometricAvailability() {
 var error: NSError?
 isBiometricAvailable = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
 biometricType = context.biometryType
 
 if let error = error {
 print("Biometric check error: \(error.localizedDescription)")
 }
 }
 
 var biometricIconName: String {
 switch biometricType {
 case.faceID:
 return "faceid"
 case.touchID:
 return "touchid"
 case.opticID:
 return "opticid"
 default:
 return "lock.fill"
 }
 }
 
 var biometricDisplayName: String {
 switch biometricType {
 case.faceID:
 return "Face ID"
 case.touchID:
 return "Touch ID"
 case.opticID:
 return "Optic ID"
 default:
 return "Biometrics"
 }
 }
 
 func authenticate() async -> Bool {
 let context = LAContext()
 context.localizedCancelTitle = "Use Password"
 context.localizedFallbackTitle = "Use Passcode"
 
 isAuthenticating = true
 errorMessage = nil
 
 defer { isAuthenticating = false }
 
 // First try biometrics, then fall back to device passcode
 let policy: LAPolicy = isBiometricAvailable 
 ?.deviceOwnerAuthenticationWithBiometrics 
 :.deviceOwnerAuthentication
 
 let reason = "Authenticate to access your account"
 
 do {
 let success = try await context.evaluatePolicy(policy, localizedReason: reason)
 return success
 } catch let error as LAError {
 handleAuthError(error)
 
 // If biometrics failed, try passcode as fallback
 if error.code ==.biometryLockout || error.code ==.biometryNotAvailable {
 return await authenticateWithPasscode()
 }
 return false
 } catch {
 errorMessage = "Authentication failed. Please try again."
 return false
 }
 }
 
 private func authenticateWithPasscode() async -> Bool {
 let context = LAContext()
 let reason = "Enter your device passcode"
 
 do {
 return try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason)
 } catch {
 errorMessage = "Passcode authentication failed."
 return false
 }
 }
 
 private func handleAuthError(_ error: LAError) {
 switch error.code {
 case.userCancel:
 errorMessage = nil // User intentionally cancelled
 case.userFallback:
 errorMessage = nil // User chose passcode
 case.biometryNotAvailable:
 errorMessage = "\(biometricDisplayName) is not available on this device."
 case.biometryNotEnrolled:
 errorMessage = "\(biometricDisplayName) is not set up. Please enable it in Settings."
 case.biometryLockout:
 errorMessage = "\(biometricDisplayName) is locked. Please use your passcode."
 case.authenticationFailed:
 errorMessage = "\(biometricDisplayName) did not recognize you. Please try again."
 default:
 errorMessage = "Authentication failed. Please try again."
 }
 }
}

// MARK: - Biometric Login View

struct BiometricLoginView: View {
 @StateObject private var authManager = BiometricAuthManager()
 @Environment(\.dismiss) private var dismiss
 
 var onAuthSuccess: () -> Void
 var onUsePassword: () -> Void
 
 var body: some View {
 VStack(spacing: 32) {
 Spacer()
 
 // Biometric Icon
 Image(systemName: authManager.biometricIconName)
.font(.system(size: 64))
.foregroundStyle(.blue)
.symbolEffect(.pulse, isActive: authManager.isAuthenticating)
 
 // Title
 VStack(spacing: 8) {
 Text("Welcome Back")
.font(.title)
.fontWeight(.bold)
 
 Text("Use \(authManager.biometricDisplayName) to sign in")
.font(.subheadline)
.foregroundStyle(.secondary)
 }
 
 // Error Message
 if let error = authManager.errorMessage {
 Text(error)
.font(.caption)
.foregroundStyle(.red)
.multilineTextAlignment(.center)
.padding(.horizontal)
 }
 
 Spacer()
 
 // Action Buttons
 VStack(spacing: 16) {
 Button {
 Task {
 if await authManager.authenticate() {
 onAuthSuccess()
 }
 }
 } label: {
 HStack {
 Image(systemName: authManager.biometricIconName)
 Text("Sign in with \(authManager.biometricDisplayName)")
 }
.frame(maxWidth:.infinity)
.padding()
.background(.blue)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
 }
.disabled(authManager.isAuthenticating)
 
 Button("Use Password Instead") {
 onUsePassword()
 }
.foregroundStyle(.blue)
 }
.padding(.horizontal, 24)
.padding(.bottom, 32)
 }
.onAppear {
 // Auto-trigger biometric prompt on appear
 if authManager.biometricLoginEnabled && authManager.isBiometricAvailable {
 Task {
 try? await Task.sleep(for:.milliseconds(500))
 if await authManager.authenticate() {
 onAuthSuccess()
 }
 }
 }
 }
 }
}

// MARK: - Biometric Settings Toggle

struct BiometricSettingsView: View {
 @StateObject private var authManager = BiometricAuthManager()
 @State private var showingAuthPrompt = false
 
 var body: some View {
 Section {
 if authManager.isBiometricAvailable {
 Toggle(isOn: $authManager.biometricLoginEnabled) {
 Label {
 VStack(alignment:.leading) {
 Text("Sign in with \(authManager.biometricDisplayName)")
 Text("Quick and secure access to your account")
.font(.caption)
.foregroundStyle(.secondary)
 }
 } icon: {
 Image(systemName: authManager.biometricIconName)
.foregroundStyle(.blue)
 }
 }
.onChange(of: authManager.biometricLoginEnabled) { oldValue, newValue in
 if newValue && !oldValue {
 // Require authentication to enable
 showingAuthPrompt = true
 }
 }
 } else {
 Label {
 VStack(alignment:.leading) {
 Text("Biometric Login")
 Text("Not available on this device")
.font(.caption)
.foregroundStyle(.secondary)
 }
 } icon: {
 Image(systemName: "lock.fill")
.foregroundStyle(.gray)
 }
 }
 } header: {
 Text("Security")
 } footer: {
 if authManager.isBiometricAvailable {
 Text("When enabled, you can use \(authManager.biometricDisplayName) to quickly sign in to your account.")
 }
 }
.alert("Enable \(authManager.biometricDisplayName)?", isPresented: $showingAuthPrompt) {
 Button("Cancel", role:.cancel) {
 authManager.biometricLoginEnabled = false
 }
 Button("Enable") {
 Task {
 let success = await authManager.authenticate()
 if !success {
 authManager.biometricLoginEnabled = false
 }
 }
 }
 } message: {
 Text("You'll need to verify your identity to enable \(authManager.biometricDisplayName) login.")
 }
 }
}

// MARK: - Preview

#Preview("Login") {
 BiometricLoginView(
 onAuthSuccess: { print("Success!") },
 onUsePassword: { print("Use password") }
 )
}

#Preview("Settings") {
 NavigationStack {
 Form {
 BiometricSettingsView()
 }
.navigationTitle("Settings")
 }
}
```

When implementing native iOS features that require platform-specific APIs like biometrics, camera, HealthKit, or other system frameworks.

  • Always check for feature availability before showing UI elements that depend on it
  • Use LAContext's localizedReason to provide context-appropriate authentication messages
  • Store biometric preference in AppStorage but always re-verify on enable

3. Android Kotlin Feature

Implement [FEATURE_NAME] for Android using Kotlin and Jetpack Compose.

Requirements:
[REQUIREMENTS]

Minimum SDK: [MIN_SDK]
Target SDK: [TARGET_SDK]

Architecture:
- Pattern: [MVVM/MVI/etc]
- DI framework: [HILT/KOIN/etc]
- Existing ViewModels/Repositories: [EXISTING_CODE]

Include proper error handling, state management, and follow Material 3 design guidelines.
Implement a location-based store finder for Android using Kotlin and Jetpack Compose.

Requirements:
- Show nearby stores on a map
- List view of stores sorted by distance
- Search by city or zip code
- Show store details (hours, phone, directions)
- Handle location permissions gracefully

Minimum SDK: 26
Target SDK: 34

Architecture:
- Pattern: MVVM with UiState
- DI framework: Hilt
- Existing ViewModels/Repositories: StoreRepository with getStores() and searchStores(query)
```kotlin
// StoreFinderUiState.kt
data class StoreFinderUiState(
 val stores: List<Store> = emptyList(),
 val selectedStore: Store? = null,
 val userLocation: LatLng? = null,
 val isLoading: Boolean = false,
 val error: String? = null,
 val searchQuery: String = "",
 val locationPermissionState: LocationPermissionState = LocationPermissionState.NotRequested
)

enum class LocationPermissionState {
 NotRequested,
 Granted,
 Denied,
 PermanentlyDenied
}

data class Store(
 val id: String,
 val name: String,
 val address: String,
 val city: String,
 val zipCode: String,
 val phone: String,
 val latitude: Double,
 val longitude: Double,
 val hours: List<StoreHours>,
 val distanceKm: Double? = null
)

data class StoreHours(
 val dayOfWeek: String,
 val openTime: String,
 val closeTime: String,
 val isClosed: Boolean = false
)

// StoreFinderViewModel.kt
@HiltViewModel
class StoreFinderViewModel @Inject constructor(
 private val storeRepository: StoreRepository,
 private val locationService: LocationService
) : ViewModel() {

 private val _uiState = MutableStateFlow(StoreFinderUiState())
 val uiState: StateFlow<StoreFinderUiState> = _uiState.asStateFlow()

 fun onPermissionResult(granted: Boolean, shouldShowRationale: Boolean) {
 val permissionState = when {
 granted -> LocationPermissionState.Granted
 shouldShowRationale -> LocationPermissionState.Denied
 else -> LocationPermissionState.PermanentlyDenied
 }
 _uiState.update { it.copy(locationPermissionState = permissionState) }
 
 if (granted) {
 fetchUserLocationAndStores()
 }
 }

 fun fetchUserLocationAndStores() {
 viewModelScope.launch {
 _uiState.update { it.copy(isLoading = true, error = null) }
 
 try {
 val location = locationService.getCurrentLocation()
 _uiState.update { it.copy(userLocation = location) }
 
 val stores = storeRepository.getStores()
.map { store ->
 store.copy(
 distanceKm = location?.let {
 calculateDistance(it, LatLng(store.latitude, store.longitude))
 }
 )
 }
.sortedBy { it.distanceKm ?: Double.MAX_VALUE }
 
 _uiState.update { it.copy(stores = stores, isLoading = false) }
 } catch (e: Exception) {
 _uiState.update { 
 it.copy(
 isLoading = false,
 error = "Failed to load stores. Please try again."
 )
 }
 }
 }
 }

 fun onSearchQueryChange(query: String) {
 _uiState.update { it.copy(searchQuery = query) }
 }

 fun onSearch() {
 val query = _uiState.value.searchQuery
 if (query.isBlank()) {
 fetchUserLocationAndStores()
 return
 }

 viewModelScope.launch {
 _uiState.update { it.copy(isLoading = true, error = null) }
 
 try {
 val stores = storeRepository.searchStores(query)
 _uiState.update { it.copy(stores = stores, isLoading = false) }
 } catch (e: Exception) {
 _uiState.update {
 it.copy(isLoading = false, error = "Search failed. Please try again.")
 }
 }
 }
 }

 fun onStoreSelected(store: Store) {
 _uiState.update { it.copy(selectedStore = store) }
 }

 fun onDismissStoreDetails() {
 _uiState.update { it.copy(selectedStore = null) }
 }

 fun clearError() {
 _uiState.update { it.copy(error = null) }
 }

 private fun calculateDistance(from: LatLng, to: LatLng): Double {
 val results = FloatArray(1)
 Location.distanceBetween(
 from.latitude, from.longitude,
 to.latitude, to.longitude,
 results
 )
 return results[0].toDouble() / 1000 // Convert to km
 }
}

// StoreFinderScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StoreFinderScreen(
 viewModel: StoreFinderViewModel = hiltViewModel(),
 onNavigateToDirections: (Store) -> Unit
) {
 val uiState by viewModel.uiState.collectAsStateWithLifecycle()
 val context = LocalContext.current
 
 val locationPermissionLauncher = rememberLauncherForActivityResult(
 contract = ActivityResultContracts.RequestMultiplePermissions()
 ) { permissions ->
 val granted = permissions.values.any { it }
 val shouldShowRationale = (context as? Activity)?.let {
 ActivityCompat.shouldShowRequestPermissionRationale(
 it,
 Manifest.permission.ACCESS_FINE_LOCATION
 )
 } ?: false
 viewModel.onPermissionResult(granted, shouldShowRationale)
 }

 LaunchedEffect(Unit) {
 when (uiState.locationPermissionState) {
 LocationPermissionState.NotRequested -> {
 locationPermissionLauncher.launch(
 arrayOf(
 Manifest.permission.ACCESS_FINE_LOCATION,
 Manifest.permission.ACCESS_COARSE_LOCATION
 )
 )
 }
 LocationPermissionState.Granted -> viewModel.fetchUserLocationAndStores()
 else -> { /* Handle in UI */ }
 }
 }

 Scaffold(
 topBar = {
 TopAppBar(
 title = { Text("Find a Store") }
 )
 }
 ) { padding ->
 Column(
 modifier = Modifier
.fillMaxSize()
.padding(padding)
 ) {
 // Search Bar
 SearchBar(
 query = uiState.searchQuery,
 onQueryChange = viewModel::onSearchQueryChange,
 onSearch = viewModel::onSearch,
 modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
 )

 when {
 uiState.locationPermissionState == LocationPermissionState.PermanentlyDenied -> {
 LocationPermissionDeniedContent(
 onOpenSettings = {
 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
 data = Uri.fromParts("package", context.packageName, null)
 }
 context.startActivity(intent)
 }
 )
 }
 uiState.isLoading -> {
 Box(
 modifier = Modifier.fillMaxSize(),
 contentAlignment = Alignment.Center
 ) {
 CircularProgressIndicator()
 }
 }
 uiState.error != null -> {
 ErrorContent(
 message = uiState.error!!,
 onRetry = viewModel::fetchUserLocationAndStores
 )
 }
 else -> {
 StoreList(
 stores = uiState.stores,
 onStoreClick = viewModel::onStoreSelected,
 modifier = Modifier.weight(1f)
 )
 }
 }
 }

 // Store Details Bottom Sheet
 uiState.selectedStore?.let { store ->
 StoreDetailsSheet(
 store = store,
 onDismiss = viewModel::onDismissStoreDetails,
 onDirectionsClick = { onNavigateToDirections(store) },
 onCallClick = {
 val intent = Intent(Intent.ACTION_DIAL).apply {
 data = Uri.parse("tel:${store.phone}")
 }
 context.startActivity(intent)
 }
 )
 }
 }

 // Error Snackbar
 uiState.error?.let { error ->
 LaunchedEffect(error) {
 // Show snackbar
 viewModel.clearError()
 }
 }
}

@Composable
private fun SearchBar(
 query: String,
 onQueryChange: (String) -> Unit,
 onSearch: () -> Unit,
 modifier: Modifier = Modifier
) {
 OutlinedTextField(
 value = query,
 onValueChange = onQueryChange,
 placeholder = { Text("Search by city or zip code") },
 leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
 trailingIcon = {
 if (query.isNotEmpty()) {
 IconButton(onClick = { onQueryChange("") }) {
 Icon(Icons.Default.Clear, contentDescription = "Clear")
 }
 }
 },
 singleLine = true,
 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
 keyboardActions = KeyboardActions(onSearch = { onSearch() }),
 modifier = modifier
 )
}

@Composable
private fun StoreList(
 stores: List<Store>,
 onStoreClick: (Store) -> Unit,
 modifier: Modifier = Modifier
) {
 LazyColumn(
 modifier = modifier,
 contentPadding = PaddingValues(16.dp),
 verticalArrangement = Arrangement.spacedBy(12.dp)
 ) {
 items(stores, key = { it.id }) { store ->
 StoreCard(
 store = store,
 onClick = { onStoreClick(store) }
 )
 }
 }
}

@Composable
private fun StoreCard(
 store: Store,
 onClick: () -> Unit
) {
 Card(
 onClick = onClick,
 modifier = Modifier.fillMaxWidth()
 ) {
 Row(
 modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
 horizontalArrangement = Arrangement.SpaceBetween,
 verticalAlignment = Alignment.CenterVertically
 ) {
 Column(modifier = Modifier.weight(1f)) {
 Text(
 text = store.name,
 style = MaterialTheme.typography.titleMedium
 )
 Text(
 text = store.address,
 style = MaterialTheme.typography.bodyMedium,
 color = MaterialTheme.colorScheme.onSurfaceVariant
 )
 Text(
 text = "${store.city}, ${store.zipCode}",
 style = MaterialTheme.typography.bodySmall,
 color = MaterialTheme.colorScheme.onSurfaceVariant
 )
 }
 
 store.distanceKm?.let { distance ->
 Text(
 text = String.format("%.1f km", distance),
 style = MaterialTheme.typography.labelLarge,
 color = MaterialTheme.colorScheme.primary
 )
 }
 }
 }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun StoreDetailsSheet(
 store: Store,
 onDismiss: () -> Unit,
 onDirectionsClick: () -> Unit,
 onCallClick: () -> Unit
) {
 ModalBottomSheet(
 onDismissRequest = onDismiss
 ) {
 Column(
 modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
 ) {
 Text(
 text = store.name,
 style = MaterialTheme.typography.headlineSmall
 )
 Spacer(modifier = Modifier.height(8.dp))
 Text(
 text = "${store.address}\n${store.city}, ${store.zipCode}",
 style = MaterialTheme.typography.bodyMedium
 )
 
 Spacer(modifier = Modifier.height(16.dp))
 
 // Hours
 Text(
 text = "Store Hours",
 style = MaterialTheme.typography.titleMedium
 )
 Spacer(modifier = Modifier.height(8.dp))
 store.hours.forEach { hours ->
 Row(
 modifier = Modifier.fillMaxWidth(),
 horizontalArrangement = Arrangement.SpaceBetween
 ) {
 Text(hours.dayOfWeek)
 Text(
 text = if (hours.isClosed) "Closed" else "${hours.openTime} - ${hours.closeTime}",
 color = if (hours.isClosed) MaterialTheme.colorScheme.error 
 else MaterialTheme.colorScheme.onSurface
 )
 }
 }
 
 Spacer(modifier = Modifier.height(24.dp))
 
 // Action Buttons
 Row(
 modifier = Modifier.fillMaxWidth(),
 horizontalArrangement = Arrangement.spacedBy(12.dp)
 ) {
 OutlinedButton(
 onClick = onCallClick,
 modifier = Modifier.weight(1f)
 ) {
 Icon(Icons.Default.Phone, contentDescription = null)
 Spacer(modifier = Modifier.width(8.dp))
 Text("Call")
 }
 Button(
 onClick = onDirectionsClick,
 modifier = Modifier.weight(1f)
 ) {
 Icon(Icons.Default.Navigation, contentDescription = null)
 Spacer(modifier = Modifier.width(8.dp))
 Text("Directions")
 }
 }
 
 Spacer(modifier = Modifier.height(32.dp))
 }
 }
}
```

When building Android features that require native platform capabilities like location services, sensors, or system integrations with Jetpack Compose UI.

  • Use collectAsStateWithLifecycle() instead of collectAsState() to properly handle lifecycle
  • Always handle all permission states including 'permanently denied' with a path to settings
  • Use LaunchedEffect with proper keys to avoid re-triggering effects unnecessarily

4. Flutter Cross-Platform Widget

Create a Flutter widget for [WIDGET_PURPOSE].

Requirements:
- Must work on: [PLATFORMS]
- State management: [STATE_APPROACH]
- Should handle: [REQUIREMENTS]

Existing code context:
[EXISTING_MODELS_OR_SERVICES]

Follow Flutter best practices with proper widget composition and responsive design.
Create a Flutter widget for an onboarding flow.

Requirements:
- Must work on: iOS, Android, Web
- State management: Riverpod
- Should handle: 4 onboarding pages, skip option, progress indicator, final CTA to sign up

Existing code context:
- AppRouter using go_router for navigation
- ThemeData with primary color and text styles defined
- SharedPreferences wrapper for storing hasSeenOnboarding flag
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

// Onboarding State
class OnboardingState {
 final int currentPage;
 final bool isLastPage;
 
 const OnboardingState({
 this.currentPage = 0,
 this.isLastPage = false,
 });
 
 OnboardingState copyWith({int? currentPage, bool? isLastPage}) {
 return OnboardingState(
 currentPage: currentPage ?? this.currentPage,
 isLastPage: isLastPage ?? this.isLastPage,
 );
 }
}

// Onboarding Notifier
class OnboardingNotifier extends StateNotifier<OnboardingState> {
 final int totalPages;
 
 OnboardingNotifier({required this.totalPages}) : super(const OnboardingState());
 
 void setPage(int page) {
 state = state.copyWith(
 currentPage: page,
 isLastPage: page == totalPages - 1,
 );
 }
 
 void nextPage() {
 if (state.currentPage < totalPages - 1) {
 setPage(state.currentPage + 1);
 }
 }
}

// Provider
final onboardingProvider = StateNotifierProvider<OnboardingNotifier, OnboardingState>(
 (ref) => OnboardingNotifier(totalPages: 4),
);

// Onboarding Page Data
class OnboardingPageData {
 final String title;
 final String description;
 final String imagePath;
 final Color backgroundColor;
 
 const OnboardingPageData({
 required this.title,
 required this.description,
 required this.imagePath,
 required this.backgroundColor,
 });
}

const onboardingPages = [
 OnboardingPageData(
 title: 'Welcome to Our App',
 description: 'Discover amazing features that will transform how you work.',
 imagePath: 'assets/onboarding/welcome.svg',
 backgroundColor: Color(0xFFE3F2FD),
 ),
 OnboardingPageData(
 title: 'Stay Organized',
 description: 'Keep all your tasks and projects in one place.',
 imagePath: 'assets/onboarding/organize.svg',
 backgroundColor: Color(0xFFF3E5F5),
 ),
 OnboardingPageData(
 title: 'Collaborate Easily',
 description: 'Work together with your team in real-time.',
 imagePath: 'assets/onboarding/collaborate.svg',
 backgroundColor: Color(0xFFE8F5E9),
 ),
 OnboardingPageData(
 title: 'Get Started Today',
 description: 'Create your free account and start your journey.',
 imagePath: 'assets/onboarding/start.svg',
 backgroundColor: Color(0xFFFFF3E0),
 ),
];

// Main Onboarding Screen
class OnboardingScreen extends ConsumerStatefulWidget {
 const OnboardingScreen({super.key});
 
 @override
 ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
}

class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
 late final PageController _pageController;
 
 @override
 void initState() {
 super.initState();
 _pageController = PageController();
 }
 
 @override
 void dispose() {
 _pageController.dispose();
 super.dispose();
 }
 
 Future<void> _completeOnboarding() async {
 // Mark onboarding as seen
 await ref.read(preferencesProvider).setHasSeenOnboarding(true);
 if (mounted) {
 context.go('/signup');
 }
 }
 
 void _skipOnboarding() {
 _completeOnboarding();
 }
 
 void _nextPage() {
 final state = ref.read(onboardingProvider);
 if (state.isLastPage) {
 _completeOnboarding();
 } else {
 _pageController.nextPage(
 duration: const Duration(milliseconds: 300),
 curve: Curves.easeInOut,
 );
 }
 }
 
 @override
 Widget build(BuildContext context) {
 final state = ref.watch(onboardingProvider);
 final screenSize = MediaQuery.of(context).size;
 final isWideScreen = screenSize.width > 600;
 
 return Scaffold(
 body: SafeArea(
 child: Column(
 children: [
 // Skip Button
 Align(
 alignment: Alignment.topRight,
 child: Padding(
 padding: const EdgeInsets.all(16.0),
 child: TextButton(
 onPressed: state.isLastPage ? null : _skipOnboarding,
 child: Text(
 state.isLastPage ? '' : 'Skip',
 style: Theme.of(context).textTheme.labelLarge?.copyWith(
 color: Theme.of(context).colorScheme.primary,
 ),
 ),
 ),
 ),
 ),
 
 // Page View
 Expanded(
 child: PageView.builder(
 controller: _pageController,
 itemCount: onboardingPages.length,
 onPageChanged: (index) {
 ref.read(onboardingProvider.notifier).setPage(index);
 },
 itemBuilder: (context, index) {
 return OnboardingPageWidget(
 data: onboardingPages[index],
 isWideScreen: isWideScreen,
 );
 },
 ),
 ),
 
 // Progress Indicator
 Padding(
 padding: const EdgeInsets.symmetric(vertical: 24.0),
 child: ProgressIndicatorDots(
 currentIndex: state.currentPage,
 totalCount: onboardingPages.length,
 ),
 ),
 
 // Action Button
 Padding(
 padding: EdgeInsets.symmetric(
 horizontal: isWideScreen ? screenSize.width * 0.3 : 24.0,
 vertical: 16.0,
 ),
 child: SizedBox(
 width: double.infinity,
 height: 56,
 child: FilledButton(
 onPressed: _nextPage,
 child: Text(
 state.isLastPage ? 'Get Started' : 'Continue',
 style: const TextStyle(fontSize: 16),
 ),
 ),
 ),
 ),
 
 const SizedBox(height: 24),
 ],
 ),
 ),
 );
 }
}

// Individual Onboarding Page
class OnboardingPageWidget extends StatelessWidget {
 final OnboardingPageData data;
 final bool isWideScreen;
 
 const OnboardingPageWidget({
 super.key,
 required this.data,
 required this.isWideScreen,
 });
 
 @override
 Widget build(BuildContext context) {
 final content = Column(
 mainAxisAlignment: MainAxisAlignment.center,
 children: [
 // Image
 Expanded(
 flex: 3,
 child: Padding(
 padding: const EdgeInsets.all(32.0),
 child: Image.asset(
 data.imagePath,
 fit: BoxFit.contain,
 ),
 ),
 ),
 
 // Text Content
 Expanded(
 flex: 2,
 child: Padding(
 padding: const EdgeInsets.symmetric(horizontal: 32.0),
 child: Column(
 children: [
 Text(
 data.title,
 style: Theme.of(context).textTheme.headlineMedium?.copyWith(
 fontWeight: FontWeight.bold,
 ),
 textAlign: TextAlign.center,
 ),
 const SizedBox(height: 16),
 Text(
 data.description,
 style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 color: Theme.of(context).colorScheme.onSurfaceVariant,
 ),
 textAlign: TextAlign.center,
 ),
 ],
 ),
 ),
 ),
 ],
 );
 
 if (isWideScreen) {
 return Container(
 color: data.backgroundColor.withOpacity(0.3),
 child: Center(
 child: ConstrainedBox(
 constraints: const BoxConstraints(maxWidth: 600),
 child: content,
 ),
 ),
 );
 }
 
 return Container(
 color: data.backgroundColor.withOpacity(0.3),
 child: content,
 );
 }
}

// Progress Indicator Dots
class ProgressIndicatorDots extends StatelessWidget {
 final int currentIndex;
 final int totalCount;
 
 const ProgressIndicatorDots({
 super.key,
 required this.currentIndex,
 required this.totalCount,
 });
 
 @override
 Widget build(BuildContext context) {
 return Row(
 mainAxisAlignment: MainAxisAlignment.center,
 children: List.generate(totalCount, (index) {
 final isActive = index == currentIndex;
 return AnimatedContainer(
 duration: const Duration(milliseconds: 200),
 margin: const EdgeInsets.symmetric(horizontal: 4),
 width: isActive ? 24 : 8,
 height: 8,
 decoration: BoxDecoration(
 color: isActive
 ? Theme.of(context).colorScheme.primary
 : Theme.of(context).colorScheme.primary.withOpacity(0.3),
 borderRadius: BorderRadius.circular(4),
 ),
 );
 }),
 );
 }
}
```

When creating Flutter widgets that need to work across multiple platforms (iOS, Android, Web) while maintaining consistent behavior and responsive design.

  • Use ConstrainedBox with maxWidth for web to prevent content from stretching too wide
  • Always dispose of controllers in StatefulWidget to prevent memory leaks
  • Use MediaQuery to adapt layouts for different screen sizes rather than platform checks

5. Mobile App Performance Optimization

Analyze and optimize performance for this mobile [PLATFORM] code:

```[LANGUAGE]
[CODE]
```

Current issues observed:
[PERFORMANCE_ISSUES]

Target metrics:
- [METRIC_1]
- [METRIC_2]

Provide optimized code with explanations for each improvement.
Analyze and optimize performance for this mobile React Native code:

```javascript
const ProductList = ({ products, onProductPress }) => {
 const [searchQuery, setSearchQuery] = useState('');
 
 const filteredProducts = products.filter(p => 
 p.name.toLowerCase().includes(searchQuery.toLowerCase())
 );
 
 return (
 <View style={styles.container}>
 <TextInput 
 value={searchQuery}
 onChangeText={setSearchQuery}
 placeholder="Search products"
 />
 <ScrollView>
 {filteredProducts.map(product => (
 <TouchableOpacity 
 key={product.id}
 onPress={() => onProductPress(product)}
 >
 <Image source={{ uri: product.imageUrl }} style={styles.image} />
 <Text>{product.name}</Text>
 <Text>${product.price}</Text>
 </TouchableOpacity>
 ))}
 </ScrollView>
 </View>
 );
};
```

Current issues observed:
- Laggy scrolling with 500+ products
- Search input feels sluggish
- Images flash when scrolling back up

Target metrics:
- 60fps scroll performance
- <100ms input latency
```tsx
import React, { useState, useMemo, useCallback, memo } from 'react';
import {
 View,
 Text,
 TextInput,
 TouchableOpacity,
 FlatList,
 Image,
 StyleSheet,
} from 'react-native';
import FastImage from 'react-native-fast-image';

// 1. Memoized Product Item - prevents re-renders when parent updates
const ProductItem = memo(({ product, onPress }) => {
 // 2. Memoize the press handler to maintain referential equality
 const handlePress = useCallback(() => {
 onPress(product);
 }, [product, onPress]);

 return (
 <TouchableOpacity onPress={handlePress} style={styles.productItem}>
 {/* 3. FastImage for better image caching and performance */}
 <FastImage
 source={{ 
 uri: product.imageUrl,
 priority: FastImage.priority.normal,
 cache: FastImage.cacheControl.immutable,
 }}
 style={styles.image}
 resizeMode={FastImage.resizeMode.cover}
 />
 <View style={styles.productInfo}>
 <Text style={styles.productName} numberOfLines={2}>
 {product.name}
 </Text>
 <Text style={styles.productPrice}>${product.price}</Text>
 </View>
 </TouchableOpacity>
 );
}, (prevProps, nextProps) => {
 // 4. Custom comparison - only re-render if product data changes
 return prevProps.product.id === nextProps.product.id &&
 prevProps.product.name === nextProps.product.name &&
 prevProps.product.price === nextProps.product.price;
});

const ProductList = ({ products, onProductPress }) => {
 const [searchQuery, setSearchQuery] = useState('');
 // 5. Debounced search state for filtering
 const [debouncedQuery, setDebouncedQuery] = useState('');
 const debounceTimerRef = useRef(null);

 // 6. Debounce search input to reduce filtering frequency
 const handleSearchChange = useCallback((text) => {
 setSearchQuery(text);
 
 if (debounceTimerRef.current) {
 clearTimeout(debounceTimerRef.current);
 }
 
 debounceTimerRef.current = setTimeout(() => {
 setDebouncedQuery(text);
 }, 150); // 150ms debounce
 }, []);

 // 7. Memoize filtered products - only recalculate when query or products change
 const filteredProducts = useMemo(() => {
 if (!debouncedQuery.trim()) {
 return products;
 }
 
 const query = debouncedQuery.toLowerCase();
 return products.filter(p => p.name.toLowerCase().includes(query));
 }, [products, debouncedQuery]);

 // 8. Stable callback reference for child components
 const handleProductPress = useCallback((product) => {
 onProductPress(product);
 }, [onProductPress]);

 // 9. Improved key extractor
 const keyExtractor = useCallback((item) => item.id.toString(), []);

 // 10. Render item function for FlatList
 const renderItem = useCallback(({ item }) => (
 <ProductItem product={item} onPress={handleProductPress} />
 ), [handleProductPress]);

 // 11. Improved item layout for fixed-height items (enables better scroll performance)
 const getItemLayout = useCallback((data, index) => ({
 length: ITEM_HEIGHT,
 offset: ITEM_HEIGHT * index,
 index,
 }), []);

 return (
 <View style={styles.container}>
 <TextInput
 value={searchQuery}
 onChangeText={handleSearchChange}
 placeholder="Search products"
 style={styles.searchInput}
 // 12. Reduce re-renders during typing
 autoCorrect={false}
 autoCapitalize="none"
 />
 {/* 13. FlatList instead of ScrollView + map for virtualization */}
 <FlatList
 data={filteredProducts}
 renderItem={renderItem}
 keyExtractor={keyExtractor}
 getItemLayout={getItemLayout}
 // 14. Performance optimizations
 removeClippedSubviews={true}
 maxToRenderPerBatch={10}
 windowSize={5}
 initialNumToRender={10}
 updateCellsBatchingPeriod={50}
 // 15. Maintain scroll position and reduce re-renders
 maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
 // 16. Empty state
 ListEmptyComponent={<Text style={styles.emptyText}>No products found</Text>}
 />
 </View>
 );
};

const ITEM_HEIGHT = 100; // Fixed height for getItemLayout

const styles = StyleSheet.create({
 container: { flex: 1, backgroundColor: '#fff' },
 searchInput: {
 height: 48,
 borderWidth: 1,
 borderColor: '#ddd',
 borderRadius: 8,
 paddingHorizontal: 16,
 margin: 16,
 fontSize: 16,
 },
 productItem: {
 flexDirection: 'row',
 padding: 12,
 height: ITEM_HEIGHT,
 borderBottomWidth: 1,
 borderBottomColor: '#eee',
 },
 image: { width: 76, height: 76, borderRadius: 8 },
 productInfo: { flex: 1, marginLeft: 12, justifyContent: 'center' },
 productName: { fontSize: 16, fontWeight: '500' },
 productPrice: { fontSize: 14, color: '#666', marginTop: 4 },
 emptyText: { textAlign: 'center', padding: 32, color: '#999' },
});

export default memo(ProductList);
```

**Key Optimizations:**

1. **FlatList instead of ScrollView** - Virtualizes the list, only rendering visible items
2. **memo() on components** - Prevents unnecessary re-renders when props haven't changed 
3. **FastImage** - Aggressive image caching, prevents flash on scroll back
4. **Debounced search** - Reduces filtering operations during typing
5. **useMemo for filtering** - Only recalculates when dependencies change
6. **useCallback for handlers** - Maintains referential equality
7. **getItemLayout** - Enables scroll-to-index and smoother scrolling with known heights
8. **FlatList tuning** - `windowSize`, `maxToRenderPerBatch`, `removeClippedSubviews`

When mobile app performance is suffering due to inefficient rendering, poor list performance, or excessive re-renders, especially with large data sets.

  • Profile with Flipper or React DevTools before improving, don't guess at bottlenecks
  • For lists with flexible heights, consider FlashList (Shopify) as a FlatList replacement
  • Use InteractionManager.runAfterInteractions() to defer heavy work until animations complete

Common Mistakes to Avoid

Using ScrollView with map() for long lists instead of FlatList/RecyclerView, causes all items to render at once

Not handling platform differences explicitly, assuming iOS behavior works the same on Android

Ignoring the keyboard on mobile, not adjusting layouts or dismissing keyboard appropriately

Frequently Asked Questions

Mobile development prompts help you build native iOS and Android apps or cross-platform solutions using React Native, Flutter, or other frameworks. These templates address common mobile challenges like responsive layouts, platform-specific APIs, offline storage, and app store deployment.

Related Templates

Have your own prompt to optimize?