How to Handle App Lifecycle with Flutter (On Android and iOS)

How to handle app lifecycle with Flutter (on Android and iOS)?

There is a method called when the system put the app in the background or return the app to foreground named didChangeAppLifecycleState.

Example with widgets:

  class _AppLifecycleReactorState extends State<AppLifecycleReactor> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

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

AppLifecycleState _notification;

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() { _notification = state; });
}

@override
Widget build(BuildContext context) {
return new Text('Last notification: $_notification');
}
}

Also there are CONSTANTS to know the states that an application can be in, eg:

  1. inactive
  2. paused
  3. resumed
  4. suspending

The usage of these constants would be the value of the constant e.g:

const AppLifecycleState(state)

Flutter exact lifecycle equivalents to onResume / onPause on Android and viewWillAppear / viewDidDisappear on iOS

I've found a few solutions, each with its own pros and cons. The one that answers this question the best is FocusDetector.

Best Pick

FocusDetector (Edson Bueno)

FocusDetector handles all the cases covered in the original question. Instead of overrides (like initState() and dispose()), you supply callback functions to a wrapping widget called FocusDetector. The two relevant callbacks are:

  • onFocusGained = onResume

  • onFocusLost = onPause

  • Cons

    • Isn’t maintained by Flutter or Google (but it uses VisibilityDetector and WigetBindingObserver under the hood)
    • The default VisibilityDetectorController.updateInterval is 500ms which means the events get fired somewhat late.

Borrowing the style from one of my favorite posts:

iOS, Android, and Flutter Life Cycle

Example Widget

class PageState extends State<Page> {

@override
void initState() {
super.initState();
log("onCreate / viewDidLoad / initState");
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
layoutComplete();
});
}

// Bonus one I've found helpful, once layout is finished
void layoutComplete() {
log("onActivityCreated / viewDidLoad / layoutComplete");
}

void viewWillAppear() {
log("onResume / viewWillAppear / onFocusGained");
}

void viewWillDisappear() {
log("onPause / viewWillDisappear / onFocusLost");
}

@override
void dispose() {
log("onDestroy / viewDidUnload / dispose");
super.dispose();
}

@override
Widget build(BuildContext context) {
return FocusDetector(
onFocusGained: viewWillAppear,
onFocusLost: viewWillDisappear,
child: Text('Rest of my widget'),
);
}
}


Other options

RouteObserver (Flutter)

  • didPush = onResume current screen is pushed on

  • didPopNext = onResume current screen is being navigated back to

  • didPop = onPause dismissing current page / going back

  • didPushNext = onPause navigating forward to a new page

  • Cons:

    • Doesn’t cover use-case (2), backgrounding then foregrounding the app

WidgetsBindingObserver (Flutter)

  • AppLifecycleState.resumed = The app is visible and responding to user input

  • AppLifecycleState.paused = The app is not visible and not responding to user input

  • Cons:

    • Isn’t widget/route specific (for external nav (2))
    • Doesn’t cover use-case (1), navigating between pages

VisibilityDetector (Google)

  • onVisibilityChanged (visibility == 1) = onResume

  • onVisibilityChanged (visibility == 0) = onPause

  • Cons:

    • Doesn’t cover use-case (2), backgrounding then foregrounding the app

Flutter: How to track app lifecycle without a widget

Below is a code sample that can listen to AppLifecycleState change events without directly involving a widget:

import 'package:flutter/material.dart';

class MyLibrary with WidgetsBindingObserver {
AppLifecycleState _state;

AppLifecycleState get state => _state;

MyLibrary() {
WidgetsBinding.instance.addObserver(this);
}

/// make sure the clients of this library invoke the dispose method
/// so that the observer can be unregistered
void dispose() {
WidgetsBinding.instance.removeObserver(this);
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
this._state = state;
}

void someFunctionality() {
// your library feature
}
}

Now you can instantiate the library in a flutter widget, for instance, from which point it will start listening to any change in AppLifecycleState.

Please note that, the above code doesn't take care of redundant bindings. For example, if the client of your library is meant to initialize the library more than once in different places, the didChangeAppLifecycleState() method will be triggered multiple times per state change (depending on the number of instances of the library that were created). Also I'm unsure whether the solution proposed conforms to flutter best practices. Nevertheless it solves the problem and hope it helps!

Flutter - Handling Life Cycle

dispose() is used to execute code when the screen is disposed. Equal to onDestroy() of Android.

Example:

@override
void dispose() {
anyController?.dispose();
timer.cancel();
super.dispose();
}

Add this in your Code

Life cycle in flutter

  1. createState():
    When the Framework is instructed to build a StatefulWidget, it immediately calls createState()

  2. mounted is true:
    When createState creates your state class, a buildContext is assigned to that state. buildContext is, overly simplified, the place in the widget tree in which this widget is placed. Here's a longer explanation.
    All widgets have a bool this.mounted property. It is turned true when the buildContext is assigned. It is an error to call setState when a widget is unmounted.

  3. initState():
    This is the first method called when the widget is created (after the class constructor, of course.) initState is called once and only once. It must call super.initState().

  4. didChangeDependencies():
    This method is called immediately after initState on the first time the widget is built.

  5. build():
    This method is called often. It is required, and it must return a Widget.

  6. didUpdateWidget(Widget oldWidget):
    If the parent widget changes and has to rebuild this widget (because it needs to give it different data), but it's being rebuilt with the same runtimeType, then this method is called.
    This is because Flutter is re-using the state, which is long lived. In this case, you may want to initialize some data again, as you would in initState.

  7. setState():
    This method is called often from the framework itself and from the developer. Its used to notify the framework that data has changed

  8. deactivate():
    Deactivate is called when State is removed from the tree, but it might be reinserted before the current frame change is finished. This method exists basically because State objects can be moved from one point in a tree to another.

  9. dispose():
    dispose() is called when the State object is removed, which is permanent. This method is where you should unsubscribe and cancel all animations, streams, etc.

  10. mounted is false:
    The state object can never remount, and error will be thrown if setState is called.

onResume() and onPause() for widgets on Flutter

The most common case where you'd want to do this is if you have an animation running and you don't want to consume resources in the background. In that case, you should extend your State with TickerProviderStateMixin and use your State as the vsync argument for the AnimationController. Flutter will take care of only calling the animation controller's listeners when your State is visible.

If you want the States that live in your PageRoute to be disposed when the PageRoute is obscured by other content, you can pass a maintainState argument of false to your PageRoute constructor. If you do this, your State will reset itself (and its children) when it's hidden and will have to re-construct itself in initState using the properties passed in as constructor arguments to its widget. You can use a model or controller class, or PageStorage, to hold the user's progress information if you don't want a complete reset.

Here is a sample app that demonstrates these concepts.

screen 1
screen 2
screen 3

import 'package:flutter/material.dart';

void main() {
runApp(new MaterialApp(
onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/') {
return new MaterialPageRoute<Null>(
settings: settings,
builder: (_) => new MyApp(),
maintainState: false,
);
}
return null;
}
));
}

class MyApp extends StatefulWidget {
MyAppState createState() => new MyAppState();
}

class MyAppState extends State<MyApp> with TickerProviderStateMixin {
AnimationController _controller;

@override
void initState() {
print("initState was called");
_controller = new AnimationController(vsync: this)
..repeat(min: 0.0, max: 1.0, period: const Duration(seconds: 1))
..addListener(() {
print('animation value ${_controller.value}');
});
super.initState();
}

@override
void dispose() {
print("dispose was called");
_controller.dispose();
super.dispose();
}

int _counter = 0;

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('home screen')
),
body: new Center(
child: new RaisedButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: new Text('Button pressed $_counter times'),
),
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.remove_red_eye),
onPressed: () {
Navigator.push(context, new MaterialPageRoute(
builder: (BuildContext context) {
return new MySecondPage(counter: _counter);
},
));
},
),
);
}
}

class MySecondPage extends StatelessWidget {
MySecondPage({ this.counter });

final int counter;

Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Certificate of achievement'),
),
body: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
new Icon(Icons.developer_mode, size: 200.0),
new Text(
'Congrats, you clicked $counter times.',
style: Theme.of(context).textTheme.title,
textAlign: TextAlign.center,
),
new Text(
'All your progress has now been lost.',
style: Theme.of(context).textTheme.subhead,
textAlign: TextAlign.center,
),
],
),
);
}
}


Related Topics



Leave a reply



Submit