Flutter and Android Lifecycle Explained: initState, didChangeAppLifecycleState and More

Search for a command to run...

No comments yet. Be the first to comment.
The build-time binary trick that lets you ship a working feature while keeping the real logic unreadable.

TL;DR — This guide covers everything you need to rebrand a Flutter app for a new client: changing the app name, swapping the icon with flutter_launcher_icons, renaming the package ID with change_app_p

A Quick Story to Set the Scene Meet Lumi and Nyx. They share one Flutter app. Lumi prefers light mode — bright cards, cheerful blues, a UI that feels open. Nyx wants the opposite — calm grays, a dark background, something easier on the eyes at night....

There's a category of bug that only shows up in production apps — the kind that doesn't crash, doesn't throw an exception, and doesn't even look wrong on the surface. It just quietly does the wrong thing. One of the most common in security-sensitive ...

When Classes Start Misbehaving At some point in every Dart project, you run into a situation where your classes are being used in ways you never intended. Someone creates an object directly when there should only ever be one. Someone calls an interna...

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 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 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();
}
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.
| 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.
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();
}
}
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.
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.
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.