S.D.
S.D.

Reputation: 5857

Dispose widget when navigating to new route

I have two screens in my app.

Screen A runs a computationally expensive operation while opened, and properly disposes by cancelling animations/subscriptions to the database when dispose() is called to prevent memory leaks.

From Screen A, you can open another screen (Screen B).

When I use Navigator.pushNamed, Screen A remains in memory, and dispose() is not called, even though Screen B is now shown.

Is there a way to force disposal of Screen A when it is not in view?

Example code where first route is never disposed:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatefulWidget {
  @override
  _FirstRouteState createState() => _FirstRouteState();
}

class _FirstRouteState extends State<FirstRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: RaisedButton(
        child: Text('Open route'),
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => SecondRoute()),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    // Never called
    print("Disposing first route");
    super.dispose();
  }
}

class SecondRoute extends StatefulWidget {
  @override
  _SecondRouteState createState() => _SecondRouteState();
}

class _SecondRouteState extends State<SecondRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: RaisedButton(
        onPressed: () {
          Navigator.pop(context);
        },
        child: Text('Go back!'),
      ),
    );
  }

  @override
  void dispose() {
    print("Disposing second route");
    super.dispose();
  }
}

Upvotes: 43

Views: 32272

Answers (7)

Benyamin Beyzaie
Benyamin Beyzaie

Reputation: 573

In new versions of flutter deactivate won't be called when you push a new widget on top of another widget. Also there is an open issue related to this topic on flutter's github: https://github.com/flutter/flutter/issues/50147

The best way to handle this issue is to add RouteObserver<PageRoute> to your material app and override didPushNext and didPopNext functions.

There is a very helpful medium article related to this topic which you can find here: https://medium.com/koahealth/how-to-track-screen-transitions-in-flutter-with-routeobserver-733984a90dea

As Article said create your own RouteAwareWidget, you can add these two call backs to the fields of the widget: didPopNext didPushNext

class RouteAwareWidget extends StatefulWidget {
  final Widget child;
  final VoidCallback? didPopNext;
  final VoidCallback? didPushNext;

  const RouteAwareWidget({
    Key? key,
    required this.child,
    this.didPopNext,
    this.didPushNext,
  }) : super(key: key);

  @override
  State<RouteAwareWidget> createState() => RouteAwareWidgetState();
}

class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
  }

  @override
  void dispose() {
    routeObserver.unsubscribe(this);
    super.dispose();
  }

  @override
  void didPush() {}

  @override
  void didPopNext() {
    dPrint('didPopNext');
    widget.didPopNext == null ? null : widget.didPopNext!();
    super.didPopNext();
  }

  @override
  void didPushNext() {
    dPrint('didPushNext');
    widget.didPushNext == null ? null : widget.didPushNext!();
    super.didPushNext();
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

Create a global RouteObserver<PageRoute> and add it to your material app:

final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();

MaterialApp(
     navigatorObservers: [routeObserver],
     debugShowCheckedModeBanner: false,
     routes: _routes,
)

then in your routes you should wrap your routes with RouteAwareWidget and add custom functions that you want:

  final _routes = {
    HomePage.routeName: (context) => RouteAwareWidget(
          child: const HomePage(),
          didPushNext: () => sl<CameraBloc>().add(Dispose()),
          didPopNext: () => sl<CameraBloc>().add(Init()),
        ),
    MyQuestions.routeName: (context) => const RouteAwareWidget(
          child: MyQuestions(),
        ),
};

didPushNext will be called when you push a widget on top of HomePage and didPopNext will be called when you pop the last widget above HomePage.

Upvotes: 8

Mert Eroglu
Mert Eroglu

Reputation: 1

You can use pushReplacementNamed method. For reference : https://api.flutter.dev/flutter/widgets/Navigator/pushReplacementNamed.html

Upvotes: 0

G-Starr
G-Starr

Reputation: 85

A light weight solution for a single route case is using a callback function triggered from the SecondRoute.

Trigger the callback from the WidgetsBinding.instance.addPostFrameCallback() within the initState() on the SecondRoute

More information on WidgetsBinding and when they run can be found here: Flutter: SchedulerBinding vs WidgetsBinding.

WidgetsBinding & SchedulerBinding will be printed only once as we called it initState(), but it will be called when build method finished it’s rendering.

import 'package:flutter/material.dart';

class FirstRoute extends StatefulWidget {
  const FirstRoute({super.key});

  @override
  State<FirstRoute> createState() => _FirstRouteState();
}

class _FirstRouteState extends State<FirstRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Screen A')),
      body: Center(
        child: TextButton(
          child: const Text('Go to Screen B'),
          onPressed: () async {
            await Navigator.of(context).push(
              MaterialPageRoute(
                builder: (BuildContext context) => SecondRoute(_callbackFn),
              ),
            );
            _secondRouteDone();
          },
        ),
      ),
    );
  }

  _callbackFn() {
    print("Widget B Loaded, Free up memory, dispose things, etc.");
  }

  _secondRouteDone() {
    print("SecondRoute Popped, Reinstate controllers, etc.");
  }
}

class SecondRoute extends StatefulWidget {
  final Function() notifyIsMountedFn;

  const SecondRoute(this.notifyIsMountedFn, {super.key});

  @override
  State<SecondRoute> createState() => _SecondRouteState();
}

class _SecondRouteState extends State<SecondRoute> {
  @override
  void initState() {
    super.initState();
    // Notify FirstRoute after paint
    WidgetsBinding.instance.addPostFrameCallback((_) {
      widget.notifyIsMountedFn();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Screen B')),
    );
  }
}

Upvotes: 2

C. Swanson
C. Swanson

Reputation: 81

With Navigator.pushReplacement(), if using MaterialPageRoute, then setting

maintainState:false

will ensure that dispose() is called.

Upvotes: 0

user17164092
user17164092

Reputation: 1

Not only to call 'deactivate()' but also to use 'Navigator.pushReplacement()' for page moving is necessary. Not working if you are using 'Navigator.push()'.

Upvotes: -1

I know it's a bit late but I think you should override the deactivate method. Since we are changing the page we are not actually destroying it, that's why the dispose isn't being called.

If you'd like more information this page lists the lifecycle of the stateful widgets.

From the link:

'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.

Upvotes: 18

Rishabh
Rishabh

Reputation: 2548

call Navigator.pushReplacement when routing between first and second screen.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatefulWidget {
  @override
  _FirstRouteState createState() => _FirstRouteState();
}

class _FirstRouteState extends State<FirstRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: RaisedButton(
        child: Text('Open route'),
        onPressed: () {
          Navigator.pushReplacement(
            context,
            MaterialPageRoute(builder: (context) => SecondRoute()),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    // Never called
    print("Disposing first route");
    super.dispose();
  }
}

class SecondRoute extends StatefulWidget {
  @override
  _SecondRouteState createState() => _SecondRouteState();
}

class _SecondRouteState extends State<SecondRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: RaisedButton(
        onPressed: () {
          Navigator.pushReplacement(
            context,
            MaterialPageRoute(builder: (context) => FirstRoute()),
          );
        },
        child: Text('Go back!'),
      ),
    );
  }

  @override
  void dispose() {
    print("Disposing second route");
    super.dispose();
  }
}

Try this

Upvotes: 12

Related Questions