Application Life Cycles In Flutter

Application Life Cycles In Flutter

When making use of an app one tends to navigate / switch between pages, minimize(put the app in the background), return back to the app and even close the app entirely (i.e. stop it from running in the background). These activities transitions the app from one state to another. These states are considered the lifecycle of an app.

In flutter we can monitor these different states in other to perform certain functionality based on the current state of the app, using the WidgetsBindingObserver class. The class provides a number of methods one of which allows us to know that a state has changed. These methods include:

  • didPopRoute: Called when the system tells the app to pop the current route. For example, on Android, this is called when the user presses the back button.

  • didPushRoute: Called when the host tells the application to push a new route onto the navigator.

  • didChangeLocales: Called when the system tells the app that the user's locale has changed. For example, if the user changes the system language settings.

  • didChangeAppLifecycleState: Called when the system puts the app in the background or returns the app to the foreground.

In this article we are going to make use of the default flutter counter application. Where we get to store the counters progress and resume from the last count when the app is opened again. We'll do this by monitoring the state of the app, using the didChangeAppLifecycleState method from the WidgetsBindingObserver class.

This method provides us with the AppLifecycleState which is an enum with the following states.

  • AppLifecycleState.detached: When the application is in this state, the engine is running without a view. It can either be in the progress of attaching a view when engine was first initializes, or after the view being destroyed due to a Navigator pop.

  • AppLifecycleState.inactive: Apps transition to this state when another activity is focused, such as a split-screen app, a phone call, a picture-in-picture app, a system dialog, or another window.

  • AppLifecycleState.paused: The application is not currently visible to the user, not responding to user input, and running in the background.

  • AppLifecycleState.resumed: The application is visible and responding to user input.

The Setup

First we'll create a new flutter app called example by running

flutter create example

Next add SharedPreference as a dependency which would be used as local storage to store the progress

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.5

Then run pub get to fetch the dependency

flutter pub get

This is what main.dart looks like with the default counter app

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Life cycle example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Making Use Of The WidgetsBindingObserver Class

The WidgetsBindingObserver class would be used as a mixin in this example. A mixin allows us to add the capabilities / functionalities of another class (WidgetsBindingObserver) or classes to your own class (MyHomePage), without inheriting from those classes as dart doesn't support multiple inheritance.

We do this by using the with keyword

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  //...
}

The WidgetsBindingObserver is used with a StatefulWidget so we get access to the init and dipose method where we would initialize and dipose the observer.

@override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

The WidgetsBindingObserver gives us access to the didChangeAppLifecycleState amongst others

@override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // TODO: implement didChangeAppLifecycleState
    super.didChangeAppLifecycleState(state);
  }

Responding to state change

What the app would do is store the current value of the counter before the app is killed and resume with that value when the app is opened.

To do this we use shared preference to store the value of the counter when the state is paused. This is because before an app is killed it first transitions to the paused state so that's what we're interested in

@override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    final preference = await SharedPreferences.getInstance();
    if (state == AppLifecycleState.paused) {
      preference.setInt('counter', _counter);
    }
  }

And to get this value when the app starts we simply reassign it in the init method.

int _counter = 0;

  @override
  void initState() {
    SharedPreferences.getInstance().then((pref) {
      setState(() {
        _counter = pref.getInt('counter') ?? 0;
      });
    });
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

A default / fallback value is provided because the first time the app is run, shared preference wont have a counter value stored and would return null

_counter = pref.getInt('counter') ?? 0;

And that's it. Now if the counters value is 5 when the app is closed, on opening the app it would start from 5.

Use Case

With the didChangeAppLifecycleState method, you can declare how your app behaves when the user leaves and re-enters the app. For example, if you're building a streaming video player, you might pause the video and terminate the network connection when the user switches to another app. When the user returns, you can reconnect to the network and allow the user to resume the video from the same spot. In other words, each state from the method allows you to perform specific work that's appropriate to that state. Doing the right work at the right time and handling transitions properly make your app more robust and performant. For example, good implementation of the lifecycle callbacks can help ensure that your app avoids:

  • Crashing if the user receives a phone call or switches to another app while using your app.
  • Consuming valuable system resources when the user is not actively using it.
  • Losing the user's progress if they leave your app and return to it at a later time.

Thanks for reading

For more information on WidgetsBindingObserver, visit the flutter docs.

You can also find more info on the AppLifecycleState in the flutter docs.

Check out the full code here