Skip to main content

Command Palette

Search for a command to run...

Flutter and Android Lifecycle Explained: initState, didChangeAppLifecycleState and More

Updated
5 min read
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:

MethodWhen 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

AndroidFlutter equivalentWhen
onCreateinitStateWidget first created
onStart / onResumeAppLifecycleState.resumedApp comes to foreground
onPauseAppLifecycleState.inactiveApp losing focus
onStopAppLifecycleState.pausedApp goes to background
onDestroydisposeWidget 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.

More from this blog

A

Anmol's blog

17 posts