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
Specify the target platform (iOS, Android, or cross-platform) and framework (SwiftUI, Kotlin, React Native, Flutter)
Mention minimum OS versions and device types you need to support
Include details about native features needed (camera, GPS, push notifications, biometrics)
Describe existing navigation patterns and state management approach if integrating with an existing app
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
Code Review Prompt Templates
AI prompt templates for thorough code reviews. Get comprehensive feedback on code quality, security, and best practices.
Debugging Prompt Templates
AI prompt templates for debugging code. Identify issues, understand errors, and find solutions faster.
Code Documentation Prompt Templates
AI prompt templates for writing code documentation. Create clear comments, READMEs, and API docs.
Have your own prompt to optimize?