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
163.66k stars 26.91k forks source link

flutter first StatefulWidget dispose method not called when app quit #28134

Closed keluokeda closed 4 years ago

keluokeda commented 5 years ago

Internal: b/142035030

if flutter app first page is StatefulWidget, in android when user press back button,State dispose method not called.

[✓] Flutter (Channel master, v1.2.1-pre.142, on Mac OS X 10.14.2 18C54, locale zh-Hans-CN)
    • Flutter version 1.2.1-pre.142 at /Users/yuyanhui/Desktop/flutter/flutter
    • Framework revision fec01201b1 (9 days ago), 2019-02-09 11:23:28 -0500
    • Engine revision 067045757f
    • Dart version 2.1.2 (build 2.1.2-dev.0.0 174d6fec3d)

[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    • Android SDK at /Users/yuyanhui/Library/Android/sdk
    • Android NDK at /Users/yuyanhui/Library/Android/sdk/ndk-bundle
    • Platform android-28, build-tools 28.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)
    • All Android licenses accepted.

[!] iOS toolchain - develop for iOS devices (Xcode 10.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 10.1, Build version 10B61
    ✗ Verify that all connected devices have been paired with this computer in Xcode.
      If all devices have been paired, libimobiledevice and ideviceinstaller may require updating.
      To update with Brew, run:
        brew update
        brew uninstall --ignore-dependencies libimobiledevice
        brew uninstall --ignore-dependencies usbmuxd
        brew install --HEAD usbmuxd
        brew unlink usbmuxd
        brew link usbmuxd
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    • ios-deploy 1.9.2
    ✗ ios-deploy out of date (1.9.4 is required). To upgrade with Brew:
        brew upgrade ios-deploy
    • CocoaPods version 1.5.0

[✓] Android Studio (version 3.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 32.0.1
    • Dart plugin version 182.5215
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)

[!] IntelliJ IDEA Ultimate Edition (version 2018.3.3)
    • IntelliJ at /Applications/IntelliJ IDEA.app
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    • Dart plugin version 183.5153.38
    • For information about installing plugins, see
      https://flutter.io/intellij-setup/#installing-the-plugins

[!] VS Code (version 1.30.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    ✗ Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (2 available)
    • Nexus 5X • 00f11dd26565c098 • android-arm64 • Android 8.1.0 (API 27)
    • m1 note  • 71MBBL524BNG     • android-arm64 • Android 5.1 (API 22)

! Doctor found issues in 3 categories.
zoechi commented 5 years ago

When the app is exited then no further Dart code is executed. So this is working as intended.

Are you looking for https://github.com/flutter/flutter/issues/6827 or https://stackoverflow.com/questions/52074265/flutter-detect-killing-off-the-app/52074534#52074534?

keluokeda commented 5 years ago

i think State.dispose lifecycle method should called when this state destroy,just like android Activity.onDestroy, whether quit this State(Activity) or quit app. I have to do something when state dispose, but when app quit dispose method not called.

this is my code

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue, primaryColor: Colors.white),
      home: MainActivity(),
    );
  }
}

class MainActivity extends StatefulWidget {
  @override
  _MainActivityState createState() => _MainActivityState();
}

class _MainActivityState extends State<MainActivity>
    with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();

    print('initState');
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void deactivate() {
    super.deactivate();
    //this method not called when user press android back button or quit
    print('deactivate');
  }

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

    //this method not called when user press android back button or quit
    print('dispose');
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    //print inactive and paused when quit
    print(state);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Title'),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            SystemNavigator.pop();
          },
          child: Text('Quit'),
        ),
      ),
    );
  }
}
zoechi commented 5 years ago

I don't think this will happen, but I leave it open to let the specialists make the final take.

csshuai commented 5 years ago

I encounter the same problem at the moment, I can't dispose resources in dispose method. Even worse, App crashed.

W/art     (19306): Native thread exiting without having called DetachCurrentThread (maybe it's going to use a pthread_key_create destructor?): Thread[14,tid=19333,Native,Thread*=0x9e73ec00,peer=0x12ebe0a0,"Thread-19292"]
E/SensorManager(19306): Exception dispatching input event.
D/AndroidRuntime(19306): Shutting down VM
E/AndroidRuntime(19306): FATAL EXCEPTION: main
E/AndroidRuntime(19306): Process: com.github.flutter.xxx_example, PID: 19306
E/AndroidRuntime(19306): java.lang.RuntimeException: Cannot execute operation because FlutterJNI is not attached to native.
E/AndroidRuntime(19306):    at io.flutter.embedding.engine.FlutterJNI.ensureAttachedToNative(FlutterJNI.java:514)
E/AndroidRuntime(19306):    at io.flutter.embedding.engine.FlutterJNI.dispatchPlatformMessage(FlutterJNI.java:444)
E/AndroidRuntime(19306):    at io.flutter.embedding.engine.dart.DartMessenger.send(DartMessenger.java:74)
E/AndroidRuntime(19306):    at io.flutter.embedding.engine.dart.DartExecutor.send(DartExecutor.java:149)
E/AndroidRuntime(19306):    at io.flutter.view.FlutterNativeView.send(FlutterNativeView.java:142)
E/AndroidRuntime(19306):    at io.flutter.plugin.common.EventChannel$IncomingStreamRequestHandler$EventSinkImplementation.success(EventChannel.java:213)
flutter doctor -v
[✓] Flutter (Channel master, v1.3.3-pre.17, on Mac OS X 10.14.3 18D109, locale zh-Hans-CN)
    • Flutter version 1.3.3-pre.17 at /Users/xxx/git/dart/flutter
    • Framework revision 740fb2a8bb (5 hours ago), 2019-03-02 13:19:49 +0000
    • Engine revision 39c46dea4b
    • Dart version 2.2.1 (build 2.2.1-dev.0.0 7c70ab1817)

[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    • Android SDK at /Users/xxx/Library/Android/sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-28, build-tools 28.0.3
    • Java binary at: /Users/xxx/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/182.5264788/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)
    • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 10.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 10.1, Build version 10B61
    • ios-deploy 1.9.4
    • CocoaPods version 1.5.3

[✓] Android Studio (version 3.3)
    • Android Studio at /Users/xxx/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/182.5264788/Android Studio.app/Contents
    • Flutter plugin version 33.3.1
    • Dart plugin version 182.5215
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)

[✓] IntelliJ IDEA Ultimate Edition (version 2018.3.5)
    • IntelliJ at /Users/xxx/Applications/JetBrains Toolbox/IntelliJ IDEA Ultimate.app
    • Flutter plugin version 30.0.2
    • Dart plugin version 182.5124

[✓] VS Code (version 1.30.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 2.23.0

@zoechi

kw2019ltd commented 5 years ago

same in case you main app class is state full dispose is not being called when app exit.

KaYBlitZ commented 5 years ago

Encountered the same issue, but I found a good workaround that works for me:

return MaterialApp(
      home: WillPopScope(
          child: HomePage(),
          onWillPop: () {
            dispose();
            return Future.value(true);
          }),
    );

This seems to work perfectly for me. No errors too.

HumFei commented 5 years ago

Encountered the same issue too,I handled this way:

//flutter
import 'package:flutter/services.dart';
@override
void initState() {
  super.initState();
  print('initState');
  new MethodChannel("flutter.temp.channel").setMethodCallHandler(platformCallHandler);
}

Future<dynamic> platformCallHandler(MethodCall call) async {
  if (call.method == "destroy"){
    print("destroy");
    dispose();
  }
}
//MainActivity.java
@Override
protected void onStop() {
    super.onStop();
    Log.i("=== MainActivity ===", "onStop: ");
    new MethodChannel(getFlutterView(), "flutter.temp.channel").invokeMethod("destroy", null, null);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    Log.i("=== MainActivity ===", "onDestroy: ");
    new MethodChannel(getFlutterView(), "flutter.temp.channel").invokeMethod("destroy", null, null);
}
mehmetf commented 4 years ago

We need a recommendation or a fix here. Various Google teams are hitting this as well and the workarounds described above are gnarly.

I am especially intrigued by

return MaterialApp(
      home: WillPopScope(
          child: HomePage(),
          onWillPop: () {
            dispose();
            return Future.value(true);
          }),
    );

since it indicates Flutter is getting the signal to pop its home route. I was originally assuming that we are simply missing the back button press since the activity is being finished. That does not seem to be the case, so why isn't the widget tree purged?

chunhtai commented 4 years ago

@goderbauer Is the navigator refactor going to solve this issue?

audkar commented 4 years ago

(My speculations): This is more related how FlutterView is embedded into native platform and what events it emits/does'n on activity destroy. So maybe @blasten can give some clarity :)

goderbauer commented 4 years ago

I believe this is an issue independent of the nav refactor, but some additional investigation will be necessary to figure out what's really going on here.

The WillPopScope solution is indeed interesting (we should confirm that it indeed works). I had also assumed that the framework just never gets informed when the entire FlutterActivity is destroyed. This will need some additional investigation to figure out what happens when the app gets closed via the back button and why the framework is not purging the widget tree in that case.

goderbauer commented 4 years ago

From a quick look it looks like the framework is informed that somebody pressed the back button. The navigator then tries to pop the route, but since it is the only route on the stack the route refuses to be popped and instead we hand the event back to the native side, which decides to pops the entire activity. However, before the route refuses to pop it informs all register WillPopScopes that a pop may be about to happen. That's why that workaround works. However, I do not recommend that workaround. There's no guarantee that a route actually pops after WillPopScope is informed. Another WillPopScope may as well block the pop!

This is intended behavior because WillPopScope is meant to prevent the pop altogether when you have unsaved data in a route.

Back to the issue at hand: What is the actual use case for this? Why is it important for people that dispose gets called? In general, it will probably be very hard to guarantee that dispose gets called in all situations. For example, if we get killed due to low memory while running in the background it's unlikely that we will have enough time to properly dispose the tree.

mehmetf commented 4 years ago

Why is it important for people that dispose gets called?

Resource leaks. Particularly w.r.t. plugins which are tied to application lifecycle. Some plugins (such as camera) depend on register/dispose calls and if you acquire a resource and you don't dispose it, the application process hangs onto it after the activity quits.

This can be solved by hooking into Android lifecycle directly:

However, cleaning up the same resource N different ways is hard. If you are acquiring something in initState(), you should be able to reasonably expect it to be released at dispose(). We don't expect dispose to run if the OS kills you for whatever reason.

chunhtai commented 4 years ago

The work in add 2 app, the flutter app can live without an activity. If we want to do this, it will be flutter engine on destroy need to notify framework to take another frame cycle to dispose the widget

mehmetf commented 4 years ago

If the engine lives without an activity, isn't it tied to application? At that point a destroy would mean the process is being terminated so I don't think you need to dispose the widget tree.

This is specifically for cases where the activity (not the process) is being terminated.

However, you bring up a good point. The background execution use case makes this interesting and also confusing. Do we really want to dispose all state when activity is terminated? If not, we are basically asking users to consider State lifecycle and App lifecycle separately :-/

Since we expect 99% of the use case of a widget tree to require a vsync, could we make this an opt-out behavior? Dispose the widget tree upon activity termination, unless some special widget (BackgroundWidget?) is used as the root which keeps the tree from being terminated (or at least takes control of what happens).

chunhtai commented 4 years ago

I have a pr to add a new lifecycle state (detached) for flutter app that does not have a activity attached https://github.com/flutter/engine/pull/11913

I think it is inevitable to distinguish between State lifecycle and App lifecycle. Add 2 app need this to properly warm up the engine before the activity is launched.

With this new life cycle enum, one can use a lifecycle observer to release resource if it is in detached state. I am not certain whether we should make it a default behavior to wipe out widget tree when it is in detached state.

The main issue is that we don't know how to distinguish between 1, engine first initialize and no activity has created 2, activity has been destroy after navigator pop

Both of them are considered as detached state, but it seems like what we want here is to do the clean up in 2 only.

mehmetf commented 4 years ago

Thanks @chunhtai.

Just to respond to my own comment above:

However, cleaning up the same resource N different ways is hard. If you are acquiring something in initState(), you should be able to reasonably expect it to be released at dispose(). We don't expect dispose to run if the OS kills you for whatever reason.

We discussed this and it turns out such resources should not be acquired at initState() because widget tree might be built without an activity. We should instead register with the WidgetBindingObserver and acquire them when app state is resumed (and get rid of them when it is paused or detached).

mehmetf commented 4 years ago

After we discussed, I tried to write pseudocode for camera plugin and came up with this:

CameraWidgetState {
  initCamera() {}
  disposeCamera() {}

  initState() {
    // This is why the willpopscope does not quite work.
    // You might get an initState when the app is not resumed.
    if (WidgetsBinding.currentLifecycle == resumed) {
      initCamera();
    }
    WidgetsBindingObserver.addObserver(this);
  }

  dispose() {
    WidgetsBindingObserver.removeObserver(this);
    disposeCamera();
  }

  onLifecycleChange(lifecycle) {
    if (lifecycle == resumed) {
      initCamera();
    } else {
      disposeCamera();
    }
  }
}

This is still pretty confusing. It would be better if the plugin managed the app lifecycle itself and the app only managed the CameraWidget itself via initState/dispose. That way, CameraWidget could remain attached to an inactive Camera which comes back alive onResume.

Hixie commented 4 years ago

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:

(And the behaviour of the above should be, to the user, the same as other cases, such as the app being terminated by the OS due to memory pressure, or the user explicitly killing the app.)

This can be solved by hooking into Android lifecycle directly:

  • Dispose resources when WidgetsBindingObserver reports the AppLifecycle as paused.
  • Code plugins such that they watch activity lifecycle and release resources as needed.

This is the correct answer. (The second bullet can be done from either the framework side or the OEM side.)

On the framework side, you could write a mixin that abstracts this out for you.

mehmetf commented 4 years ago

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

In principle, I agree with you. I think that's what most devs want. However, be aware that, that's not how Android apps work so it would be surprising for that platform. On Android, 1 and 3 are the same (app is paused). 2 is not. Android finishes the activity which then restarts when you tap the app icon with a clean state. If you have a textbox with a value in it, (2) will reset that value if the app does not explicitly save state.

@goderbauer Do we intend to solve the save state problem this way? Because it would look like the state is surviving activity.close operation but it won't survive the process kill by Android OS which is what the state saving issue was about IIRC.

goderbauer commented 4 years ago

Saving/Restoring instance state will happen whenever Android or iOS call us to do it.

On Android, instance state will be saved when Activity.onSaveInstanceState is invoked and restored when Activity.onRestoreInstanceState is invoked by the OS.

audkar commented 4 years ago

@hixie what about cases when application has more active components? E.g. foreground service, activity and user press back button to close that last activity.

Application in that case is kept running. All plugins don't get dismiss callback. Which means that plugins:

New activity is created after user opens app again.

mehmetf commented 4 years ago

@audkar The point is that plugins should not keep doing what they were doing when there is no Flutter activity/engine attached. There's a new plugin API under development that should make this much easier.

audkar commented 4 years ago

The point is that plugins should not keep doing what they were doing

Yes. Of-course. That is my point. And there is problem which this ticket describes. That neither flutter widget, neither plugins gets callbacks when flutterView and flutterEngine is destroyed. So they continue their work. Example is EventChannel onCancel method is not called. And there is zero plugin developers who knows that additional listeners are required to properly handle this specific case.

mehmetf commented 4 years ago

That neither flutter widget, neither plugins gets callbacks when flutterView and flutterEngine is destroyed.

They do though. That's what https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver-class.html is.

We can't fix people not using this by disposing the widgets when activity ends. That creates other problems (such as for background handling). Since the Flutter team agrees on the expected behavior, this issue can be closed.

feel2174 commented 3 years ago

Encountered the same issue too,I handled this way:

//flutter
import 'package:flutter/services.dart';
@override
void initState() {
  super.initState();
  print('initState');
  new MethodChannel("flutter.temp.channel").setMethodCallHandler(platformCallHandler);
}

Future<dynamic> platformCallHandler(MethodCall call) async {
  if (call.method == "destroy"){
    print("destroy");
    dispose();
  }
}
//MainActivity.java
@Override
protected void onStop() {
    super.onStop();
    Log.i("=== MainActivity ===", "onStop: ");
    new MethodChannel(getFlutterView(), "flutter.temp.channel").invokeMethod("destroy", null, null);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    Log.i("=== MainActivity ===", "onDestroy: ");
    new MethodChannel(getFlutterView(), "flutter.temp.channel").invokeMethod("destroy", null, null);
}

It was really helpful for me, I appreciate you!

morgwai commented 3 years ago

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

(And the behaviour of the above should be, to the user, the same as other cases, such as the app being terminated by the OS due to memory pressure, or the user explicitly killing the app.)

This can be solved by hooking into Android lifecycle directly:

  • Dispose resources when WidgetsBindingObserver reports the AppLifecycle as paused.
  • Code plugins such that they watch activity lifecycle and release resources as needed.

This is the correct answer. (The second bullet can be done from either the framework side or the OEM side.)

On the framework side, you could write a mixin that abstracts this out for you.

copy paste of my comment from similar issue: 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 2nd 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 a user navigated away and returned again. However, in the first case, the app is always terminated immediately and all its OS resources released. It may be removed from switcher or not (depending on android version/flavor, but not sure here). If it's not removed and a user navigates back to it, the old state will be 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 1st case (but not necessarily in 2nd and 3rd case), I claim that there should be a reliable way to release state's child resources according to general rule of object 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).

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.