Flutter and Android Lifecycle Explained: initState, didChangeAppLifecycleState and More

If you've come to Flutter from native Android, one of the first things you'll notice is that the lifecycle methods you relied on — onCreate, onResume, onPause, onStop, onDestroy — don't exist in the same form. Flutter manages lifecycle differently, and understanding the mapping between the two systems is essential for building apps that handle background/foreground transitions, resource cleanup, and state restoration correctly.
Android's Activity Lifecycle
Android apps are built around the Activity class, and the OS communicates state changes through a set of well-defined callbacks:
| Method | When it fires |
onCreate() | Activity is first created |
onStart() | Activity becomes visible |
onResume() | User can interact with the activity |
onPause() | Activity is partially obscured or losing focus |
onStop() | Activity is no longer visible |
onDestroy() | Activity is being destroyed |
These fire in sequence as the user navigates into and out of the activity. The most commonly used pair is onResume and onPause — when the app comes to the foreground and when it's about to leave it.
Flutter's Widget Lifecycle
Flutter replaces the activity-centric model with a widget-centric one. A StatefulWidget goes through its own lifecycle:
initState() — called once when the widget is inserted into the tree. This is the equivalent of onCreate. Initialize controllers, set up listeners, and fetch initial data here.
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_loadData();
}
didChangeDependencies() — called immediately after initState and again whenever the widget's inherited dependencies change. Use this when setup depends on InheritedWidget values like Theme or MediaQuery.
build() — called whenever Flutter needs to render the widget. This can be called many times, so keep it free of side effects.
didUpdateWidget() — called when the parent rebuilds and passes new configuration to this widget. Useful for reacting to external prop changes.
dispose() — called when the widget is permanently removed from the tree. This is where you release resources — cancel timers, close streams, dispose controllers. The equivalent of onDestroy.
@override
void dispose() {
_controller.dispose();
_subscription.cancel();
super.dispose();
}
Observing App-Level Lifecycle in Flutter
Widget lifecycle handles what happens to a specific screen. But for app-level events — the user pressing home, switching apps, receiving a phone call — Flutter uses WidgetsBindingObserver.
Add the mixin to your StatefulWidget's state class, register it in initState, and override didChangeAppLifecycleState:
class _HomeState extends State<HomeScreen> 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:
// App is in foreground and interactive
break;
case AppLifecycleState.inactive:
// App is losing focus (e.g., phone call, app switcher)
break;
case AppLifecycleState.paused:
// App is in background and not visible
break;
case AppLifecycleState.detached:
// Engine is running but no views attached
break;
case AppLifecycleState.hidden:
// All views are hidden (added in newer Flutter versions)
break;
}
}
}
Always call removeObserver in dispose. Failing to do so causes memory leaks — the observer will continue receiving callbacks even after the widget is gone.
Mapping Android to Flutter
| Android | Flutter equivalent | When |
onCreate | initState | Widget first created |
onStart / onResume | AppLifecycleState.resumed | App comes to foreground |
onPause | AppLifecycleState.inactive | App losing focus |
onStop | AppLifecycleState.paused | App goes to background |
onDestroy | dispose | Widget permanently removed |
One important nuance: Flutter's state names don't map one-to-one to Android's names. When Android calls Activity.onPause, Flutter enters inactive — not paused. When Android calls Activity.onStop, Flutter enters paused. This mismatch trips up developers coming from native Android, so it's worth keeping this table close.
Practical Use Cases
Pause video playback when the app goes to the background:
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_videoController.pause();
} else if (state == AppLifecycleState.resumed) {
_videoController.play();
}
}
Release the camera when the app is not visible:
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive) {
_cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
_initCamera();
}
}
Refresh stale data when returning to the foreground:
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_refreshUserSession();
}
}
Using the Newer AppLifecycleListener
Flutter also provides AppLifecycleListener, a newer class that offers named callbacks for each transition rather than a single switch statement. For new code, this tends to be cleaner:
late final AppLifecycleListener _listener;
@override
void initState() {
super.initState();
_listener = AppLifecycleListener(
onResume: () => _onResumed(),
onPause: () => _onPaused(),
onInactive: () => _onInactive(),
onDetach: () => _onDetached(),
);
}
@override
void dispose() {
_listener.dispose();
super.dispose();
}
The behavior is the same as WidgetsBindingObserver, but the API is more explicit and easier to read at a glance.
Common Mistakes
Forgetting to remove the observer. If you add an observer in initState but don't remove it in dispose, the widget keeps receiving callbacks after it's been removed from the tree. This can cause exceptions or unexpected behavior.
Doing heavy work in build. Unlike onCreate, build can be called many times. Database queries, network calls, and expensive computations don't belong there — they belong in initState or triggered by state changes.
Treating inactive like paused. inactive fires for brief interruptions — phone calls, the notification shade, the app switcher — where the app is still partially visible. paused is when the app has genuinely moved to the background. The right action (pause media vs. release camera) depends on which state you're actually in.
Summary
Flutter's lifecycle is widget-first rather than activity-first, but all the same events exist — they're just expressed differently. initState and dispose bookend a widget's life. WidgetsBindingObserver and AppLifecycleListener handle app-level foreground/background transitions. Keep the Android-to-Flutter mapping in mind, always clean up in dispose, and use the named callbacks in AppLifecycleListener for clarity in new code.




