| name | app-lifecycle |
| description | Mobile app lifecycle across iOS and Android (foreground, background, suspended, killed), state restoration, process death, and cross-platform parallels in Flutter and React Native. Use when handling background work, persistence of in-flight state, or resume flows. |
App Lifecycle
Instructions
Mobile apps run in a hostile runtime. The OS freezes, backgrounds, and kills processes to preserve battery and memory. Treating the lifecycle as an afterthought leads to lost data, broken deep links, and angry reviews.
1. The Lifecycle States
Both platforms converge to roughly:
| State | iOS term | Android term | Cross-platform term |
|---|
| Visible & interactive | Active | Resumed | Foreground |
| Visible but not active | Inactive (incoming call, control center) | Paused | Inactive |
| Not visible | Background | Stopped | Background |
| Frozen but in memory | Suspended | Stopped | Suspended |
| Gone | Terminated | Process death | Killed |
2. Transitions and Contracts
- Entering background: cancel network calls not needed offline, persist drafts, release camera/audio/sensors, pause timers.
- Entering foreground: refresh data that may be stale, reconnect websockets, re-check auth, reclaim resources.
- Process death: on cold start, detect whether this is a restart from death (saved-state bundle present) or a first launch; restore the previous screen and selection.
- Low memory warning: drop caches, close non-visible images, release heavy objects.
3. Platform Hooks
@main struct App: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup { RootView() }
.onChange(of: scenePhase) { _, phase in
switch phase {
case .active: AppEvents.foreground()
case .inactive: AppEvents.inactive()
case .background: AppEvents.background()
@unknown default: break
}
}
}
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) = AppEvents.foreground()
override fun onStop(owner: LifecycleOwner) = AppEvents.background()
}
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
// Flutter
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); }
@override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); }
@override void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed: AppEvents.foreground();
case AppLifecycleState.inactive: AppEvents.inactive();
case AppLifecycleState.paused: AppEvents.background();
case AppLifecycleState.detached: AppEvents.detached();
case AppLifecycleState.hidden: break;
}
}
}
import { AppState } from 'react-native';
useEffect(() => {
const sub = AppState.addEventListener('change', (s) => {
if (s === 'active') AppEvents.foreground();
else if (s === 'background') AppEvents.background();
});
return () => sub.remove();
}, []);
4. State Restoration
State restoration keeps the user's mental context after the OS kills the process.
- iOS:
@SceneStorage, StateRestoration with NSUserActivity for deep links.
- Android:
SavedStateHandle in ViewModels; onSaveInstanceState for Views; Navigation Component restores the back stack.
- Flutter:
RestorationMixin, RestorableProperty, restorationScopeId in MaterialApp.
- React Native:
@react-navigation persistence with AsyncStorage.
Keep restored state small (IDs, not payloads). Refetch the data using the ID on resume.
5. Background Work Budgets
The OS is strict. Do not run background network or CPU work without a matching API:
- iOS:
BGTaskScheduler (background refresh, processing tasks), URLSession background configuration for downloads, VoIP push for real-time.
- Android:
WorkManager for deferrable guaranteed work, ForegroundService for user-visible ongoing work (with the right foreground-service type declared), JobScheduler under the hood.
- Flutter:
workmanager plugin, native channel on iOS.
- React Native:
expo-background-fetch, react-native-background-fetch, Headless JS on Android.
Assume background execution may never happen. Design for eventual consistency and sync on foreground.
6. Process Death and Cold Start
- Restore navigation first, then restore feature state.
- Re-run auth checks and token refresh on every cold start.
- If a push notification launched the app, route to the target screen only after auth is verified.
- Measure cold start time; store and foreground launches should be under 2 seconds to first meaningful paint.
7. Common Bugs
- Losing form drafts when the user switches apps. Fix: persist on
onPause / background.
- Continuing location updates when backgrounded without user awareness. Fix: stop on background unless there is a declared background mode and a UI affordance.
- Showing stale data after a long background. Fix: soft-invalidate caches on foreground and refetch.
- Keeping websockets open while backgrounded. Fix: close on background, reconnect with backoff on foreground, and rely on push for real-time.
- Deep link opens on cold start before the nav graph is ready. Fix: buffer intents until the navigator is mounted.
8. Observability
Log these events (without PII):
app.foreground, app.background, app.cold_start, app.warm_start.
restore.success / restore.failure with a bucketed reason.
- Cold-start duration and first-frame-time as metrics, not logs.
9. Anti-Patterns
- Running long work in
onPause or applicationWillResignActive. You have milliseconds.
- Storing large objects in saved-state bundles. Store IDs, rehydrate.
- Polling while backgrounded. Use push or periodic
WorkManager/BGTask.
- Assuming the app was foreground since last launch. Always check.
Checklist