
Software Mobile
- 168 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
Helps with ai & agent building tasks.
About
software-mobile is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- software-mobile
- AI & Agent Building
- AI-coding skill
Software Mobile by the numbers
- 168 all-time installs (skills.sh)
- +9 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #3,159 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill software-mobileAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 168 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
What it does
Helps with ai & agent building tasks.
Files
Mobile Development Skill — Quick Reference
This skill equips mobile developers with execution-ready patterns for building native and cross-platform mobile applications. Apply these patterns when you need iOS/Android app architecture, UI components, navigation flows, API integration, offline storage, authentication, or mobile-specific features.
---
When to Use This Skill
Use this skill when you need:
- iOS app development (Swift, SwiftUI, UIKit)
- Android app development (Kotlin, Jetpack Compose)
- Cross-platform development (React Native, WebView)
- Mobile app architecture and patterns
- Navigation and routing
- State management (Redux, MobX, MVVM)
- Network requests and API integration
- Local data storage (Core Data, Room, SQLite)
- Authentication and session management
- Push notifications (APNs, FCM)
- Camera and media access
- Location services
- App Store / Play Store deployment
- Mobile performance optimization
- Offline-first architecture
- Deep linking and universal links
---
Quick Reference Table
| Task | iOS | Android | Cross-Platform | When to Use |
|---|---|---|---|---|
| Native UI | SwiftUI + UIKit | Jetpack Compose + Views | React Native | Native: Best performance; Cross-platform: Code sharing |
| Navigation | NavigationStack | Navigation Component | React Navigation | Platform-specific for native feel |
| State Management | @State/@Observable | ViewModel + StateFlow | Redux/MobX | iOS: @Observable; Android: ViewModel; RN: Redux |
| Networking | URLSession + async/await | Retrofit + Coroutines | Axios/Fetch | Native: Type-safe; RN: JavaScript ecosystem |
| Local Storage | Core Data + SwiftData | Room Database | AsyncStorage/SQLite | Native: Full control; RN: Simpler |
| Push Notifications | APNs | FCM | React Native Firebase | Native: Platform-specific; RN: Unified API |
| Background Tasks | BGTaskScheduler | WorkManager | Headless JS | For scheduled/background work |
| Deep Linking | Universal Links | App Links | React Navigation linking | For URL-based app entry |
| Authentication | AuthenticationServices | Credential Manager | Expo AuthSession | For social/biometric auth |
| Analytics | Firebase/Amplitude | Firebase/Amplitude | Expo Analytics | Track user behavior |
---
Decision Tree: Platform Selection
Need to build mobile app for: [Target Audience]
│
├─ iOS only?
│ ├─ New app? → SwiftUI (modern, declarative)
│ ├─ Existing UIKit codebase? → UIKit + incremental SwiftUI adoption
│ └─ Complex animations? → UIKit for fine-grained control
│
├─ Android only?
│ ├─ New app? → Jetpack Compose (modern, declarative)
│ ├─ Existing Views codebase? → Views + incremental Compose adoption
│ └─ Complex custom views? → Custom View for fine-grained control
│
├─ Both iOS and Android?
│ ├─ Need maximum performance / platform fidelity?
│ │ └─ Build separate native apps (Swift + Kotlin)
│ │
│ ├─ Need faster development + code sharing?
│ │ ├─ JavaScript/TypeScript team? → React Native (Expo-managed or bare)
│ │ ├─ Dart team? → Flutter
│ │ └─ Kotlin team? → Kotlin Multiplatform (KMP)
│ │
│ ├─ Kotlin Multiplatform (KMP)?
│ │ ├─ Share business logic only? → KMP shared module + native UI
│ │ ├─ Share some UI? → Compose Multiplatform (validate iOS maturity for your needs)
│ │ └─ Shared modules need platform UI? → Keep native UI, share domain/data/networking
│ │
│ └─ Wrapping existing web app?
│ ├─ Simple wrapper? → WebView (iOS WKWebView / Android WebView)
│ └─ Native features needed? → Capacitor or React Native WebView
│
└─ Prototype/MVP only?
└─ React Native or Flutter for fastest iterationDecision Tree: Architecture Pattern
Choosing architecture pattern?
│
├─ iOS (Swift)?
│ ├─ SwiftUI app? → MVVM with @Observable/ObservableObject (based on OS baseline)
│ ├─ Complex SwiftUI? → TCA (Composable Architecture) for testability
│ ├─ UIKit app? → MVVM-C (Coordinator pattern)
│ ├─ Large team? → Clean Architecture + MVVM
│ └─ Simple app? → MVC (Apple default)
│
├─ Android (Kotlin)?
│ ├─ Compose app? → MVVM with ViewModel + StateFlow
│ ├─ Views app? → MVVM with LiveData
│ ├─ Large team? → Clean Architecture + MVVM
│ └─ Simple app? → Activity/Fragment-based
│
└─ React Native?
├─ Small app? → Context API + useState
├─ Medium app? → Redux Toolkit or Zustand
└─ Large app? → Redux + RTK Query + feature-based structureDecision Tree: Data Persistence
Need to store data locally?
│
├─ Simple key-value pairs?
│ ├─ iOS → UserDefaults
│ ├─ Android → SharedPreferences / DataStore
│ └─ RN → AsyncStorage
│
├─ Structured data with relationships?
│ ├─ iOS → Core Data or SwiftData
│ ├─ Android → Room Database
│ └─ RN → WatermelonDB or Realm
│
├─ Secure credentials?
│ ├─ iOS → Keychain
│ ├─ Android → EncryptedSharedPreferences / Keystore
│ └─ RN → react-native-keychain
│
└─ Large files/media?
├─ iOS → FileManager (Documents/Cache)
├─ Android → Internal/External Storage
└─ RN → react-native-fsDecision Tree: Networking
Need to make API calls?
│
├─ iOS?
│ ├─ Simple REST? → URLSession + async/await
│ ├─ Complex API? → URLSession + Codable
│ └─ GraphQL? → Apollo iOS
│
├─ Android?
│ ├─ Simple REST? → Retrofit + Coroutines
│ ├─ Complex API? → Retrofit + OkHttp interceptors
│ └─ GraphQL? → Apollo Android
│
└─ React Native?
├─ Simple REST? → fetch() or Axios
├─ Complex API? → RTK Query or React Query
└─ GraphQL? → Apollo Client---
Core Capabilities
iOS Development
- UI Frameworks: SwiftUI (declarative), UIKit (imperative)
- Architecture: MVVM, Clean Architecture, Coordinator, TCA (Composable Architecture)
- Concurrency: Swift Concurrency (async/await, actors, TaskGroup); keep UI state on
@MainActor; enable strict concurrency checks as appropriate - Storage: Core Data, SwiftData, Keychain
- Networking: URLSession, async/await patterns
- Platform compliance: Privacy manifests + required-reason APIs, background execution limits, and accessibility settings (Dynamic Type, VoiceOver)
- Defensive Decoding: Handle missing fields, array/dict formats, snake_case/camelCase
Android Development
- UI Frameworks: Jetpack Compose (declarative), Views (XML)
- Architecture: MVVM, Clean Architecture, MVI
- Concurrency: Coroutines, Flow, LiveData
- Storage: Room, DataStore, Keystore
- Networking: Retrofit, OkHttp, Ktor
Cross-Platform Development
- Kotlin Multiplatform (KMP): Share domain/data/networking; keep native UI; consider Compose Multiplatform when shared UI is worth the constraints
- React Native: JavaScript/TypeScript; evaluate New Architecture readiness and native-module surface area; Expo-managed path is often fastest for greenfield apps
- Flutter: Dart; high code sharing; validate platform-specific gaps and plugin maturity for your requirements
- WebView: WKWebView (iOS), WebView (Android), JavaScript bridge
---
Platform Baselines (Verify Current Requirements)
iOS/iPadOS (Core)
- Privacy manifest files (app + embedded SDKs) are maintained and reviewed https://developer.apple.com/documentation/bundlereferences/privacy_manifest_files
- Required-reason APIs are declared with valid reasons https://developer.apple.com/documentation/bundlereferences/privacy_manifest_files
- Background work uses supported primitives (avoid fragile timers) https://developer.apple.com/documentation/backgroundtasks
- App Transport Security is configured; exceptions are justified and documented https://developer.apple.com/documentation/bundlereferences/information_property_list/nsapptransportsecurity
- Concurrency is implemented with Swift Concurrency (async/await, actors, TaskGroup) and checked with current Swift language mode settings https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html
- Swift 6 migration / strict concurrency guidance is followed when upgrading toolchains https://developer.apple.com/documentation/swift/adoptingswift6
- UI/UX follows current Human Interface Guidelines (including accessibility) https://developer.apple.com/design/human-interface-guidelines/ios
Android (Core)
- Background work uses WorkManager for deferrable, guaranteed work https://developer.android.com/topic/libraries/architecture/workmanager
- Network calls and auth state survive process death (no hidden singleton assumptions) [Inference]
- Target SDK meets current Google Play requirements (verify policy + deadlines) https://support.google.com/googleplay/android-developer/answer/11926878
- Prefer Play Integrity API over deprecated SafetyNet Attestation https://developer.android.com/google/play/integrity
- Prefer Credential Manager for passkeys and modern sign-in flows https://developer.android.com/identity/sign-in/credential-manager
Cross-Platform (Core)
- Feature parity is explicit (document what is native-only vs shared) [Inference]
- Bridges are treated as public APIs (versioned, tested, and observable) [Inference]
- React Native upgrades follow the official upgrade guide; validate New Architecture readiness against your native-module surface area https://reactnative.dev/docs/upgrading
- Expo SDK upgrades follow Expo release notes and upgrade guides https://expo.dev/changelog
Optional: AI/Automation Extensions
Note: Skip unless the app ships AI/automation features.
- iOS: Core ML / on-device inference primitives https://developer.apple.com/documentation/coreml
- Android: Google ML Kit https://developers.google.com/ml-kit
- Verify: model size/battery impact, offline/online behavior, user controls (cancel/undo), and privacy boundaries [Inference]
---
Common Patterns
App Startup Checklist
1. Initialize dependencies
- Configure DI container (Hilt/Koin/Swinject)
- Set up logging and crash reporting
- Initialize analytics
2. Check authentication state
- Validate stored tokens
- Refresh if needed
- Route to login or main screen
3. Configure app state
- Load user preferences
- Set up push notification handlers
- Initialize deep link handling
Offline-First Architecture
1. Local-first data access
- Always read from local database
- Display cached data immediately
- Show loading indicator for sync
2. Background sync
- Queue write operations
- Sync when connectivity available
- Handle conflict resolution
3. Optimistic updates
- Update UI immediately
- Sync in background
- Rollback on failurePush Notification Setup
iOS (APNs):
1. Enable Push Notifications capability
2. Request user permission
3. Register for remote notifications
4. Handle device token
5. Implement notification delegate
Android (FCM):
1. Add Firebase to project
2. Implement FirebaseMessagingService
3. Handle notification/data messages
4. Manage notification channels (Android 8+)
5. Handle background/foreground states---
Performance Optimization
| Area | iOS | Android | Metric |
|---|---|---|---|
| Launch time | Pre-warm, lazy loading | Cold start optimization | < 2s cold start |
| List scrolling | LazyVStack, prefetch | LazyColumn, paging | 60 FPS |
| Image loading | AsyncImage, cache | Coil/Glide, disk cache | < 100ms visible |
| Memory | Instruments profiling | LeakCanary, Profiler | No memory leaks |
| Battery | Background App Refresh limits | Doze mode compliance | Minimal drain |
---
App Store Deployment Checklist
iOS App Store
- [ ] App icons (all required sizes)
- [ ] Launch screen configured
- [ ] Privacy manifest per target and embedded frameworks (iOS 18+)
- [ ] Required-reason APIs declared with justifications
- [ ] Third-party SDK privacy manifests attached; SDK signature attestation (iOS 19+)
- [ ] Info.plist permissions explanations
- [ ] App Store screenshots (all device sizes)
- [ ] App Store description and keywords
- [ ] Privacy policy URL
- [ ] TestFlight beta testing
Google Play Store
- [ ] App icons and feature graphic
- [ ] Store listing screenshots
- [ ] Privacy policy URL
- [ ] Content rating questionnaire
- [ ] Target API level compliance
- [ ] Data safety form
- [ ] Internal/closed/open testing tracks
---
Navigation
Resources
- references/ios-best-practices.md — iOS architecture, concurrency, testing, performance, defensive decoding, and accessibility
- references/android-best-practices.md — Android/Kotlin architecture, coroutines, Compose, testing, performance
- references/operational-playbook.md — Mobile architecture patterns, platform-specific guides, security notes, and decision tables
- references/cross-platform-comparison.md — React Native vs Flutter vs KMP vs native: performance, ecosystem, CI/CD, migration paths
- references/mobile-testing-patterns.md — Testing pyramid, XCTest, Espresso, Detox, Maestro, device farms, performance testing
- references/offline-first-architecture.md — Local databases, sync strategies, conflict resolution, background sync, optimistic updates
- references/push-notifications-guide.md — APNs, FCM, permission patterns, notification channels, rich notifications, analytics
- references/deep-linking-guide.md — Universal Links, App Links, deferred deep links, React Native integration, routing architecture
- data/sources.json — Curated external references by platform
Shared Checklists
- ../software-clean-code-standard/assets/checklists/mobile-release-checklist.md — Product-agnostic mobile release readiness checklist (core + optional AI)
Shared Utilities (Centralized patterns — extract, don't duplicate)
- ../software-clean-code-standard/utilities/auth-utilities.md — Argon2id, jose JWT, OAuth 2.1/PKCE (backend auth for mobile clients)
- ../software-clean-code-standard/utilities/error-handling.md — Error patterns, Result types
- ../software-clean-code-standard/utilities/resilience-utilities.md — Retry, circuit breaker for network calls
- ../software-clean-code-standard/utilities/logging-utilities.md — Structured logging patterns
- ../software-clean-code-standard/utilities/testing-utilities.md — Test factories, fixtures, mocks
- ../software-clean-code-standard/references/clean-code-standard.md — Canonical clean code rules (
CC-*) for citation
Templates
- Swift: assets/swift/template-swift.md, assets/swift/template-swift-concurrency.md, assets/swift/template-swift-combine.md, assets/swift/template-swift-performance.md, assets/swift/template-swift-testing.md
- SwiftUI: assets/swiftui/template-swiftui-advanced.md
- Kotlin/Android: assets/kotlin/template-kotlin.md, assets/kotlin/template-kotlin-coroutines.md, assets/kotlin/template-kotlin-compose-advanced.md, assets/kotlin/template-kotlin-testing.md
- Cross-platform: assets/cross-platform/template-platform-patterns.md, assets/cross-platform/template-webview.md
---
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Blocking main thread | UI freezes, ANRs | Use async/coroutines for all I/O |
| Massive view controllers | Hard to test/maintain | Extract to MVVM/services |
| Hardcoded strings | No localization | Use NSLocalizedString/strings.xml |
| Ignoring lifecycle | Memory leaks, crashes | Respect activity/view lifecycle |
| No offline handling | Poor UX without network | Cache data, queue operations |
| Storing secrets in code | Security vulnerability | Use Keychain/Keystore |
Using decode() without fallback | Crashes on missing/malformed API data | Use decodeIfPresent() with defaults |
| Missing @Bindable for @Observable | NavigationStack bindings don't work | Add @Bindable var vm = vm in body |
---
Trend Awareness Protocol
IMPORTANT: When users ask recommendation questions about mobile development, you MUST use a web search capability (if available) to check current trends before answering. If web search is unavailable, say so and answer using data/sources.json, clearly flagging that the recommendation may be stale.
Trigger Conditions
- "What's the best mobile framework for [use case]?"
- "What should I use for [cross-platform/native/hybrid]?"
- "What's the latest in iOS/Android development?"
- "Current best practices for [Swift/Kotlin/React Native]?"
- "Is [React Native/Flutter/Expo] still relevant in 2026?"
- "[React Native] vs [Flutter] vs [native]?"
- "Best approach for [offline/push/deep linking]?"
Required Searches
1. Search: "mobile development best practices 2026" 2. Search: "[iOS/Android/React Native/Flutter] updates 2026" 3. Search: "mobile framework comparison 2026" 4. Search: "[Expo/Swift/Kotlin] new features 2026"
What to Report
After searching, provide:
- Current landscape: What frameworks/approaches are popular NOW
- Emerging trends: New patterns or tools gaining traction
- Deprecated/declining: Approaches that are losing relevance
- Recommendation: Based on fresh data and recent releases
Example Topics (verify with fresh search)
- Current iOS + Swift Concurrency migration guidance
- Current Play target SDK policy and identity/auth guidance
- React Native New Architecture maturity and upgrade pain points
- Expo-managed vs bare React Native tradeoffs
- Flutter vs React Native vs KMP ecosystem in 2026
- Compose Multiplatform readiness for iOS in 2026
---
Related Skills
- ../software-frontend/SKILL.md — Web-facing UI patterns and Next.js integration
- ../software-backend/SKILL.md — API design, auth, and backend contracts for mobile clients
- ../qa-testing-strategy/SKILL.md — Mobile CI, test strategy, and reliability gates
- ../qa-resilience/SKILL.md — Resilience patterns for networked mobile apps
- ../qa-testing-ios/SKILL.md — iOS-focused test planning, XCTest/Swift Testing patterns, device matrix, and app health checks
- ../software-ui-ux-design/SKILL.md — Mobile UI/UX design patterns and accessibility
Fact-Checking
- Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.
- Prefer primary sources; report source links and dates for volatile information.
- If web access is unavailable, state the limitation and mark guidance as unverified.
Mobile Platform Patterns Reference
Comprehensive reference for iOS (SwiftUI, UIKit), Android (Jetpack Compose, Views), and cross-platform mobile development patterns including navigation, state management, networking, storage, and platform-specific features.
---
Table of Contents
iOS Patterns:
- Navigation (SwiftUI)
- Navigation (UIKit)
- State Management (SwiftUI)
- Network Requests (iOS)
- Local Storage (Core Data)
- Authentication (iOS)
- Push Notifications (iOS)
Android Patterns:
- Navigation (Jetpack Compose)
- State Management (Jetpack Compose)
- Network Requests (Android)
- Local Storage (Room)
- Authentication (Android)
- Push Notifications (Android)
---
Navigation (iOS - SwiftUI)
Use when: Implementing navigation in iOS apps.
NavigationStack (iOS 17+)
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
NavigationLink("Users", value: Route.users)
NavigationLink("Settings", value: Route.settings)
}
.navigationDestination(for: Route.self) { route in
switch route {
case .users:
UsersView()
case .settings:
SettingsView()
case .userDetail(let id):
UserDetailView(userId: id)
}
}
}
}
}
enum Route: Hashable {
case users
case settings
case userDetail(String)
}TabView
struct MainTabView: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(0)
ProfileView()
.tabItem {
Label("Profile", systemImage: "person")
}
.tag(1)
}
}
}Checklist:
- [ ] Type-safe navigation
- [ ] Deep linking support
- [ ] Back navigation handling
- [ ] State preservation
- [ ] Tab bar for primary navigation
---
Navigation (Android - Jetpack Compose)
Use when: Implementing navigation in Android apps.
Navigation Component
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToDetail = { id ->
navController.navigate("detail/$id")
}
)
}
composable(
route = "detail/{userId}",
arguments = listOf(
navArgument("userId") { type = NavType.StringType }
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
DetailScreen(userId = userId)
}
}
}Bottom Navigation
@Composable
fun MainScreen() {
val navController = rememberNavController()
val items = listOf(
Screen.Home,
Screen.Search,
Screen.Profile
)
Scaffold(
bottomBar = {
NavigationBar {
items.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(screen.title) },
selected = currentRoute == screen.route,
onClick = { navController.navigate(screen.route) }
)
}
}
}
) { paddingValues ->
NavHost(
navController = navController,
startDestination = Screen.Home.route,
modifier = Modifier.padding(paddingValues)
) {
// Navigation graph
}
}
}Checklist:
- [ ] Safe Args for type safety
- [ ] Deep linking configured
- [ ] Back stack management
- [ ] Navigation animations
- [ ] Bottom navigation for primary sections
---
State Management (iOS - SwiftUI)
Use when: Managing app state in iOS.
@State (Local State)
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}@Observable (iOS 18+)
import Observation
@Observable
class UserViewModel {
var users: [User] = []
var isLoading = false
var errorMessage: String?
func fetchUsers() async {
isLoading = true
defer { isLoading = false }
do {
users = try await APIService.shared.getUsers()
} catch {
errorMessage = error.localizedDescription
}
}
}
struct UsersView: View {
@State private var viewModel = UserViewModel()
var body: some View {
List(viewModel.users) { user in
Text(user.name)
}
.task {
await viewModel.fetchUsers()
}
}
}@Environment (Global State)
@Observable
class AuthManager {
var isAuthenticated = false
var user: User?
func login(email: String, password: String) async throws {
// Login logic
isAuthenticated = true
}
func logout() {
isAuthenticated = false
user = nil
}
}
@main
struct MyApp: App {
@State private var authManager = AuthManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(authManager)
}
}
}
struct ProfileView: View {
@Environment(AuthManager.self) private var authManager
var body: some View {
if authManager.isAuthenticated {
Text("Welcome, \(authManager.user?.name ?? "")")
}
}
}Checklist:
- [ ] @State for local UI state
- [ ] @Observable for view models (iOS 18+)
- [ ] @Environment for global state
- [ ] Async/await for async operations
---
State Management (Android - Jetpack Compose)
Use when: Managing app state in Android.
remember (Local State)
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}ViewModel (Screen-Level)
class UserViewModel : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun fetchUsers() {
viewModelScope.launch {
_isLoading.value = true
try {
_users.value = apiService.getUsers()
} catch (e: Exception) {
// Handle error
} finally {
_isLoading.value = false
}
}
}
}
@Composable
fun UsersScreen(viewModel: UserViewModel = viewModel()) {
val users by viewModel.users.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
LaunchedEffect(Unit) {
viewModel.fetchUsers()
}
if (isLoading) {
CircularProgressIndicator()
} else {
LazyColumn {
items(users) { user ->
Text(user.name)
}
}
}
}Checklist:
- [ ] remember for local state
- [ ] ViewModel for business logic
- [ ] StateFlow for observable state
- [ ] collectAsState for UI updates
- [ ] viewModelScope for coroutines
---
Network Requests (iOS)
Use when: Making API calls in iOS apps.
URLSession with async/await
struct APIService {
static let shared = APIService()
private let baseURL = "https://api.example.com"
func getUsers() async throws -> [User] {
guard let url = URL(string: "\(baseURL)/users") else {
throw APIError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.serverError
}
let users = try JSONDecoder().decode([User].self, from: data)
return users
}
func createUser(_ user: CreateUserRequest) async throws -> User {
guard let url = URL(string: "\(baseURL)/users") else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(user)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.serverError
}
return try JSONDecoder().decode(User.self, from: data)
}
}
enum APIError: Error {
case invalidURL
case serverError
case decodingError
}Checklist:
- [ ] Error handling
- [ ] Response validation
- [ ] JSON encoding/decoding
- [ ] Authentication headers
- [ ] Request timeout configuration
- [ ] Network reachability check
---
Network Requests (Android)
Use when: Making API calls in Android apps.
Retrofit with Coroutines
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
@GET("users/{id}")
suspend fun getUser(@Path("id") id: String): User
@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): User
@PUT("users/{id}")
suspend fun updateUser(
@Path("id") id: String,
@Body user: UpdateUserRequest
): User
@DELETE("users/{id}")
suspend fun deleteUser(@Path("id") id: String)
}
object RetrofitClient {
private const val BASE_URL = "https://api.example.com/"
val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
// Usage in ViewModel
class UserViewModel : ViewModel() {
fun fetchUsers() {
viewModelScope.launch {
try {
val users = RetrofitClient.apiService.getUsers()
_users.value = users
} catch (e: Exception) {
_error.value = e.message
}
}
}
}Checklist:
- [ ] Retrofit interface defined
- [ ] Coroutines for async calls
- [ ] Error handling
- [ ] Authentication interceptor
- [ ] Logging interceptor (debug)
- [ ] Network timeout configuration
---
Local Storage (iOS - Core Data)
Use when: Persisting data locally in iOS.
Core Data Setup
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Core Data failed to load: \(error)")
}
}
}
func save() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
print("Failed to save: \(error)")
}
}
}
}
// CRUD Operations
class UserRepository {
private let context = PersistenceController.shared.container.viewContext
func fetchUsers() -> [UserEntity] {
let request = UserEntity.fetchRequest()
do {
return try context.fetch(request)
} catch {
print("Failed to fetch users: \(error)")
return []
}
}
func createUser(name: String, email: String) {
let user = UserEntity(context: context)
user.id = UUID()
user.name = name
user.email = email
PersistenceController.shared.save()
}
func deleteUser(_ user: UserEntity) {
context.delete(user)
PersistenceController.shared.save()
}
}Checklist:
- [ ] Data model defined
- [ ] Persistent container initialized
- [ ] Context management
- [ ] Error handling
- [ ] Background context for heavy operations
---
Local Storage (Android - Room)
Use when: Persisting data locally in Android.
Room Setup
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val email: String,
val createdAt: Long
)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAll(): Flow<List<UserEntity>>
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getById(id: String): UserEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: UserEntity)
@Update
suspend fun update(user: UserEntity)
@Delete
suspend fun delete(user: UserEntity)
@Query("DELETE FROM users")
suspend fun deleteAll()
}
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
// Repository
class UserRepository(private val userDao: UserDao) {
val allUsers: Flow<List<UserEntity>> = userDao.getAll()
suspend fun insert(user: UserEntity) {
userDao.insert(user)
}
suspend fun delete(user: UserEntity) {
userDao.delete(user)
}
}Checklist:
- [ ] Entity classes defined
- [ ] DAO interfaces created
- [ ] Database class configured
- [ ] Migration strategy
- [ ] Type converters for complex types
- [ ] Flow for reactive queries
---
Authentication (iOS)
Use when: Implementing user authentication.
Token Storage (Keychain)
import Security
class KeychainManager {
static let shared = KeychainManager()
func save(token: String, key: String) {
let data = Data(token.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
func get(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
guard let data = result as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
}Checklist:
- [ ] Secure token storage (Keychain)
- [ ] Token refresh logic
- [ ] Biometric authentication
- [ ] Session management
- [ ] Logout functionality
---
Authentication (Android)
Use when: Implementing user authentication.
Token Storage (EncryptedSharedPreferences)
class SecureStorage(context: Context) {
private val sharedPreferences = EncryptedSharedPreferences.create(
"secure_prefs",
MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
context,
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 clearToken() {
sharedPreferences.edit()
.remove("auth_token")
.apply()
}
}---
Push Notifications (iOS)
Use when: Implementing push notifications.
APNs Setup
import UserNotifications
class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
static let shared = NotificationManager()
func requestAuthorization() {
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .sound, .badge]
) { granted, error in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
}
// In AppDelegate
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("Device Token: \(token)")
// Send token to your server
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound])
}
}Checklist:
- [ ] Permission requested
- [ ] Device token obtained
- [ ] Token sent to server
- [ ] Notification handling (foreground/background)
- [ ] Deep linking from notifications
---
Push Notifications (Android)
Use when: Implementing push notifications.
FCM Setup
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d("FCM", "Token: $token")
// Send token to your server
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
remoteMessage.notification?.let {
showNotification(it.title, it.body)
}
}
private fun showNotification(title: String?, body: String?) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
notificationManager.notify(0, notification)
}
}---
This reference provides comprehensive platform-specific patterns for building production-ready mobile applications on iOS and Android.
WebView Wrapper Template (iOS + Android)
Wrap your existing web application in native mobile containers using WKWebView (iOS) and WebView (Android). Perfect for Progressive Web Apps (PWAs) or when you want native app distribution for a web application.
---
When to Use WebView Approach
Good Fit:
- Existing web application you want to distribute via app stores
- Content-heavy apps (news, blogs, documentation)
- Rapid prototyping before building native UI
- Web-first strategy with native app as secondary channel
- Frequent updates without app store review delays
Not Recommended:
- Performance-critical apps (games, video editing)
- Heavy use of device hardware (camera, sensors)
- Apps requiring complex native UI patterns
- Offline-first mobile experiences
---
Architecture
WebView App
├── Native Shell (iOS/Android)
│ ├── WebView container
│ ├── JavaScript bridge
│ ├── Deep linking
│ ├── Push notifications
│ └── Native features (camera, location, etc.)
└── Web Application
├── Responsive web UI
├── Service Worker (offline support)
└── Web APIs---
Part 1: iOS WebView Implementation
Project Structure (iOS)
YourWebApp-iOS/
├── YourWebApp/
│ ├── App/
│ │ ├── YourWebAppApp.swift
│ │ └── ContentView.swift
│ ├── WebView/
│ │ ├── WebView.swift
│ │ ├── WebViewCoordinator.swift
│ │ └── JavaScriptBridge.swift
│ ├── Services/
│ │ ├── NotificationService.swift
│ │ └── DeepLinkService.swift
│ └── Utilities/
│ └── Constants.swift
└── Info.plist1. WebView Component (iOS)
WebView/WebView.swift
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: URL
@Binding var isLoading: Bool
@Binding var error: Error?
func makeCoordinator() -> WebViewCoordinator {
WebViewCoordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
// Enable caching
configuration.websiteDataStore = .default()
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.scrollView.contentInsetAdjustmentBehavior = .never
// Add JavaScript bridge
let bridge = JavaScriptBridge()
bridge.setupBridge(for: webView)
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
if webView.url != url {
let request = URLRequest(url: url)
webView.load(request)
}
}
}WebView/WebViewCoordinator.swift
import WebKit
class WebViewCoordinator: NSObject, WKNavigationDelegate {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.isLoading = false
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
parent.isLoading = false
parent.error = error
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// Handle external links
if let url = navigationAction.request.url,
!url.absoluteString.hasPrefix("https://yourdomain.com") {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}
}WebView/JavaScriptBridge.swift
import WebKit
class JavaScriptBridge: NSObject, WKScriptMessageHandler {
func setupBridge(for webView: WKWebView) {
let contentController = webView.configuration.userContentController
// Add message handlers
contentController.add(self, name: "nativeLog")
contentController.add(self, name: "nativeShare")
contentController.add(self, name: "nativeNotification")
// Inject JavaScript
let script = """
window.nativeBridge = {
log: function(message) {
window.webkit.messageHandlers.nativeLog.postMessage(message);
},
share: function(data) {
window.webkit.messageHandlers.nativeShare.postMessage(data);
},
requestNotificationPermission: function() {
window.webkit.messageHandlers.nativeNotification.postMessage('request');
}
};
"""
let userScript = WKUserScript(
source: script,
injectionTime: .atDocumentEnd,
forMainFrameOnly: false
)
contentController.addUserScript(userScript)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "nativeLog":
if let body = message.body as? String {
print("Web Log:", body)
}
case "nativeShare":
if let data = message.body as? [String: Any],
let text = data["text"] as? String {
shareContent(text: text)
}
case "nativeNotification":
requestNotificationPermission()
default:
break
}
}
private func shareContent(text: String) {
DispatchQueue.main.async {
let activityVC = UIActivityViewController(
activityItems: [text],
applicationActivities: nil
)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootViewController = windowScene.windows.first?.rootViewController {
rootViewController.present(activityVC, animated: true)
}
}
}
private func requestNotificationPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
print("Notification permission granted:", granted)
}
}
}2. Main App (iOS)
App/ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var isLoading = false
@State private var error: Error?
@State private var showError = false
private let webURL = URL(string: "https://yourdomain.com")!
var body: some View {
ZStack {
WebView(url: webURL, isLoading: $isLoading, error: $error)
.edgesIgnoringSafeArea(.all)
if isLoading {
VStack {
ProgressView()
.scaleEffect(1.5)
Text("Loading...")
.padding(.top, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white.opacity(0.9))
}
}
.alert("Error", isPresented: $showError) {
Button("OK") { showError = false }
} message: {
Text(error?.localizedDescription ?? "An error occurred")
}
.onChange(of: error) { newError in
showError = newError != nil
}
}
}3. Configuration (iOS)
Info.plist
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>yourdomain.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<false/>
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access for taking photos</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access</string>---
Part 2: Android WebView Implementation
Project Structure (Android)
app/
├── src/main/
│ ├── java/com/example/yourwebapp/
│ │ ├── MainActivity.kt
│ │ ├── WebAppInterface.kt
│ │ └── WebViewClient.kt
│ ├── res/
│ │ └── layout/
│ │ └── activity_main.xml
│ └── AndroidManifest.xml
└── build.gradle.kts1. WebView Activity (Android)
MainActivity.kt
package com.example.yourwebapp
import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.*
import android.widget.ProgressBar
import android.view.View
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var progressBar: ProgressBar
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webview)
progressBar = findViewById(R.id.progress_bar)
setupWebView()
webView.loadUrl("https://yourdomain.com")
}
private fun setupWebView() {
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
mediaPlaybackRequiresUserGesture = false
setSupportZoom(true)
builtInZoomControls = false
useWideViewPort = true
loadWithOverviewMode = true
}
// Add JavaScript interface
webView.addJavascriptInterface(WebAppInterface(this), "Android")
// Set WebViewClient
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val url = request?.url.toString()
// Handle external URLs
if (!url.startsWith("https://yourdomain.com")) {
// Open in external browser
return true
}
return false
}
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
progressBar.visibility = View.VISIBLE
}
override fun onPageFinished(view: WebView?, url: String?) {
progressBar.visibility = View.GONE
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
progressBar.visibility = View.GONE
// Show error message
}
}
// Set WebChromeClient for features like file upload, geolocation
webView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progressBar.progress = newProgress
}
override fun onPermissionRequest(request: PermissionRequest?) {
// Handle permission requests (camera, microphone, etc.)
request?.grant(request.resources)
}
}
}
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onDestroy() {
webView.destroy()
super.onDestroy()
}
}WebAppInterface.kt
package com.example.yourwebapp
import android.content.Context
import android.content.Intent
import android.webkit.JavascriptInterface
import android.widget.Toast
class WebAppInterface(private val context: Context) {
@JavascriptInterface
fun log(message: String) {
android.util.Log.d("WebApp", message)
}
@JavascriptInterface
fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
@JavascriptInterface
fun share(text: String) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
context.startActivity(Intent.createChooser(shareIntent, "Share via"))
}
@JavascriptInterface
fun getDeviceInfo(): String {
return """
{
"platform": "Android",
"version": "${android.os.Build.VERSION.RELEASE}",
"model": "${android.os.Build.MODEL}"
}
""".trimIndent()
}
}2. Layout (Android)
res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_alignParentTop="true"
android:visibility="gone" />
</RelativeLayout>3. Configuration (Android)
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.YourWebApp"
android:usesCleartextTraffic="false">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep linking -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="yourdomain.com" />
</intent-filter>
</activity>
</application>
</manifest>---
Part 3: Web Application Integration
JavaScript Bridge Usage
In your web application:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Web App</title>
</head>
<body>
<button onclick="shareContent()">Share</button>
<button onclick="logMessage()">Log to Native</button>
<script>
// Detect platform
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isAndroid = /Android/.test(navigator.userAgent);
// Share content
function shareContent() {
const text = "Check out this awesome app!";
if (isIOS && window.nativeBridge) {
window.nativeBridge.share({ text: text });
} else if (isAndroid && window.Android) {
window.Android.share(text);
} else if (navigator.share) {
navigator.share({ text: text });
}
}
// Log message
function logMessage() {
const message = "Hello from web!";
if (isIOS && window.nativeBridge) {
window.nativeBridge.log(message);
} else if (isAndroid && window.Android) {
window.Android.log(message);
} else {
console.log(message);
}
}
// Request notification permission
function requestNotifications() {
if (isIOS && window.nativeBridge) {
window.nativeBridge.requestNotificationPermission();
} else if ('Notification' in window) {
Notification.requestPermission();
}
}
</script>
</body>
</html>Service Worker (Offline Support)
service-worker.js
const CACHE_NAME = 'your-app-v1';
const urlsToCache = [
'/',
'/styles.css',
'/app.js',
'/offline.html'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
.catch(() => caches.match('/offline.html'))
);
});---
Best Practices
Performance
- Enable caching for faster load times
- Minimize JavaScript bridge calls
- Use lazy loading for images and assets
- Implement service worker for offline support
Security
- Use HTTPS only
- Validate all JavaScript bridge inputs
- Restrict external URL navigation
- Implement Content Security Policy
User Experience
- Show loading indicators
- Handle errors gracefully
- Support back button navigation
- Maintain scroll position on app resume
Testing
- Test on various devices and OS versions
- Test offline functionality
- Verify deep linking works
- Test JavaScript bridge communication
---
Deployment Checklist
iOS:
- [ ] Configure App Transport Security in Info.plist
- [ ] Add required permission descriptions
- [ ] Set up deep linking URL schemes
- [ ] Test on physical devices
- [ ] Submit to App Store
Android:
- [ ] Add required permissions in AndroidManifest.xml
- [ ] Configure ProGuard rules for release builds
- [ ] Test deep linking
- [ ] Generate signed APK/AAB
- [ ] Submit to Google Play
---
Advanced Features
Deep Linking
iOS - Handle Deep Links:
// AppDelegate.swift or Scene
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
// Extract path and query parameters
if url.scheme == "yourapp" {
let path = url.path
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let queryItems = components?.queryItems
// Navigate WebView to corresponding URL
let webURL = "https://yourdomain.com\(path)"
// Update WebView URL
}
}Android - Handle Deep Links:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let { handleIntent(it) }
}
private fun handleIntent(intent: Intent) {
val data: Uri? = intent.data
data?.let {
val webUrl = "https://yourdomain.com${it.path}?${it.query}"
webView.loadUrl(webUrl)
}
}File Upload Support
iOS - File Upload:
class WebViewCoordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
func webView(
_ webView: WKWebView,
runOpenPanelWith parameters: WKOpenPanelParameters,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping ([URL]?) -> Void
) {
let picker = UIDocumentPickerViewController(
forOpeningContentTypes: [.image, .pdf]
)
picker.delegate = self
picker.allowsMultipleSelection = parameters.allowsMultipleSelection
// Present picker
completionHandler([])
}
}Android - File Upload:
private var fileUploadCallback: ValueCallback<Array<Uri>>? = null
webView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
fileUploadCallback = filePathCallback
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
startActivityForResult(
Intent.createChooser(intent, "Choose File"),
FILE_CHOOSER_REQUEST_CODE
)
return true
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
fileUploadCallback?.onReceiveValue(
WebChromeClient.FileChooserParams.parseResult(resultCode, data)
)
fileUploadCallback = null
}
super.onActivityResult(requestCode, resultCode, data)
}Push Notifications
iOS - Firebase Cloud Messaging:
import FirebaseMessaging
class NotificationService {
func configure() {
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .sound, .badge]
) { granted, _ in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
Messaging.messaging().token { token, error in
if let token = token {
// Send token to web app
self.sendTokenToWeb(token)
}
}
}
private func sendTokenToWeb(_ token: String) {
let script = """
if (window.receiveNativeToken) {
window.receiveNativeToken('\(token)');
}
"""
// Execute in WebView
}
}Android - Firebase Cloud Messaging:
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
// Send to web app via JavaScript bridge
MainActivity.instance?.runOnUiThread {
MainActivity.instance?.webView?.evaluateJavascript(
"window.receiveNativeToken && window.receiveNativeToken('$token')",
null
)
}
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
// Show notification or pass to web app
message.data["webUrl"]?.let { url ->
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("url", url)
}
startActivity(intent)
}
}
}Cookie Management
iOS - Cookie Sync:
class CookieManager {
static func syncCookies(for url: URL) {
let cookieStore = WKWebsiteDataStore.default().httpCookieStore
// Get cookies from HTTPCookieStorage
if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
for cookie in cookies {
cookieStore.setCookie(cookie)
}
}
}
static func clearCookies() {
let dataStore = WKWebsiteDataStore.default()
dataStore.removeData(
ofTypes: [WKWebsiteDataTypeCookies],
modifiedSince: Date(timeIntervalSince1970: 0)
) {
print("Cookies cleared")
}
}
}Android - Cookie Sync:
class CookieManager {
companion object {
fun syncCookies(url: String) {
val cookieManager = android.webkit.CookieManager.getInstance()
cookieManager.setAcceptCookie(true)
cookieManager.setAcceptThirdPartyCookies(webView, true)
// Set specific cookies
cookieManager.setCookie(url, "session_token=abc123")
cookieManager.flush()
}
fun clearCookies() {
val cookieManager = android.webkit.CookieManager.getInstance()
cookieManager.removeAllCookies { success ->
Log.d("WebView", "Cookies cleared: $success")
}
}
}
}Offline Detection
Web App - Detect Online/Offline:
// In your web application
window.addEventListener('online', () => {
console.log('Back online');
// Sync pending data
});
window.addEventListener('offline', () => {
console.log('Gone offline');
// Show offline UI
});
// Check current status
if (!navigator.onLine) {
showOfflineMessage();
}Pull-to-Refresh
iOS - Pull to Refresh:
struct WebView: UIViewRepresentable {
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
let refreshControl = UIRefreshControl()
refreshControl.addTarget(
context.coordinator,
action: #selector(WebViewCoordinator.refresh),
for: .valueChanged
)
webView.scrollView.refreshControl = refreshControl
webView.scrollView.bounces = true
return webView
}
}
class WebViewCoordinator {
@objc func refresh(_ sender: UIRefreshControl) {
parent.webView?.reload()
sender.endRefreshing()
}
}Android - Pull to Refresh:
// Use SwipeRefreshLayout
class MainActivity : AppCompatActivity() {
private lateinit var swipeRefresh: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
swipeRefresh = findViewById(R.id.swipe_refresh)
swipeRefresh.setOnRefreshListener {
webView.reload()
swipeRefresh.isRefreshing = false
}
}
}Error Handling
Custom Error Pages:
<!-- offline.html -->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: #f5f5f5;
}
.error-container {
text-align: center;
padding: 20px;
}
button {
margin-top: 20px;
padding: 12px 24px;
background: #007AFF;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
}
</style>
</head>
<body>
<div class="error-container">
<h1>You're Offline</h1>
<p>Please check your internet connection and try again.</p>
<button onclick="location.reload()">Retry</button>
</div>
</body>
</html>---
Performance Optimization
Image Optimization
// iOS - Optimize images before loading
webView.configuration.preferences.minimumFontSize = 10
webView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = false
// Limit memory usage
webView.configuration.processPool = WKProcessPool()// Android - Optimize WebView performance
webView.settings.apply {
// Enable hardware acceleration
setRenderPriority(WebSettings.RenderPriority.HIGH)
// Optimize caching
cacheMode = WebSettings.LOAD_DEFAULT
setAppCacheEnabled(true)
setAppCachePath(cacheDir.absolutePath)
// Reduce memory usage
setGeolocationEnabled(false)
setSaveFormData(false)
}Memory Management
// iOS - Clear cache
func clearCache() {
let dataStore = WKWebsiteDataStore.default()
let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
let date = Date(timeIntervalSince1970: 0)
dataStore.removeData(ofTypes: dataTypes, modifiedSince: date) {
print("Cache cleared")
}
}// Android - Clear cache
fun clearCache() {
webView.clearCache(true)
webView.clearHistory()
val cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null)
}---
Security Hardening
SSL Pinning (iOS)
class WebViewCoordinator: NSObject, WKNavigationDelegate {
func webView(
_ webView: WKWebView,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Verify certificate
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
}
}Content Security Policy
<!-- Add to your web app -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;">---
Monitoring & Analytics
Track Page Views
// In your web app
function trackPageView(page) {
if (window.nativeBridge) {
window.nativeBridge.trackPageView(page);
}
}
// Track route changes
window.addEventListener('popstate', () => {
trackPageView(window.location.pathname);
});// iOS - Analytics integration
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
if message.name == "trackPageView",
let page = message.body as? String {
Analytics.logEvent("page_view", parameters: ["page": page])
}
}---
This enhanced template provides production-ready WebView wrapper implementation with advanced features including deep linking, file uploads, push notifications, offline support, and security hardening for both iOS and Android platforms.
Jetpack Compose Advanced Patterns Reference
Comprehensive guide to advanced Jetpack Compose patterns including custom modifiers, side effects, animations, and Kotlin-specific features for building sophisticated Android UIs.
---
Table of Contents
1. Custom Modifiers 2. Side Effects 3. State Hoisting & Delegation 4. Advanced Animations 5. Performance Optimization
---
Custom Modifiers
Use when: Creating reusable UI styling and behavior.
Custom Modifier
fun Modifier.cardStyle(
backgroundColor: Color = MaterialTheme.colorScheme.surface,
cornerRadius: Dp = 12.dp,
elevation: Dp = 4.dp
): Modifier = this
.shadow(elevation, RoundedCornerShape(cornerRadius))
.background(backgroundColor, RoundedCornerShape(cornerRadius))
.padding(16.dp)
// Usage
Text(
text = "Hello, World!",
modifier = Modifier.cardStyle()
)
Column(
modifier = Modifier
.fillMaxWidth()
.cardStyle(
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
cornerRadius = 16.dp
)
) {
Text("Custom Card")
}Conditional Modifier
fun Modifier.conditional(
condition: Boolean,
modifier: Modifier.() -> Modifier
): Modifier = if (condition) {
then(modifier(Modifier))
} else {
this
}
// Usage
Text(
text = "Conditional Styling",
modifier = Modifier
.conditional(isHighlighted) {
background(Color.Yellow)
.border(2.dp, Color.Red)
}
)Modifier with Parameters
fun Modifier.shimmer(
isLoading: Boolean,
shimmerColor: Color = Color.LightGray.copy(alpha = 0.6f)
): Modifier = composed {
if (!isLoading) return@composed this
val transition = rememberInfiniteTransition(label = "shimmer")
val translateAnim by transition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1200, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart
),
label = "shimmer_translate"
)
background(
brush = Brush.linearGradient(
colors = listOf(
shimmerColor.copy(alpha = 0.6f),
shimmerColor.copy(alpha = 0.2f),
shimmerColor.copy(alpha = 0.6f)
),
start = Offset(translateAnim, 0f),
end = Offset(translateAnim + 200f, 0f)
)
)
}
// Usage
Box(
modifier = Modifier
.size(200.dp, 20.dp)
.shimmer(isLoading = true)
)Checklist:
- [ ] Extract repeated styling into custom modifiers
- [ ] Use
composedfor stateful modifiers - [ ] Keep modifiers composable and reusable
- [ ] Document modifier parameters
- [ ] Use conditional logic for dynamic styling
---
Side Effects
Use when: Performing side effects in composables that sync with Compose lifecycle.
LaunchedEffect
@Composable
fun UserScreen(userId: String, viewModel: UserViewModel) {
LaunchedEffect(userId) {
// Runs when userId changes
viewModel.loadUser(userId)
}
// UI code
}
// One-time effect
@Composable
fun AnalyticsScreen(screenName: String) {
LaunchedEffect(Unit) {
// Runs once when composable enters composition
analyticsService.logScreenView(screenName)
}
}DisposableEffect
@Composable
fun BackPressHandler(onBackPressed: () -> Unit) {
val context = LocalContext.current
val backCallback = remember {
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed()
}
}
}
DisposableEffect(context) {
val activity = context as? ComponentActivity
activity?.onBackPressedDispatcher?.addCallback(backCallback)
onDispose {
backCallback.remove()
}
}
}SideEffect
@Composable
fun SystemBarsTheme(isDark: Boolean) {
val view = LocalView.current
SideEffect {
// Runs after every successful recomposition
val window = (view.context as Activity).window
window.statusBarColor = if (isDark) Color.Black else Color.White
}
}rememberCoroutineScope
@Composable
fun SwipeableContent() {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState
) {
Button(
onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Hello!")
}
}
) {
Text("Show Snackbar")
}
}
}derivedStateOf
@Composable
fun ItemList(items: List<Item>) {
val listState = rememberLazyListState()
// Only recomputes when first visible item changes
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
Box {
LazyColumn(state = listState) {
items(items) { item ->
ItemRow(item)
}
}
AnimatedVisibility(visible = showButton) {
FloatingActionButton(
onClick = {
// Scroll to top
}
) {
Icon(Icons.Default.ArrowUpward, null)
}
}
}
}Checklist:
- [ ] Use LaunchedEffect for coroutines tied to keys
- [ ] Use DisposableEffect for cleanup
- [ ] Use SideEffect for synchronizing non-Compose state
- [ ] Use derivedStateOf for computed state
- [ ] Use rememberCoroutineScope for event handlers
---
State Hoisting & Delegation
Use when: Managing state in composable hierarchies.
State Hoisting Pattern
// Stateless composable
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier,
placeholder = { Text("Search") }
)
}
// Stateful composable
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
Column {
SearchBar(
query = query,
onQueryChange = { query = it }
)
SearchResults(query)
}
}Remember with Custom Key
@Composable
fun UserProfile(userId: String) {
// Resets when userId changes
val userData by remember(userId) {
mutableStateOf(UserData())
}
LaunchedEffect(userId) {
// Load new user data
}
}rememberSaveable for Configuration Changes
@Composable
fun FormScreen() {
var name by rememberSaveable { mutableStateOf("") }
var email by rememberSaveable { mutableStateOf("") }
// State survives configuration changes (rotation)
Column {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") }
)
}
}State Delegate
class SearchState {
var query by mutableStateOf("")
private set
var results by mutableStateOf<List<Result>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
fun updateQuery(newQuery: String) {
query = newQuery
}
fun updateResults(newResults: List<Result>) {
results = newResults
isLoading = false
}
}
@Composable
fun rememberSearchState() = remember { SearchState() }
@Composable
fun SearchScreen() {
val searchState = rememberSearchState()
Column {
SearchBar(
query = searchState.query,
onQueryChange = searchState::updateQuery
)
if (searchState.isLoading) {
CircularProgressIndicator()
} else {
SearchResults(searchState.results)
}
}
}Checklist:
- [ ] Hoist state to appropriate level
- [ ] Make composables stateless when possible
- [ ] Use remember for state that doesn't survive config changes
- [ ] Use rememberSaveable for state that should survive
- [ ] Create state holders for complex state logic
---
Advanced Animations
Use when: Creating smooth, interactive animations.
AnimatedVisibility
@Composable
fun ExpandableCard() {
var expanded by remember { mutableStateOf(false) }
Card {
Column {
Row(
modifier = Modifier.clickable { expanded = !expanded }
) {
Text("Card Title")
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = null
)
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Text(
"Card content that appears and disappears with animation",
modifier = Modifier.padding(16.dp)
)
}
}
}
}Animated Content Size
@Composable
fun DynamicContent() {
var showMore by remember { mutableStateOf(false) }
Column(
modifier = Modifier.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
) {
Text("Always visible content")
if (showMore) {
Text("Additional content with animated height")
}
TextButton(onClick = { showMore = !showMore }) {
Text(if (showMore) "Show Less" else "Show More")
}
}
}Animated Value
@Composable
fun ProgressIndicator(progress: Float) {
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = tween(durationMillis = 1000, easing = EaseInOutCubic),
label = "progress"
)
LinearProgressIndicator(
progress = animatedProgress,
modifier = Modifier.fillMaxWidth()
)
}Transition Animation
enum class BoxState { Collapsed, Expanded }
@Composable
fun AnimatedBox() {
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box")
val size by transition.animateDp(
label = "size",
transitionSpec = { spring(stiffness = Spring.StiffnessLow) }
) { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 128.dp
}
}
val color by transition.animateColor(
label = "color"
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colorScheme.primary
BoxState.Expanded -> MaterialTheme.colorScheme.secondary
}
}
Box(
modifier = Modifier
.size(size)
.background(color)
.clickable {
currentState = when (currentState) {
BoxState.Collapsed -> BoxState.Expanded
BoxState.Expanded -> BoxState.Collapsed
}
}
)
}Infinite Animation
@Composable
fun PulsingIcon() {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(800),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
modifier = Modifier.scale(scale)
)
}Checklist:
- [ ] Use AnimatedVisibility for enter/exit animations
- [ ] Use animateContentSize for size changes
- [ ] Use animate*AsState for simple value animations
- [ ] Use Transition for coordinated animations
- [ ] Use InfiniteTransition for continuous animations
---
Performance Optimization
Use when: Optimizing recomposition and rendering performance.
Remember Expensive Computations
@Composable
fun ExpensiveList(items: List<Item>) {
// Avoid recomputing on every recomposition
val processedItems = remember(items) {
items.map { processItem(it) }
}
LazyColumn {
items(processedItems) { item ->
ItemRow(item)
}
}
}Key for LazyColumn Items
@Composable
fun ItemList(items: List<Item>) {
LazyColumn {
items(
items = items,
key = { item -> item.id } // Helps Compose identify items
) { item ->
ItemRow(item)
}
}
}Stable Collections
// Immutable collections prevent unnecessary recompositions
@Immutable
data class UserList(val users: List<User>)
// Or use kotlinx.collections.immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
fun UserScreen(users: ImmutableList<User>) {
// Compose knows this won't change unless reference changes
LazyColumn {
items(users) { user ->
UserRow(user)
}
}
}Skip Recomposition with Stability
// Unstable class - recomposes every time
data class SearchQuery(var text: String)
// Stable class - only recomposes when text changes
@Stable
data class SearchQuery(val text: String)
// Or make it immutable
data class SearchQuery(val text: String) {
fun update(newText: String) = copy(text = newText)
}Avoid Allocation in Composition
// Bad - creates new lambda on every recomposition
@Composable
fun Button() {
Button(onClick = { viewModel.doSomething() }) {
Text("Click")
}
}
// Good - lambda is stable
@Composable
fun Button(viewModel: ViewModel) {
Button(onClick = viewModel::doSomething) {
Text("Click")
}
}Defer State Reads
@Composable
fun ScrollToTopButton(listState: LazyListState) {
val scope = rememberCoroutineScope()
// Bad - reads state in composition phase
val showButton = listState.firstVisibleItemIndex > 0
// Good - defers state read
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(visible = showButton) {
FloatingActionButton(
onClick = {
scope.launch {
listState.animateScrollToItem(0)
}
}
) {
Icon(Icons.Default.ArrowUpward, null)
}
}
}Checklist:
- [ ] Use
rememberfor expensive computations - [ ] Provide
keyfor lazy list items - [ ] Use immutable/stable data classes
- [ ] Avoid allocations in composition
- [ ] Use
derivedStateOffor computed values - [ ] Profile with Layout Inspector
---
Best Practices
1. Modifiers
- Keep custom modifiers focused and reusable
- Use
composedfor stateful modifiers - Document modifier behavior and parameters
- Chain modifiers in logical order
2. Side Effects
- Use the right effect for the job
- Clean up resources in onDispose
- Avoid long-running work in composition
- Use remember for state initialization
3. State Management
- Hoist state to appropriate level
- Make composables stateless when possible
- Use state holders for complex state
- Preserve state across configuration changes
4. Animations
- Use built-in animation APIs
- Coordinate related animations with Transition
- Use appropriate easing functions
- Test animations on real devices
5. Performance
- Minimize recompositions
- Use stable/immutable data
- Provide keys for lists
- Defer expensive computations
- Profile with tools
---
This reference provides advanced Jetpack Compose patterns for building sophisticated, production-ready Android user interfaces.
Kotlin Coroutines Patterns Reference
Comprehensive guide to Kotlin Coroutines patterns including Flow, StateFlow, Channels, and advanced async techniques for Android development.
---
Table of Contents
1. Coroutine Scope Management 2. Flow for Streaming Data 3. StateFlow & SharedFlow 4. Channels for Communication 5. Advanced Patterns
---
Coroutine Scope Management
Use when: Managing lifecycle-aware coroutines in Android components.
ViewModel Scope
class UserViewModel : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
fun fetchUsers() {
viewModelScope.launch {
try {
val result = apiService.getUsers()
_users.value = result
} catch (e: Exception) {
// Handle error
}
}
}
// Coroutine is automatically cancelled when ViewModel is cleared
}Lifecycle Scope
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Cancelled when lifecycle is destroyed
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}Custom Scope with SupervisorJob
class DataRepository {
private val scope = CoroutineScope(
SupervisorJob() + Dispatchers.IO
)
fun fetchData() {
scope.launch {
// Job failure doesn't cancel sibling jobs
try {
val data = apiService.getData()
processData(data)
} catch (e: Exception) {
handleError(e)
}
}
}
fun cleanup() {
scope.cancel()
}
}Checklist:
- [ ] Use viewModelScope for ViewModel coroutines
- [ ] Use lifecycleScope for Activity/Fragment
- [ ] Use SupervisorJob for independent child jobs
- [ ] Cancel scopes in cleanup methods
- [ ] Use appropriate dispatchers (IO, Main, Default)
---
Flow for Streaming Data
Use when: Processing streams of asynchronous data with backpressure.
Flow Builders
// Cold flow - starts emitting when collected
fun getUsers(): Flow<List<User>> = flow {
val users = apiService.getUsers()
emit(users)
}
// Flow from callback
fun locationUpdates(): Flow<Location> = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
trySend(result.lastLocation)
}
}
locationManager.requestLocationUpdates(callback)
awaitClose {
locationManager.removeUpdates(callback)
}
}
// Flow from LiveData
val userFlow: Flow<User> = userLiveData.asFlow()Flow Operators
class PostRepository {
fun getPostsStream(): Flow<List<Post>> = flow {
while (true) {
val posts = apiService.getPosts()
emit(posts)
delay(30_000) // Refresh every 30 seconds
}
}
.map { posts -> posts.filter { it.isPublished } }
.distinctUntilChanged()
.catch { e -> emit(emptyList()) }
.flowOn(Dispatchers.IO)
}
// Usage in ViewModel
class PostViewModel : ViewModel() {
val posts: StateFlow<List<Post>> = repository.getPostsStream()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}Combining Flows
class FeedViewModel : ViewModel() {
private val posts = repository.getPostsFlow()
private val filter = MutableStateFlow(PostFilter.ALL)
val filteredPosts: StateFlow<List<Post>> = combine(
posts,
filter
) { posts, filter ->
when (filter) {
PostFilter.ALL -> posts
PostFilter.STARRED -> posts.filter { it.isStarred }
PostFilter.RECENT -> posts.filter {
it.createdAt > System.currentTimeMillis() - 86400_000
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun setFilter(newFilter: PostFilter) {
filter.value = newFilter
}
}Checklist:
- [ ] Use
flow { }for cold flows - [ ] Use
callbackFlowfor callback-based APIs - [ ] Apply operators (map, filter, catch, etc.)
- [ ] Use
flowOn()to specify dispatcher - [ ] Convert to StateFlow with
stateIn()
---
StateFlow & SharedFlow
Use when: Sharing state or events between components.
StateFlow for State
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val user = repository.getUser(id)
_uiState.update {
it.copy(
user = user,
isLoading = false
)
}
} catch (e: Exception) {
_uiState.update {
it.copy(
error = e.message,
isLoading = false
)
}
}
}
}
}
data class UserUiState(
val user: User? = null,
val isLoading: Boolean = false,
val error: String? = null
)SharedFlow for Events
class EventBus {
private val _events = MutableSharedFlow<Event>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
// Usage in ViewModel
class MainViewModel(private val eventBus: EventBus) : ViewModel() {
init {
viewModelScope.launch {
eventBus.events.collect { event ->
when (event) {
is Event.UserLoggedIn -> handleLogin(event.user)
is Event.UserLoggedOut -> handleLogout()
}
}
}
}
}SharedFlow with Replay
class LocationRepository {
private val _locations = MutableSharedFlow<Location>(
replay = 1, // Cache last location for new collectors
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val locations: SharedFlow<Location> = _locations.asSharedFlow()
suspend fun updateLocation(location: Location) {
_locations.emit(location)
}
}Checklist:
- [ ] Use StateFlow for UI state (always has value)
- [ ] Use SharedFlow for one-time events
- [ ] Use
update { }for atomic state updates - [ ] Set appropriate replay/buffer settings
- [ ] Expose read-only flows with
asStateFlow()/asSharedFlow()
---
Channels for Communication
Use when: Sending values between coroutines with buffering.
Channel Basics
class DownloadManager {
private val downloadChannel = Channel<Download>(Channel.BUFFERED)
fun enqueueDownload(url: String) {
scope.launch {
downloadChannel.send(Download(url))
}
}
init {
scope.launch {
for (download in downloadChannel) {
processDownload(download)
}
}
}
private suspend fun processDownload(download: Download) {
// Download file
}
}Channel with Select
suspend fun selectFromMultipleSources(
channel1: ReceiveChannel<Data>,
channel2: ReceiveChannel<Data>
) {
select<Unit> {
channel1.onReceive { data ->
println("Received from channel1: $data")
}
channel2.onReceive { data ->
println("Received from channel2: $data")
}
}
}Producer-Consumer Pattern
fun CoroutineScope.produceNumbers() = produce<Int> {
var x = 1
while (true) {
send(x++)
delay(100)
}
}
fun CoroutineScope.square(numbers: ReceiveChannel<Int>) = produce<Int> {
for (x in numbers) {
send(x * x)
}
}
// Usage
val numbers = produceNumbers()
val squares = square(numbers)
for (i in 1..5) {
println(squares.receive())
}Checklist:
- [ ] Use Channel for buffered communication
- [ ] Choose appropriate channel capacity
- [ ] Close channels when done
- [ ] Use
producefor channel builders - [ ] Handle channel cancellation
---
Advanced Patterns
Debouncing User Input
class SearchViewModel : ViewModel() {
private val searchQuery = MutableStateFlow("")
val searchResults: StateFlow<List<Result>> = searchQuery
.debounce(300)
.filter { it.isNotBlank() }
.distinctUntilChanged()
.flatMapLatest { query ->
flow {
emit(apiService.search(query))
}.catch { e ->
emit(emptyList())
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun onSearchQueryChanged(query: String) {
searchQuery.value = query
}
}Retry with Exponential Backoff
suspend fun <T> retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(maxRetries) { attempt ->
try {
return block()
} catch (e: Exception) {
if (attempt == maxRetries - 1) throw e
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong()
}
}
throw IllegalStateException("Unreachable")
}
// Usage
val users = retryWithBackoff {
apiService.getUsers()
}Parallel Async with async/await
class UserRepository {
suspend fun fetchUserWithDetails(userId: String): UserDetails {
return coroutineScope {
val userDeferred = async { apiService.getUser(userId) }
val postsDeferred = async { apiService.getUserPosts(userId) }
val friendsDeferred = async { apiService.getUserFriends(userId) }
UserDetails(
user = userDeferred.await(),
posts = postsDeferred.await(),
friends = friendsDeferred.await()
)
}
}
}Limiting Concurrency
class ImageDownloader {
private val semaphore = Semaphore(5) // Max 5 concurrent downloads
suspend fun downloadImages(urls: List<String>): List<Image> {
return coroutineScope {
urls.map { url ->
async {
semaphore.withPermit {
downloadImage(url)
}
}
}.awaitAll()
}
}
private suspend fun downloadImage(url: String): Image {
// Download image
delay(1000)
return Image(url)
}
}Timeout and withContext
class ApiRepository {
suspend fun fetchDataWithTimeout(): Result<Data> {
return try {
withTimeout(5000) { // 5 second timeout
withContext(Dispatchers.IO) {
val data = apiService.getData()
Result.success(data)
}
}
} catch (e: TimeoutCancellationException) {
Result.failure(Exception("Request timed out"))
} catch (e: Exception) {
Result.failure(e)
}
}
}Cancellation and Cleanup
class FileProcessor {
suspend fun processFile(file: File) {
try {
val stream = file.inputStream()
try {
processStream(stream)
} finally {
stream.close() // Cleanup even if cancelled
}
} catch (e: CancellationException) {
println("Processing cancelled")
throw e // Re-throw to propagate cancellation
}
}
private suspend fun processStream(stream: InputStream) {
// Check for cancellation periodically
ensureActive()
// Or use yield()
yield()
// Process data
}
}---
Best Practices
1. Scope Management
- Use viewModelScope/lifecycleScope for lifecycle-aware coroutines
- Create custom scopes with SupervisorJob for independent tasks
- Cancel scopes in cleanup methods
- Use appropriate dispatchers (IO for I/O, Default for CPU-intensive)
2. Flow Usage
- Prefer Flow over LiveData for reactive streams
- Use StateFlow for state that always has a value
- Use SharedFlow for one-time events
- Apply operators in the right order
- Use
stateIn()to convert cold flows to hot
3. Error Handling
- Use try-catch in coroutines
- Use
catchoperator in flows - Handle CancellationException separately
- Implement retry logic for transient failures
4. Performance
- Use
flowOn()to specify dispatcher - Use
conflate()for dropping intermediate values - Use
debounce()for user input - Limit concurrency with Semaphore
- Use
distinctUntilChanged()to avoid redundant work
5. Testing
- Use
StandardTestDispatcherfor tests - Use
runTestfor testing coroutines - Use
turbinelibrary for testing flows - Mock suspend functions with MockK
---
Common Pitfalls
AVOID: Blocking Main Thread
// Bad
fun loadData() {
runBlocking { // Blocks UI thread!
val data = apiService.getData()
}
}
// Good
fun loadData() {
viewModelScope.launch {
val data = apiService.getData()
updateUI(data)
}
}AVOID: Ignoring Cancellation
// Bad
suspend fun processItems(items: List<Item>) {
for (item in items) {
process(item) // Continues even if cancelled
}
}
// Good
suspend fun processItems(items: List<Item>) {
for (item in items) {
ensureActive() // Check for cancellation
process(item)
}
}AVOID: Flow Collection in ViewModel init
// Bad
class ViewModel : ViewModel() {
init {
repository.dataFlow.collect { // Blocks initialization!
// Process data
}
}
}
// Good
class ViewModel : ViewModel() {
val data = repository.dataFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}---
This reference provides comprehensive Kotlin Coroutines patterns for building safe, performant concurrent Android applications.
Android Testing Patterns Reference (Kotlin + Jetpack Compose)
Comprehensive guide to testing Android applications with JUnit, MockK, Compose UI Testing, and modern testing patterns.
---
Table of Contents
1. Unit Testing ViewModels 2. Testing Coroutines & Flow 3. Compose UI Testing 4. Testing Repository Layer 5. End-to-End Testing
---
Unit Testing ViewModels
Use when: Testing business logic in ViewModels.
Basic ViewModel Test
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private lateinit var repository: UserRepository
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
repository = mockk()
viewModel = UserViewModel(repository)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loadUser success updates UI state`() = runTest {
// Given
val user = User(id = "1", name = "John")
coEvery { repository.getUser("1") } returns Result.success(user)
// When
viewModel.loadUser("1")
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertEquals(user, viewModel.uiState.value.user)
assertFalse(viewModel.uiState.value.isLoading)
assertNull(viewModel.uiState.value.error)
}
@Test
fun `loadUser failure shows error`() = runTest {
// Given
coEvery { repository.getUser(any()) } returns Result.failure(Exception("Network error"))
// When
viewModel.loadUser("1")
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertNull(viewModel.uiState.value.user)
assertFalse(viewModel.uiState.value.isLoading)
assertEquals("Network error", viewModel.uiState.value.error)
}
}Testing with Turbine (Flow Testing)
@Test
fun `searchQuery emits filtered results`() = runTest {
// Given
val allUsers = listOf(User("1", "John"), User("2", "Jane"))
coEvery { repository.getUsers() } returns allUsers
// When/Then
viewModel.searchResults.test {
viewModel.setSearchQuery("John")
assertEquals(listOf(User("1", "John")), awaitItem())
}
}---
Testing Coroutines & Flow
Use when: Testing asynchronous code with coroutines and flows.
Testing Suspend Functions
@Test
fun `fetchData returns correct data`() = runTest {
// Given
val expected = Data("test")
coEvery { apiService.getData() } returns expected
// When
val result = repository.fetchData()
// Then
assertEquals(expected, result)
coVerify { apiService.getData() }
}Testing Flow with Turbine
@Test
fun `user flow emits all users`() = runTest {
// Given
val users = listOf(User("1", "John"), User("2", "Jane"))
coEvery { dao.getAllUsers() } returns flowOf(users)
// When/Then
repository.getUsersFlow().test {
assertEquals(users, awaitItem())
awaitComplete()
}
}
@Test
fun `posts flow handles errors`() = runTest {
// Given
coEvery { apiService.getPosts() } throws IOException()
// When/Then
repository.getPostsFlow().test {
assertTrue(awaitItem().isEmpty()) // Error handling returns empty list
awaitComplete()
}
}Testing StateFlow
@Test
fun `uiState updates correctly`() = runTest {
// Given
val user = User("1", "John")
coEvery { repository.getUser(any()) } returns Result.success(user)
// When
val states = mutableListOf<UserUiState>()
val job = launch {
viewModel.uiState.collect { states.add(it) }
}
viewModel.loadUser("1")
advanceUntilIdle()
// Then
assertTrue(states[0].isLoading) // Initial loading state
assertEquals(user, states[1].user) // Success state
assertFalse(states[1].isLoading)
job.cancel()
}---
Compose UI Testing
Use when: Testing Compose UI components.
Basic Compose Test
class LoginScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun `login button is disabled when fields are empty`() {
composeTestRule.setContent {
LoginScreen(
onNavigateToRegister = {},
onLoginSuccess = {}
)
}
composeTestRule
.onNodeWithText("Sign In")
.assertIsNotEnabled()
}
@Test
fun `clicking login with valid credentials calls viewModel`() {
val viewModel = mockk<LoginViewModel>(relaxed = true)
composeTestRule.setContent {
LoginScreen(
onNavigateToRegister = {},
onLoginSuccess = {},
viewModel = viewModel
)
}
// Enter credentials
composeTestRule
.onNodeWithText("Email")
.performTextInput("test@example.com")
composeTestRule
.onNodeWithText("Password")
.performTextInput("password123")
// Click login
composeTestRule
.onNodeWithText("Sign In")
.performClick()
// Verify
verify { viewModel.login("test@example.com", "password123") }
}
}Testing Lists
@Test
fun `user list displays all users`() {
val users = listOf(
User("1", "John"),
User("2", "Jane"),
User("3", "Bob")
)
composeTestRule.setContent {
UserList(users = users)
}
users.forEach { user ->
composeTestRule
.onNodeWithText(user.name)
.assertIsDisplayed()
}
}
@Test
fun `clicking user item navigates to detail`() {
var clickedUserId: String? = null
composeTestRule.setContent {
UserList(
users = listOf(User("1", "John")),
onUserClick = { clickedUserId = it }
)
}
composeTestRule
.onNodeWithText("John")
.performClick()
assertEquals("1", clickedUserId)
}Testing with Semantics
@Composable
fun LoadingButton(onClick: () -> Unit, isLoading: Boolean) {
Button(
onClick = onClick,
modifier = Modifier.semantics { testTag = "loading_button" }
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.semantics { testTag = "loading_indicator" }
)
} else {
Text("Submit")
}
}
}
@Test
fun `shows loading indicator when loading`() {
composeTestRule.setContent {
LoadingButton(onClick = {}, isLoading = true)
}
composeTestRule
.onNodeWithTag("loading_indicator")
.assertIsDisplayed()
}---
Testing Repository Layer
Use when: Testing data layer with mocked dependencies.
Repository Test with MockK
class UserRepositoryTest {
private lateinit var apiService: ApiService
private lateinit var dao: UserDao
private lateinit var repository: UserRepository
@Before
fun setup() {
apiService = mockk()
dao = mockk()
repository = UserRepository(apiService, dao)
}
@Test
fun `getUser fetches from API when not cached`() = runTest {
// Given
val userDto = UserDto("1", "john@example.com", "John")
coEvery { dao.getUserById("1") } returns null
coEvery { apiService.getUser("1") } returns userDto
coEvery { dao.insert(any()) } just Runs
// When
val result = repository.getUser("1")
// Then
assertTrue(result.isSuccess)
assertEquals("John", result.getOrNull()?.name)
coVerify { apiService.getUser("1") }
coVerify { dao.insert(any()) }
}
@Test
fun `getUser returns cached data when available`() = runTest {
// Given
val cachedUser = UserEntity("1", "john@example.com", "John", 123456)
coEvery { dao.getUserById("1") } returns cachedUser
// When
val result = repository.getUser("1")
// Then
assertTrue(result.isSuccess)
assertEquals("John", result.getOrNull()?.name)
coVerify(exactly = 0) { apiService.getUser(any()) }
}
}---
End-to-End Testing
Use when: Testing complete user flows.
Espresso E2E Test
@RunWith(AndroidJUnit4::class)
class LoginFlowTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun `complete login flow`() {
// Enter email
onView(withId(R.id.emailField))
.perform(typeText("test@example.com"), closeSoftKeyboard())
// Enter password
onView(withId(R.id.passwordField))
.perform(typeText("password123"), closeSoftKeyboard())
// Click login
onView(withId(R.id.loginButton))
.perform(click())
// Verify navigation to home
onView(withId(R.id.homeScreen))
.check(matches(isDisplayed()))
}
}---
Best Practices
1. Unit Tests
- Test business logic, not implementation details
- Use MockK for mocking
- Test edge cases and error handling
- Aim for 80%+ code coverage
2. Coroutine Testing
- Use
runTestfor coroutine tests - Use
StandardTestDispatcherfor controlled execution - Test cancellation scenarios
- Use Turbine for Flow testing
3. Compose Testing
- Use semantic properties for test identifiers
- Test user interactions, not internal state
- Use
composeTestRulefor UI tests - Test accessibility
4. Integration Tests
- Test repository layer with mocked services
- Verify caching behavior
- Test error handling
- Use in-memory databases for tests
5. E2E Tests
- Test critical user flows
- Keep tests stable and deterministic
- Use page objects for maintainability
- Run on CI pipeline
---
This reference provides comprehensive testing patterns for building reliable, well-tested Android applications.
Swift Concurrency Patterns Reference
Comprehensive guide to modern Swift Concurrency patterns including Actors, TaskGroup, AsyncSequence, and advanced async/await techniques for iOS development.
---
Table of Contents
1. Swift Concurrency (Swift 5.5+) 2. Actors for Thread-Safe State 3. Task Groups for Parallel Operations 4. AsyncSequence for Streaming Data 5. Advanced Concurrency Patterns
---
Swift Concurrency (Swift 5.5+)
Use when: Writing async code with async/await, cancellation, actors, and data-race safety; especially when upgrading toolchains with stricter concurrency checking.
Swift Concurrency is based on structured concurrency: tasks inherit priority and cancellation, and actor isolation protects shared mutable state. For UI apps, the default is to keep UI-facing state on the main actor and move I/O / CPU work off the main thread explicitly.
Key Benefits
- Structured cancellation: Cancellation propagates through child tasks
- Actor isolation: Shared mutable state is protected without manual locking
- Static checking: Toolchains can enforce
Sendableand isolation boundaries to prevent data races
Basic Pattern
@MainActor
final class UserListViewModel: ObservableObject {
@Published private(set) var users: [User] = []
@Published private(set) var isLoading = false
private let api: APIService
init(api: APIService) { self.api = api }
func loadUsers() async {
isLoading = true
defer { isLoading = false }
do {
users = try await api.fetchUsers()
} catch {
// Map to UI-safe error and present
}
}
}Running CPU/I/O Work Off the Main Actor
// Use Task.detached only when you explicitly need to break actor inheritance.
// Captures must be Sendable (prefer value types).
func processData(_ input: Input) async -> Output {
await Task.detached(priority: .userInitiated) {
await heavyComputation(input)
}.value
}SwiftUI Integration
struct UserListView: View {
@StateObject private var viewModel = UserListViewModel(api: APIService())
var body: some View {
List(viewModel.users) { user in
Text(user.name)
}
.task {
await viewModel.loadUsers()
}
}
}Best Practices:
- Keep SwiftUI-facing state on
@MainActor - Use actors for caches, dedupers, and shared state
- Prefer structured tasks over detached tasks so cancellation and priority propagate
- Treat concurrency warnings as design feedback; fix via isolation (
@MainActor, actors) andSendablecorrectness
---
Actors for Thread-Safe State
Use when: Managing shared mutable state safely across concurrent tasks.
Actor for Thread-Safe State
actor UserCache {
private var cache: [String: User] = [:]
private var lastUpdated: Date?
func getUser(id: String) async -> User? {
cache[id]
}
func setUser(_ user: User) async {
cache[user.id] = user
lastUpdated = Date()
}
func clear() async {
cache.removeAll()
lastUpdated = nil
}
// Non-isolated for read-only access
nonisolated func cacheSize() -> Int {
// Compiler error - can't access mutable state
// Use Task to await if needed
return 0
}
}
// Usage
let cache = UserCache()
Task {
await cache.setUser(user)
let cached = await cache.getUser(id: "123")
}Global Actor (@MainActor)
@MainActor
class UIStateManager: ObservableObject {
@Published var isLoading = false
@Published var errorMessage: String?
func showError(_ message: String) {
// Always runs on main thread
errorMessage = message
}
}
// Usage in ViewModel
@MainActor
class ProfileViewModel: ObservableObject {
@Published var user: User?
func loadUser() async {
// Already on MainActor, safe to update @Published
user = try? await APIService.shared.getUser()
}
}Custom Global Actor
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
@DatabaseActor
class DatabaseManager {
private var connection: DatabaseConnection?
func query(_ sql: String) async throws -> [Row] {
// All calls serialized through DatabaseActor
try await connection?.execute(sql)
}
}Checklist:
- [ ] Use actors for shared mutable state
- [ ] @MainActor for UI-related code
- [ ] Avoid blocking actor's executor
- [ ] Use nonisolated for pure functions
- [ ] Custom actors for domain-specific isolation
---
Task Groups for Parallel Operations
Use when: Running multiple async operations concurrently and collecting results.
TaskGroup for Parallel Fetching
func fetchMultipleUsers(ids: [String]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
// Add tasks to group
for id in ids {
group.addTask {
try await APIService.shared.getUser(id: id)
}
}
// Collect results as they complete
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}TaskGroup with Error Handling
func fetchUsersWithFallback(ids: [String]) async -> [Result<User, Error>] {
await withTaskGroup(of: Result<User, Error>.self) { group in
for id in ids {
group.addTask {
do {
let user = try await APIService.shared.getUser(id: id)
return .success(user)
} catch {
return .failure(error)
}
}
}
var results: [Result<User, Error>] = []
for await result in group {
results.append(result)
}
return results
}
}Limiting Concurrency
func batchFetchUsers(ids: [String], maxConcurrent: Int = 5) async throws -> [User] {
var users: [User] = []
for batch in ids.chunked(into: maxConcurrent) {
let batchUsers = try await withThrowingTaskGroup(of: User.self) { group in
for id in batch {
group.addTask {
try await APIService.shared.getUser(id: id)
}
}
var results: [User] = []
for try await user in group {
results.append(user)
}
return results
}
users.append(contentsOf: batchUsers)
}
return users
}
extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}Checklist:
- [ ] Use TaskGroup for parallel async operations
- [ ] Handle errors per task or globally
- [ ] Limit concurrency for resource control
- [ ] Cancel group when needed
- [ ] Collect results efficiently
---
AsyncSequence for Streaming Data
Use when: Processing streams of asynchronous data.
Custom AsyncSequence
struct NotificationStream: AsyncSequence {
typealias Element = Notification
let notificationName: Notification.Name
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(notificationName: notificationName)
}
struct AsyncIterator: AsyncIteratorProtocol {
let notificationName: Notification.Name
private var continuation: AsyncStream<Notification>.Continuation?
private let stream: AsyncStream<Notification>
private var iterator: AsyncStream<Notification>.Iterator
init(notificationName: Notification.Name) {
self.notificationName = notificationName
self.stream = AsyncStream { continuation in
self.continuation = continuation
let observer = NotificationCenter.default.addObserver(
forName: notificationName,
object: nil,
queue: nil
) { notification in
continuation.yield(notification)
}
continuation.onTermination = { _ in
NotificationCenter.default.removeObserver(observer)
}
}
self.iterator = stream.makeAsyncIterator()
}
mutating func next() async -> Notification? {
await iterator.next()
}
}
}
// Usage
for await notification in NotificationStream(notificationName: .userDidLogin) {
print("User logged in: \(notification)")
}AsyncSequence Operators
extension AsyncSequence {
func compactMap<T>(_ transform: @escaping (Element) async throws -> T?) rethrows -> AsyncCompactMapSequence<Self, T> {
AsyncCompactMapSequence(self, transform: transform)
}
func filter(_ predicate: @escaping (Element) async throws -> Bool) rethrows -> AsyncFilterSequence<Self> {
AsyncFilterSequence(self, predicate: predicate)
}
}
// Usage
let eventStream = NotificationStream(notificationName: .dataUpdated)
.compactMap { notification -> User? in
notification.userInfo?["user"] as? User
}
.filter { user in
user.isActive
}
for await user in eventStream {
print("Active user updated: \(user.name)")
}URL Session AsyncBytes
func downloadLargeFile(url: URL) async throws {
let (asyncBytes, response) = try await URLSession.shared.bytes(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
var data = Data()
for try await byte in asyncBytes {
data.append(byte)
// Progress tracking
let progress = Double(data.count) / Double(httpResponse.expectedContentLength)
print("Download progress: \(progress * 100)%")
}
}Checklist:
- [ ] Use AsyncSequence for streaming data
- [ ] Implement custom iterators when needed
- [ ] Use operators for transformation
- [ ] Handle cancellation properly
- [ ] Monitor memory for long-running streams
---
Advanced Concurrency Patterns
Task Cancellation
class DownloadManager {
private var downloadTask: Task<Data, Error>?
func startDownload(from url: URL) {
downloadTask = Task {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
func cancelDownload() {
downloadTask?.cancel()
downloadTask = nil
}
func checkCancellation() async throws -> Data {
try Task.checkCancellation()
// Or use:
if Task.isCancelled {
throw CancellationError()
}
let data = try await heavyOperation()
return data
}
}Task Priority
func processWithPriority() {
// High priority task
Task(priority: .high) {
await criticalOperation()
}
// Background task
Task(priority: .background) {
await cleanupOperation()
}
// User-initiated task (default)
Task(priority: .userInitiated) {
await fetchData()
}
}Detached Tasks
func performHeavyComputation() {
// Detached task - doesn't inherit context
Task.detached(priority: .background) {
let result = await expensiveCalculation()
print(result)
}
}AsyncStream
func makeLocationStream() -> AsyncStream<CLLocation> {
AsyncStream { continuation in
let manager = CLLocationManager()
let delegate = LocationDelegate(continuation: continuation)
manager.delegate = delegate
manager.startUpdatingLocation()
continuation.onTermination = { _ in
manager.stopUpdatingLocation()
}
}
}
// Usage
for await location in makeLocationStream() {
print("New location: \(location.coordinate)")
}Actor Reentrancy
actor Counter {
private var value = 0
func increment() async {
// Suspension point - actor can be re-entered
await Task.yield()
// Value might have changed!
value += 1
}
func safeIncrement() {
// No suspension - atomic
value += 1
}
}Sendable Types
// Value types are implicitly Sendable
struct User: Sendable {
let id: String
let name: String
}
// Classes must explicitly conform
final class UserCache: Sendable {
let cache: [String: User] // Must be immutable
}
// Mark closure as Sendable
func performAsync(_ operation: @Sendable () async -> Void) async {
await operation()
}---
Best Practices
1. Use @MainActor for UI Code
- Mark ViewModels and UI-related classes
- Ensures all UI updates happen on main thread
- Prevents threading issues
2. Prefer Actors Over Locks
- Actors provide compile-time safety
- No deadlocks or race conditions
- Better performance in most cases
3. Handle Cancellation
- Check
Task.isCancelledin long operations - Use
Task.checkCancellation() - Clean up resources on cancellation
4. Limit Concurrency
- Don't create unlimited concurrent tasks
- Use TaskGroup with batching
- Consider system resources
5. Avoid Blocking Operations
- Never block an actor's executor
- Use
Task.detachedfor CPU-intensive work - Keep actor methods fast
6. Test Concurrent Code
- Test cancellation paths
- Test race conditions
- Use async test helpers
---
Common Pitfalls
AVOID: Capturing Self Strongly in Tasks
// Bad
class ViewModel {
func loadData() {
Task {
self.data = await fetchData() // Retains self
}
}
}
// Good
class ViewModel {
func loadData() {
Task { [weak self] in
guard let self = self else { return }
self.data = await fetchData()
}
}
}AVOID: Ignoring Cancellation
// Bad
func processItems(_ items: [Item]) async {
for item in items {
await process(item) // Continues even if cancelled
}
}
// Good
func processItems(_ items: [Item]) async throws {
for item in items {
try Task.checkCancellation()
await process(item)
}
}AVOID: Mixing Actors with Locks
// Bad
actor Cache {
private let lock = NSLock()
private var data: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return data[key]
}
}
// Good - actors don't need locks
actor Cache {
private var data: [String: Data] = [:]
func get(_ key: String) -> Data? {
data[key]
}
}---
This reference provides comprehensive Swift Concurrency patterns for building safe, performant concurrent iOS applications.