flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.19k stars 27.49k forks source link

dispose() of Stateful widgets is not called when the App is exited by pressing the backbutton #40940

Open HQiang opened 5 years ago

HQiang commented 5 years ago

Why are some widget state dispose() executed when the page is closed, some widget states don't execute dispose, it's really annoying, I want to cry, and the resource release is a problem.

dblokhin commented 5 years ago

It would be nice if you attach some problem code.

HQiang commented 5 years ago

Because there is a lot of code, I can't paste it. I probably tested it myself. This happens when the current page is the last page of the application. Clicking Back will exit the application. At this time, the Widget and the Widget are included. The dispose() of all custom widgets is not executed. I wrote a demo that indicates what I want to express.

void main() => runApp(MyApp());

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

class AppHomeWidget extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return AppHomeState();
  }
}

class AppHomeState extends State<AppHomeWidget>{

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: <Widget>[
          CustomWidget(),
          Text("text")
        ],
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();
    print("AppHomeWidget dispose");
  }

}

class CustomWidget extends StatefulWidget{

  @override
  State<StatefulWidget> createState() {
    return SecondScreenState();
  }
}

class SecondScreenState extends State<StatefulWidget> {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: new Text('CustomWidget')
    );
  }

  @override
  void dispose() {
    super.dispose();
    print("CustomWidget dispose");
  }
}

This demo has only one page. This page is also the last page of the application. In my actual project, it is the project's home page. Before it has a guide page, the login page is just that they have been closed. The home page is the end of the application. A page, similar to my demo, I don't know why, AppHomeWidget dispose and CustomWidget dispose are not printed.

HQiang commented 5 years ago

In short, when the last page of the application is closed, all widgets' dispose() on this page will not be executed.

escamoteur commented 5 years ago

@dblokhin Why the thumbs down if this code reproduces the problem?

escamoteur commented 5 years ago

@tvolkert is this by design?

dblokhin commented 5 years ago

@escamoteur the code was not fenced and highlighted. Currently it's nice. :)

HQiang commented 5 years ago

I think this is a big problem. When I want to quit the application, I want to release some resources. Dispose() does not execute. I really don't know how to implement the resource shutdown. I think this problem should be faced by everyone. How to solve it? I want the answer, I am going crazy.

escamoteur commented 5 years ago

@HQiang What you can do use https://api.flutter.dev/flutter/widgets/WillPopScope-class.html for this.

HQiang commented 5 years ago

@escamoteur Ok, thank you, rest on the weekend, I will try the company next Monday, thank you again, thank you

dblokhin commented 5 years ago

A question is why the state of CustomWidget extends State<StatefulWidget>. I wasn't able to check it but my guess is its the problem. I don't think it is a proper way to make a stateful widget.

escamoteur commented 5 years ago

Hey you have a sharp eye. It has to be State but I guess that's not really the problem because CustomWidget is a State full Widget. So this should only make problems when you try to access the widget from the state. But I will give it a try.

HQiang commented 5 years ago

CustomWidget extends State<StatefulWidget> No problem, in actual projects, you might write extends States<CustomWidget>, but this has no effect. In fact, you can test your own Flutter project to see when the application exits. All StatefulWidget State dispose() on the last page will not be executed. I just came into contact with Flutter and I encountered this problem. As escamoteur said, is this designed? If this is the case, then the last page Widgets will never be removed from the tree? Is it because I didn't write the code according to the rules, but it is really an entry-level code structure, I don't understand.

dblokhin commented 5 years ago

Yeap, I can reproduce the problem. dispose() is never called also on hot restart.

pedromassangocode commented 4 years ago

Still reproducible in 1.21. I updated the code to make the state classes to not expect a StatefulWidget but its own widget type.

complete code (updated) ``` 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: AppHomeWidget(), ); } } class AppHomeWidget extends StatefulWidget{ @override State createState() { return AppHomeState(); } } class AppHomeState extends State{ @override Widget build(BuildContext context) { return new Scaffold( backgroundColor: Colors.white, body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CustomWidget(), Text("text") ], ), ); } @override void dispose() { super.dispose(); print("AppHomeWidget dispose"); } } class CustomWidget extends StatefulWidget{ @override State createState() { return SecondScreenState(); } } class SecondScreenState extends State { @override Widget build(BuildContext context) { return Center( child: new Text('CustomWidget') ); } @override void dispose() { super.dispose(); print("CustomWidget dispose"); } } ```
flutter doctor -v ``` [✓] Flutter (Channel master, 1.21.0-6.0.pre.160, on Mac OS X 10.15.6 19G73, locale en) • Flutter version 1.21.0-6.0.pre.160 at /Users/pedromassango/dev/SDKs/flutter_master • Framework revision 31ee51a302 (13 hours ago), 2020-08-03 17:27:21 -0700 • Engine revision fa16c3d0ba • Dart version 2.10.0 (build 2.10.0-2.0.dev 0f0e04ec3a) • Pub download mirror https://pub.flutter-io.cn • Flutter download mirror https://storage.flutter-io.cn [!] Android toolchain - develop for Android devices (Android SDK version 30.0.1) • Android SDK at /Users/pedromassango/Library/Android/sdk • Platform android-30, build-tools 30.0.1 • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593) ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses [✓] Xcode - develop for iOS and macOS (Xcode 11.6) • Xcode at /Applications/Xcode.app/Contents/Developer • Xcode 11.6, Build version 11E708 • CocoaPods version 1.9.3 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [!] Android Studio (version 4.0) • Android Studio at /Applications/Android Studio.app/Contents ✗ Flutter plugin not installed; this adds Flutter specific functionality. ✗ Dart plugin not installed; this adds Dart specific functionality. • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593) [✓] IntelliJ IDEA Community Edition (version 2020.2) • IntelliJ at /Applications/IntelliJ IDEA CE.app • Flutter plugin version 47.1.4 • Dart plugin version 202.6397.47 [✓] Connected device (3 available) • iPhone SE (2nd generation) (mobile) • 1119074E-0493-4533-9212-6758A33EB40C • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-6 (simulator) • Web Server (web) • web-server • web-javascript • Flutter Tools • Chrome (web) • chrome • web-javascript • Google Chrome 84.0.4147.105 ! Doctor found issues in 2 categories. ```
carman247 commented 3 years ago

I was facing a similar issue but I think the problem stemmed from calling super.dispose(); before anything else ... I've moved it to the end of the dispose function and seems ok now .. Not sure if this is why others are facing issues though, and I don't really understand what the super method is for 😄

morgwai commented 3 years ago

even examples from official documentation trigger this bug: https://flutter.dev/docs/cookbook/networking/web-sockets#complete-example why is no one working on it since 2019? was State.dispose() deprecated? if so, what should be used instead to reliably release resources on app exit?

escamoteur commented 3 years ago

you have to register a handler for app live cycle events.

morgwai commented 3 years ago

@escamoteur do you suggest it as a temporary workaround or are you implying that State.dispose() should not be relied on? the latter is contrary to many of the current docs, but this may be a bug in docs. Could you please provide a link to an authoritative source if you think this State.dispose() should not be used/relied on?

Thanks!

escamoteur commented 3 years ago

Where is stated that it will get called in case the App gets killed?

morgwai commented 3 years ago

@escamoteur now you are just trolling: from the source docs:

/// Subclasses should override this method to release any resources retained /// by this object (e.g., stop any active animations).

moreover, there's a lot of examples in the official docs that use dispose() for this purpose, you can find one instance in the link I provided before.

However, as I wrote before, these may be bugs in the docs. That's why I asked if you happen to have any link to an authoritative docs that say that dispose() should not be used/relied on for this purpose?

escamoteur commented 3 years ago

it is always called when the widget is removed from the widget tree. I don't think it is guaranteed when the app is killed. What resources do you have to dispose in such a case?

escamoteur commented 3 years ago

@Hixie could you confirm, that this behavior is by design?

morgwai commented 3 years ago

@escamoteur this bug is exactly about the fact that it should be called when the app quits in a normal way (such as pressing back on the last mobile screen, closing browser tab, closing desktop app window). This is the behavior that everyone expects, including core flutter libraries authors, as you can see from the websocket example. This is also the behavior that source documentation describes.

Unless there's a really strong reason not to call it when a view is getting destroyed because of app's exit, it should be fixed. The main reason is that otherwise you will have to expose the resources that are local to a particular screen, so that the app life cycle handlers can clean them up. This breaks abstraction and encapsulation rules.

Finally, if a strong reason not to fix it indeed exists (which I personally doubt), the current documentation should be fixed.

escamoteur commented 3 years ago

I'm not sure if the framework has the time to do that in case of an app being closed by back presses as Android rather drastically closes Apps. the other question is which resources do you have to release in such a case that aren't released anyway by the OS if the App is killed? BTW, I would be a bit careful who you call trolling

morgwai commented 3 years ago

@escamoteur since it is possible to do it in the app life cycle handlers as you wrote, it strongly suggests that there generally is the time for it. Moreover, pressing 'back', can be handled programmatically and does not need to remove the current view at all (for example in android chrome browser, it loads the previous page without removing fragment/activity), so a simple solution is for the flutter framework to do it this way on mobile (by overriding default 'back' button behavior, which I believe it already does in general, as separate flutter views are not mapped 1-1 with android activities/fragments AFAIK)

network connections using any protocol that requires some kind of a terminal message before closing is a good example of a resource that is not cleaned properly by the OS. Failure to send a terminating frame may result for example in a transaction being undesirably rolled back by a program on the other side of the connection. (to some extent a websocket is such a protocol: it requires an explicit notification before closing. if you run the above linked example on a browser, it will automatically send a terminating frame if you close the tab, as browsers are aware of websocket protocol. However if you run it on mobile or desktop, the final frame will no be sent and you will receive error on the server side).

escamoteur commented 3 years ago

Do a bit of research on App live cycling on Android, it's a very unpleasant topic :-) From what I read so far you have to use a plugin to securely handle all live cycle events on Android. Flutter uses several threads so I'm not sure if the UI thread that is responsible of the Widget tree would get information fast enough to react.

As you say a Webbrowser is aware of the Websockets which make all the difference.

Also I would never ever handle a network protocol in the UI layer. So you have to hook into the live cycle events anyway if you really need to. On the other side a backend should be totally able to cope with any interruption because mobile devices can always loose connections. I'm not even sure if there is a way to react on the termination of an application on Windows because there is no concept of live cycle events there.

Easiest way is to use WillPopScope if you want to react on the backbutton and manage this intentionally instead of relying on the live cycle event. IMHO the reason this hasn't gotten more attention is that in most cases it isn't any real problem. And it's also not an error in the documentation of Stateful Widget as ending an app is outside the normal UI events. However it is probably a good idea to add a comment that this case isn't covered at the moment.

morgwai commented 3 years ago

@escamoteur if a network connection is a bound and local to a particular UI screen (ie, should be closed once a user willfully navigates away from this screen), it makes perfect sense to close it in dispose. For example, if a user willingly navigates away from a chat screen, it makes perfect sense to notify other members that he has left.

The framework should make it easy for developers to do it: they should not be forced to complicate their code by disposing resources both in dispose (in case this view was not at the bottom of the stack) and then again check if they were disposed properly in life-cycle handlers (in case it was at the bottom). As i wrote before, this also forces a bad design on devs as they need to expose resources that should remain private.

If a programmer can use life-cycle handlers to manually check if all resources were released, then the framework can use exactly the same mechanism to make sure dispose was called on all widgets. The fact how complicated android app life-cycle is does not have anything to do here: if it works for devs, it works for framework also. it's just a matter if the framework forces devs to do this work manually, or if it does the expected tasks for them.

escamoteur commented 3 years ago

Disagree. Dispose is for disposing UI element subscriptions or animation controllers and the like and not to model app logic. If you willfully navigate away from a screen you can call a disposal function at that point.

And if an OS doesn't offer a live cycle event or gives the App only a limited amount of time to react before a process is just killed it's not like you can do everything. If possible you shouldn't rely any app logic on that events.

morgwai commented 3 years ago

a more 'complete' example: if a user willingly leaves an online game by navigating away from a game screen (for example in a 'safe location' where games allows to safely stop playing and finally let the player go to sleep ;-) ), his character should be removed from the game instead of letting other users take advantage of stale player who can be easily looted.

escamoteur commented 3 years ago

Again, that would be core business logic that should not depend of the state of a Widget. It would be something I always call explicitely

morgwai commented 3 years ago

Disagree. Dispose is for disposing UI element subscriptions or animation controllers and the like and not to model app logic.

well, you have a right to your opinion of course, but that's not what majority of devs expects, including core libs devs. Finally, as I explained, the current situation forces more complicated code and bad design.

If you willfully navigate away from a screen you can call a disposal function at that point.

dispose should exactly be telling devs: "hey, the user just decided to navigate away". This is exactly what this bug is about.

And if an OS doesn't offer a live cycle event or gives the App only a limited amount of time to react before a process is just killed it's not like you can do everything. If possible you shouldn't rely any app logic on that events.

as I already wrote before this is incorrect: if a dev can use a life-cycle handler, the framework can use it also.

escamoteur commented 3 years ago

well, you have a right to your opinion of course, but that's not what majority of devs expects, including core libs devs. Finally, as I explained, the current situation forces more complicated code and bad design.

I have no idea who core lib devs are (I'm publishing packages since almost 3 years now). It's exactly what is stated in the docs. And IMHO you get worse design if you connect you app logic to a Widgets disposal function that is clearly intended for a Widget to clean up itself.

If you willfully navigate away from a screen you can call a disposal function at that point.

dispose should exactly be telling devs: "hey, the user just decided to navigate away". This is exactly what this bug is about. That is you interpretation of you want dispose to be.

And if an OS doesn't offer a live cycle event or gives the App only a limited amount of time to react before a process is just killed it's not like you can do everything. If possible you shouldn't rely any app logic on that events.

as I already wrote before this is incorrect: if a dev can use a life-cycle handler, the framework can use it also. Please study a bit more the problems people all around have with life-cycle events. I don't say it might not be possible the question is how much complexity it adds for something that can be solved easily differently. And for a cross platform framework it has to work everywhere. Win32 e.g. doesn't have a concept for reacting when an app closes.

This issue has 5! likes and the Flutter team has limited resources. if it were such a pressing issue it had way more likes.

morgwai commented 3 years ago

I have no idea who core lib devs are (I'm publishing packages since almost 3 years now).

I believe websocket support is a core library. you could probably find more examples.

And IMHO you get worse design if you connect you app logic to a Widgets disposal function that is clearly intended for a Widget to clean up itself.

neither UI widgets nor network connections are exempted from object-oriented design rules: if a parent object owns a child object (meaning that child object is created/initialized in parent's initialization code and is supposed to be destroyed/released when the parent is destroyed/released), then the child object should be released in parents final life-cycle method. The fact that parent is UI widget and child is either a connection, an animation or whatever else, does not affect object-oriented design rules. Stating that "this is widget's life-cycle method called at the end of life, but you are allowed to release only UI related child objects in it: for any other child objects find yourself another way" does not follow good practices of object-oriented design in my opinion.

dispose should exactly be telling devs: "hey, the user just decided to navigate away". This is exactly what this bug is about.

That is you interpretation of you want dispose to be.

I believe that this is what object-orient design rules suggest and what majority of flutter devs expect.

And if an OS doesn't offer a live cycle event or gives the App only a limited amount of time to react before a process is just killed it's not like you can do everything. If possible you shouldn't rely any app logic on that events.

as I already wrote before this is incorrect: if a dev can use a life-cycle handler, the framework can use it also.

Please study a bit more the problems people all around have with life-cycle events. I don't say it might not be possible the question is how much complexity it adds for something that can be solved easily differently.

in the same sentence you mention how many problems ppl have with life-cycle events and then suggest that the child resource releasing can be solved easily by using life-cycle events, I presume as this is the way you suggested in the first message. Or do you have something else in mind? apologies if I misunderstood.

And for a cross platform framework it has to work everywhere. Win32 e.g. doesn't have a concept for reacting when an app closes.

I haven't been using windows for many years now indeed, but I'm pretty sure that when I close a notepad (or another file editing app) with unsaved changes, instead of closing immediately, the app will display a popup asking if changes should be saved before quitting.

This issue has 5! likes and the Flutter team has limited resources. if it were such a pressing issue it had way more likes.

https://stackoverflow.com/questions/61611415/dispose-method-not-called-in-flutter https://stackoverflow.com/questions/52571976/dispose-doesnt-dispose-completely https://stackoverflow.com/questions/58893259/flutter-provider-dispose-method-not-called-when-exiting-the-app https://stackoverflow.com/questions/54599561/flutter-navigation-and-the-requirement-for-dispose-in-the-mean-time ... I could be copy-pasting links for another 20 minutes or so... People get constantly confused by this, ask in many places on the web, some of them finally arrive here, they see that the issue has been open for 2 years and no one is assigned and give-up. Dismissive answers suggesting "where did you come up with such a crazy idea that this method should get called" from someone with 'contributor' badge do not encourage people to participate in the discussion either. Neither do threatening people when they point it out.

morgwai commented 3 years ago

I found this comment by @Hixie in a similar, closed issue:

The dispose method on State should not be called when the app quits.

The behaviour of the following three cases should all be the same:

  • Hitting the home button
  • Hitting the back button on the first page
  • Switching applications in the app switcher

I'd like to point that the behavior in the first case will never be the same in general because of how android works: in the 1st and 3rd case, the app will generally not be terminated immediately (unless the system is tight on resources). Therefore if a user navigates back to the app via 'recent apps' switcher, the old connection will still be open (unless server closes it due to application specific timeout of client inactivity) and the server will not even notice that the user navigated away and returned again. However, in the 2nd case, the app is always terminated immediately and all its OS resources released. It may be removed from the app switcher or not (depending on android version/flavor, but not sure here). If it's not removed and the user navigates back to it, the old state will have been already discarded by this time, a new instance of state will be created and a new connection to the server will be established. Therefore, since the state is always destroyed in the 2nd case (but not necessarily in 1st and 3rd case), I claim that there should be a reliable way to release state's child resources according to the general rule of resource ownership mentioned before. Since dispose method already exists and many devs expect it to be exactly this mechanism, I claim that dispose method should be guaranteed to be called on app exit. If there is a desire to unify behavior in the above 3 cases, then the only way to achieve this is by reliably terminating app in all 3 cases and thus reliably calling dispose also. (which personally I think is a terrible idea: I don't want to lose app's state whenever I switch to another for 10 seconds to copy some text).

escamoteur commented 3 years ago
  1. On windows the app really has to do that intentionally and there are not events.
  2. A short poll on Twitter with some people I highly regard as experts confirmed my opinion only that you should not use dispose or any lifecycle-methods to control app logic. It's not reliable enough
  3. I perceived your tone from the beginning as pretty aggressive.
Hixie commented 3 years ago

Generally speaking you can't rely on anything happening when the OS terminates your app. Many platforms (including iOS, Android, Linux, and Web) give you little to no notice that you are being terminated at the best of times, and at the worst of times (power outage, sudden unexpected physical destruction of the hardware, kernel panic, application crash) not even the operating system gets any notice.

All of which means you cannot rely on dispose being called during shutdown. For this reason, we prefer to model the framework as if shutdown was not something that happened.

You can listen to the lifecycle events though if you want to try to do some cleanup on application shutdown. It won't be reliable, but it's possible. This can either use the framework hooks or you can write plugin code to provide platform-specific behaviour.

morgwai commented 3 years ago

@Hixie

Generally speaking you can't rely on anything happening when the OS terminates your app. Many platforms (including iOS, Android, Linux, and Web) give you little to no notice that you are being terminated at the best of times, and at the worst of times (power outage, sudden unexpected physical destruction of the hardware, kernel panic, application crash) not even the operating system gets any notice.

All of which means you cannot rely on dispose being called during shutdown. For this reason, we prefer to model the framework as if shutdown was not something that happened.

there has always been some totally unrecoverable situations, like fiber getting cut, data-center getting blown away etc, but so far they were never considered to be a good reason to give up on a graceful termination in situations when it is possible. On the contrary: given the growing popularity of protocols like gRPC and long-living stream connections in general, it gets more and more important to properly terminate connections, so that the recovery/resuming is easier or even at all possible.

I understand that reliability of such mechanisms will differ from platform to platform, but I claim that on each particular one an effort should be made to make it as reliable as possible:

Again, I understand that these mechanisms have various degrees of reliability (on linux SIGSEGV cannot be handled, on android onDestroy may not get called if the system kills the whole process of an inactive app, etc), but each of them is way better than just giving-up. Otherwise, Flutter as a platform will be less reliable in several situations comparing to each platform's natively developed apps, which in turn will force devs back to native development in such cases.

You can listen to the lifecycle events though if you want to try to do some cleanup on application shutdown. It won't be reliable, but it's possible. This can either use the framework hooks or you can write plugin code to provide platform-specific behaviour.

is there any strong reason why Flutter shouldn't itself automatically listen to these events (as reliably as feasible) and make sure that each state's dispose gets called? As mentioned before, it takes away this burden from a developers, promotes simpler and cleaner code (many developers will basically expose parts of states that should remain private to the life-cycle even handlers), which are pretty good reasons, I think.

Please reconsider pros and cons of this decision.

Hixie commented 3 years ago

is there any strong reason why Flutter shouldn't itself automatically listen to these events (as reliably as feasible) and make sure that each state's dispose gets called?

I mean, I described the reason in my last comment. You may not agree that it's strong, but it's the reason. We believe apps would be better if they were designed to assume they will never get shutdown notifications, and instead are written to assume they might disappear without notice at any time. Writing an app that way means it works great in the same situations that an app would work great if it did have shutdown notifications, and works great in all the other cases where an app that relies on shutdown notifications would have a terrible user experience.

AbhishekDoshi26 commented 3 years ago

Sometimes, if dispose is not called, deactivate is called. I had another scenario. If we have any button as actions of appBar and from that if we navigate to any page, previous page's dispose is not called but deactivate is called.

sebastianhaberey commented 2 years ago

Here's another vote for a graceful shutdown. My application is registering as a service on the local network via DNS-SD. DNS-SD requires the application to unregister once the service is not available anymore. Without unregistering, DNS-SD will list the dead service as available to clients.

A graceful shutdown would help a lot in minimizing those situations. Of course, if the OS absolutely SIGKILL s the application, that would be pointless to implement.

salva73 commented 2 years ago

Another example of the absurdity of this situation. I have implemented some push notifications and I need to save the state of the application so that later, in the background function of the push notifications, I can make some decisions or others. A determining state that I need to store in the sharedpreferences is when the user terminates the application, that is, when it goes to detached state, because I cannot store it in the shared preference because detached already happened before sending the state to the didchangeapplifecycle function. How am I supposed to know what state it is in when I open the application, if I can't save the state before

maheshj01 commented 1 year ago

Reproducible on both IOS and Android. Dispose method isn't called on the app exit.

code sample ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const AppHomeWidget(), ); } } class AppHomeWidget extends StatefulWidget { const AppHomeWidget({super.key}); @override State createState() { return AppHomeState(); } } class AppHomeState extends State { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [CustomWidget(), Text("text")], ), ); } @override void dispose() { print("AppHomeWidget dispose"); super.dispose(); } } class CustomWidget extends StatefulWidget { const CustomWidget({super.key}); @override State createState() { return SecondScreenState(); } } class SecondScreenState extends State { @override Widget build(BuildContext context) { return const Center(child: Text('CustomWidget')); } @override void dispose() { print("CustomWidget dispose"); super.dispose(); } } ```
flutter doctor -v (mac) ``` [✓] Flutter (Channel master, 3.7.0-3.0.pre.33, on macOS 12.6 21G115 darwin-arm64, locale en-IN) • Flutter version 3.7.0-3.0.pre.33 on channel master • Upstream repository https://github.com/flutter/flutter.git • Framework revision 5201856805 (38 minutes ago), 2022-12-05 18:27:21 -0800 • Engine revision a309d239c4 • Dart version 2.19.0 (build 2.19.0-463.0.dev) • DevTools version 2.20.0 • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades. [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc4) • Android SDK at /Users/mahesh/Library/Android/sdk • Platform android-33, build-tools 33.0.0-rc4 • ANDROID_HOME = /Users/mahesh/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 14.0.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14A400 • CocoaPods version 1.11.3 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2021.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840) [✓] IntelliJ IDEA Community Edition (version 2021.2.1) • IntelliJ at /Applications/IntelliJ IDEA CE.app • Flutter plugin version 61.2.4 • Dart plugin version 212.5080.8 [✓] VS Code (version 1.70.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.55.20221129 [✓] Connected device (3 available) • iPhone 12 Pro (mobile) • 026D5789-9E78-4AD5-B1B2-3F8D4E7F65E4 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-5 (simulator) • macOS (desktop) • macos • darwin-arm64 • macOS 12.6 21G115 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.94 [✓] HTTP Host Availability • All required HTTP hosts are available • No issues found! ``` ``` [✓] Flutter (Channel stable, 3.3.9, on macOS 12.6 21G115 darwin-arm, locale en-IN) • Flutter version 3.3.9 on channel stable at /Users/mahesh/Development/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision b8f7f1f986 (24 hours ago), 2022-11-23 06:43:51 +0900 • Engine revision 8f2221fbef • Dart version 2.18.5 • DevTools version 2.15.0 [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc4) • Android SDK at /Users/mahesh/Library/Android/sdk • Platform android-33, build-tools 33.0.0-rc4 • ANDROID_HOME = /Users/mahesh/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 14.0.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14A400 • CocoaPods version 1.11.3 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2021.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840) [✓] IntelliJ IDEA Community Edition (version 2021.2.1) • IntelliJ at /Applications/IntelliJ IDEA CE.app • Flutter plugin version 61.2.4 • Dart plugin version 212.5080.8 [✓] VS Code (version 1.70.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.53.20221101 [✓] Connected device (3 available) • iPhone 12 Pro (mobile) • 026D5789-9E78-4AD5-B1B2-3F8D4E7F65E4 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-5 (simulator) • macOS (desktop) • macos • darwin-arm64 • macOS 12.6 21G115 darwin-arm • Chrome (web) • chrome • web-javascript • Google Chrome 107.0.5304.110 [✓] HTTP Host Availability • All required HTTP hosts are available • No issues found! ```
AurelienBallier commented 1 year ago

This is also the case on Linux Desktop, dispose is not called on application normal exit. The only way around I found was to use native code and MethodChannel to call dispose() before exit. For those interested, this article give some details about ways to implement native code on different platforms.

chaerf commented 1 year ago

Same here with another use case. Im playing background FM Radio source. if the app is shutdown, need to stop he signal and dispose the resources, if not the Radio will keep playing until another audio source send the singal to use the device main audio channel.

ndusart commented 1 year ago

There seem to be a gap between what reporters tries to express and how flutter devs understand it.

@Hixie @escamoteur I don't think reporters want to be able to handle termination cases. If application is terminated by the OS, it is normal to not being able to execute any code in this process. That's really not the point reported here, reporters are talking about proper exit of the last screen or even process normal exit in case of desktop platforms and resource management consistency. Please do not disregard it as a termination case, that's not.

Disagree. Dispose is for disposing UI element subscriptions or animation controllers and the like and not to model app logic.

How can this be true ? Animations and UI elements subscriptions are not part of app logic then ? Animations are not the only resources that is tied to UI lifecycle, we can have a socket which is only meaningful while a certain widget is visible, we can want to release the NFC reader when going out of the screen asking for NFC device, etc... there are plenty of other cases possible.

Every argument you can bring on that this could be handled by other mean in the app logic could be applied to animations as well. If you are true, well, just let agree to remove the useless dispose entirely then.

Just to rephrase the questioning then: Why do not you consider a normal exit of the process as being a proper event to trigger UI element disposal ?

I cannot find any other language runtime where process exit does not produce at least stack unwinding and let clean up our resources properly when that is feasible.

Hixie commented 1 year ago

The problem is that there is no such thing as a normal exit on some of the platforms we support. For example, on iOS you literally cannot quit an app. If you do, it is considered equivalent to a crash.

If you want to just dispose the entire widget tree (running all the dispose methods etc) before quitting an app on platforms that support it, you could run runApp(SizedBox.shrink()) or similar before you tell the OS to close the application.

ndusart commented 1 year ago

Of course, there is a level of abstraction brought by Flutter and all platforms cannot strictly adhere to the same terms. As we can effectively accept that there is no such thing as a process exit strictly speaking for mobile platforms as the OS (at least Android in my knowledge) will try to keep this process alive for running other activities/services/..., I'd argue that there is still an analogue to a "clean exit" in Flutter abstraction which is called SystemNavigator.pop(). On Linux platform, this is very close to a normal exit (if we do not take into consideration it still crashes and aborts the process 3 times out of 4 in my case).

Why this is not same as poping all widget from the tree before "exiting" the application ?

It makes very hard to consistently manage resources properly across all platforms. We have to hack around using either willPopScope, didChangeAppLifecycleState or either directly manipulating the code inside android/, ios/ or linux/ and having to cautiously examine the changes brought by new flutter version in order to keep our custom changes. Having a matching call between initState and dispose would help tremendously in these situations.

Let consider the following example of an application that binds to a TCP port when opening a Widget and closes it when widget is popped out of the tree. The TCP port is just for having a simple example to show and play with, but it can be any kind of resources, really.

It is the main.dart of the project targeting Android and Linux platforms, other files are identical to a bare flutter create --platforms android,linux. (Except the new dependency ffi: ^2.0.2 in pubspec.yaml):

import 'dart:ffi' as ffi;
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/***************************************************************
  FFI: Export symbols from libc to open and bind TCP socket
       + define struct definition of sockaddr_in
      (it may feel overly complicated for an example, but I want to show an actual example for a resource 
       that effectively pose a problem if not released because it cannot be reacquired if kept too long.
       exporting syscalls is for playing with TCP socket without bringing async/await which
       would be way more complex for an example, ironically)
****************************************************************/
final class SockAddrIn extends ffi.Struct {
    @ffi.Int16()
    external int family;
    @ffi.Int16()
    external int port;
    @ffi.Int32()
    external int address;
    @ffi.Int32()
    external int zero1;
    @ffi.Int32()
    external int zero2;
  }

class libc {
  static final ffi.DynamicLibrary _lib = ffi.DynamicLibrary.process();

  static final int Function(int, int, int) socket = _lib.lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int, ffi.Int)>>("socket").asFunction();
  static final int Function(int, ffi.Pointer<SockAddrIn>, int) bind = _lib.lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Pointer<SockAddrIn>, ffi.Int)>>("bind").asFunction();
  static final int Function(int, int) listen = _lib.lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int)>>("listen").asFunction();
  static final int Function(int) htons = _lib.lookup<ffi.NativeFunction<ffi.Int16 Function(ffi.Int16)>>("htons").asFunction();
  static final int Function(int) close = _lib.lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int)>>("close").asFunction();
}
/***************************************************************
  END OF FFI:
****************************************************************/

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late final int sockfd;

  @override
  void initState() {
    // Bind a TCP socket to port 8080
    sockfd = libc.socket(2, 1, 0);
    ffi.Pointer<SockAddrIn> sockaddr = calloc<SockAddrIn>();
    sockaddr.ref.family = 2;
    sockaddr.ref.port = libc.htons(8080);
    sockaddr.ref.address = 0;
    var r = libc.bind(sockfd, sockaddr, 16);
    if( r == 0 ) {
      r = libc.listen(sockfd, 1);
    }

    if( r != 0 ) {
      throw 'Failed to open TCP port 8080';
    }
  }

  @override
  void dispose() {
    // release resource
    libc.close(sockfd);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Push button to exit app',
            ),
            ElevatedButton(
              child: Text("Exit"),
              onPressed: (){
                SystemNavigator.pop();
              },
            ),
          ],
        ),
      ),
    );
  }
}

We have now the problem that was reported here: dispose is never called when exiting the application, even using SystemNavigator.pop, and the resource management become complicated to handle portably across platforms.

Following the platforms, we would have to either:

These two solutions are either adding complexity or completely not maintainable in the longterm. And this add inconsistency in the way we manage resources. The inconsistency can be expressed by doing an analogy to how Android let manage lifecycle of Activities. If an activity is popped out of the stack, onDestroy will be called. Wouldn't you find it weird that for some reason, the last activity for the backstack will just be destroyed without onDestroy being called ? The State's dispose has this kind of inconsistency and I'd guess a very high number of developers does not expect this.

If you run the given example project in Android, you'll experience the fun fact that crashes end up being a safer exit condition than proper "app exit", since during crash, the OS will close the opened socket while when exiting the application using SystemNavigator.pop (or using back button) the process is kept running and opened socket will never been close. Application will not being able to bind to this port ever again without being manually killed.

In other words, it means that for a given process, we can have multiple initState called for the same widget in the tree that are not matched by a call to dispose. I can hear that it is not the exact same widget, but where did the previous one go then ? The unbalance between initState/dispose calls is a big sign that there is something wrong here.

@Hixie if I made some bad assumptions, please correct me and points us to documentation that define how to handle these cases properly. But for me, there is either a lack of documentation in this area or there are inconsistencies in how dispose are called.

ndusart commented 1 year ago

As mentioned earlier by @maheshmnj #53997 is a very good example of what can go south when initState is not matched by a dispose.

In Android, contrary to what you seem to believe, the process is not stopped when going out of the last activity. The activity is destroyed but the process is still running.

It ends up with this buggy behaviour where some code may still be running but end up trying to reach a dead Flutter engine, which cannot be handled by dart code. The reporter needs to implement this hacky workaround to put a blank route at the root so that going back from the Widget which needs cleaning has its dispose method actually called. This blank route is only there to then stop the app by calling SystemNavigator.pop.

I cannot see how this can't be considered as a bug from the flutter engine, but if the problem lies in our usage of Widget, please let us know how we can properly handle these cases for all your tier-1 platforms.

Hixie commented 1 year ago

The Android issue will probably get resolved when @goderbauer's multiwindow work lands (or some time after it lands, once we remember to hook up Android to it!).

Let consider the following example of an application that binds to a TCP port when opening a Widget and closes it when widget is popped out of the tree.

Does adding runApp(SizedBox.shrink) just before the SystemNavigator.pop() call do what you want?

ndusart commented 1 year ago

Does adding runApp(SizedBox.shrink) just before the SystemNavigator.pop() call do what you want?

Not really, I need then to comment SystemNavigator.pop() for dispose to be called.

I get your point that we could always use some alternative to effectively call our clean-up code when we want to exit the app programmatically. The problem is that the exit intent can come from other sources and each platform has its own specific sources (back button on Android, window close button on desktop, close tab in web,...).

While flutter should provide the common abstraction layer, it does not for this specific case and it adds a burden to the app developer which is likely to think dispose is the function to use for releasing what is acquired in initState by reading official docs and guides. And the expectation from developers would be even stronger given the fact that this is the actual behavior for widget tree changes outside flutter engine termination.

The Android issue will probably get resolved when @goderbauer's multiwindow work lands (or some time after it lands, once we remember to hook up Android to it!).

That's encouraging :)

How could we track the progress of it ? Is it https://github.com/goderbauer/mvp ? It seems desktop-oriented, so shouldn't it fix it for linux platform as well ?