State 在 disposed() 后重新关联,而不是创建一个新的。

huangapple go评论80阅读模式
英文:

State is reassociated after disposed() rather than create a new one

问题

I have app with a simple login system based in named routes and Navigator. When login is successful, the loggin route si pop from stack and the first route (home) is pushed, using Navigator.popAndPushNamed,'/first'). When the user is logged the routes of the app (except from login route) are correctly push and pop from stack to allow a smooth navigation. When the user decides to log out, all routes are removed from stack and the login route is pushed, using Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false). All of that is working fine, but the problem is the user logs again, because the first route (a statefulwidget) is being associated with its previous State which was previously disposed, so the mounted property is false. That's generating that the State properties not being correctly initialized and the error "setState() calls after dispose()" is being shown.
It's an example of the login system based in named routes and Navigator that I'm using in my app.

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

void main() {
  runApp(
    MaterialApp(
      title: 'Named Routes Demo',
      initialRoute: '/',
      routes: {
        '/': (context) => const LoginScreen(),
        '/first': (context) => FirstScreen(),
        '/second': (context) => const SecondScreen(),
      },
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Loggin screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.popAndPushNamed(context, '/first'),
          child: const Text('Launch app'),
        ),
      ),
    );
  }
}

class FirstScreen extends StatefulWidget {
  const FirstScreen({super.key});
  FirstState createState() => FirstState();
}

class FirstState extends State<FirstScreen> {
  int cont;
  Timer? t;
  final String a;

  FirstState() : cont = 0, a='a' {
    debugPrint("Creando estado de First screen");
  }

  @override
  void initState() {
    super.initState();
    debugPrint("Inicializando estado de First Screen");
    cont = 10;
    t = Timer.periodic(Duration(seconds: 1), (timer) => setState(() => cont++));
  }

  @override
  void dispose() {
    debugPrint("Eliminando estado de First Screen");
    cont = 0;
    t?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Screen'),
      ),
      body: Center(child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Contador: $cont"),
            SizedBox(height: 50,),
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(context, '/second');
              },
              child: const Text('Go to second screen'),
            ),
          ]
        )
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
      ),
      body: Center(child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  Navigator.pop(context);
                },
                child: const Text('Go back'),
              ),
              ElevatedButton(
                onPressed: () {
                  Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
                },
                child: const Text('Logout'),
              )
            ]
        )
      )
    );
  }
}

However, the example is not showing the described error, so I'm suspecting the cause could be an uncaught exception during the state dispose.
I'm using Flutter 3.7.12 (Dart 2.19.6). I've not updated to avoid to restructure code to be compatible with Dart 3 (null safety). Another detail is that the error appears sometimes and mainly in Android.

英文:

I have app with a simple login system based in named routes and Navigator. When login is successful, the loggin route si pop from stack and the first route (home) is pushed, using Navigator.popAndPushNamed,'/first'). When the user is logged the routes of the app (except from login route) are correctly push and pop from stack to allow a smooth navigation. When the user decides to log out, all routes are removed from stack and the login route is pushed, using Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false). All of that is working fine, but the problem is the user logs again, because the first route (a statefulwidget) is being associated with its previous State which was previously disposed, so the mounted property is false. That's generating that the State properties not being correctly initialized and the error "setState() calls after dispose()" is being shown.
It's an example of the login system based in named routes and Navigator that I'm using in my app.

import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
title: 'Named Routes Demo',
initialRoute: '/',
routes: {
'/': (context) => const LoginScreen(),
'/first': (context) => FirstScreen(),
'/second': (context) => const SecondScreen(),
},
),
);
}
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Loggin screen'),
),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.popAndPushNamed(context, '/first'),
child: const Text('Launch app'),
),
),
);
}
}
class FirstScreen extends StatefulWidget {
const FirstScreen({super.key});
FirstState createState() => FirstState();
}
class FirstState extends State<FirstScreen> {
int cont;
Timer? t;
final String a;
FirstState() : cont = 0, a='a' {
debugPrint("Creando estado de First screen");
}
@override
void initState() {
super.initState();
debugPrint("Inicializando estado de First Screen");
cont = 10;
t = Timer.periodic(Duration(seconds: 1), (timer) => setState(() => cont++));
}
@override
void dispose() {
debugPrint("Eliminando estado de First Screen");
cont = 0;
t?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First Screen'),
),
body: Center(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Contador: $cont"),
SizedBox(height: 50,),
ElevatedButton(
// Within the `FirstScreen` widget
onPressed: () {
// Navigate to the second screen using a named route.
Navigator.pushNamed(context, '/second');
},
child: const Text('Go to second screen'),
),
]
)
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
),
body: Center(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
// Within the SecondScreen widget
onPressed: () {
// Navigate back to the first screen by popping the current route
// off the stack.
Navigator.pop(context);
},
child: const Text('Go back'),
),
ElevatedButton(
// Within the SecondScreen widget
onPressed: () {
// Navigate back to the first screen by popping the current route
// off the stack.
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
},
child: const Text('Logout'),
)
]
)
)
);
}
}

However, the example is not showing the described error, so I'm suspecting the cause could be an uncaught exception during the state dispose.
I'm using Flutter 3.7.12 (Dart 2.19.6). I've not updated to avoid to restructure code to be compatible with Dart 3 (null safety). Another detail is that the error appears sometimes and mainly in Android.

答案1

得分: 0

你可能在某些异步方法中使用了setState,而且不巧的是,setState调用发生在某些延迟之后,而状态对象已经被释放。

您可以在异步方法中使用mounted标志来检查状态是否仍然挂载,而不是手动取消所有的futures等等在dispose内部。

没有看到您的具体代码,很难准确地说出您的错误发生在何时何地。

关于您的示例,它没有任何问题,Flutter永远不会将小部件与卸载的状态对象关联起来。

英文:

You are probably using setState in some async method and with some unlucky timing your setState call happens after some delay while the state object is already disposed.

You can use the mounted flag inside of your async method to check if the state is still mounted instead of manually cancelling all futures, etc inside of dispose.

Without seeing your concrete code, it's tough to say exactly when and where your error is happening.

Regarding your example, there is nothing wrong with it and flutter will never associate a widget with a dismounted state object.

答案2

得分: 0

问题是我没有关闭 Firebase Cloud Messaging (FCM) 流的订阅。感谢这篇文章,我发现 State 对象在被释放后仍然存在,所以如果你保持一个定时器、流订阅或类似的东西处于活动状态,它将继续执行并生成结果。因此,关闭或取消这种类型的 State 属性非常重要。关于 FCM 流订阅,它们应该按照以下方式处理:

class _AppState extends State<_App> {

    @override
    void initState() {
        ...
        FirebaseMessaging.instance.getInitialMessage().then(handleInteraction);
        _suscrStreamFCMAppBackgnd = FirebaseMessaging.onMessageOpenedApp.listen(handleInteraction);
        FirebaseMessaging.onBackgroundMessage(procesarNotificacion);
        _suscrStreamFCMAppForegnd = FirebaseMessaging.onMessage.listen(_procesarNotificacionAppPrimerPlano);
        for (final topic in TOPICS_FIREBASE) {
          FirebaseMessaging.instance.subscribeToTopic(topic);
        }
        ...
    }
    
    @override
    void dispose() {
        ...
        _suscrStreamFCMAppBackgnd?.cancel();
        _suscrStreamFCMAppForegnd?.cancel();
        ...
    }
}

因此,旧的 State(卸载的)没有重新关联到 StatefulWidget 对象,但由于流订阅的存在,它仍然保持活动状态并在后台执行代码。

英文:

The problem was that I was not closing the subscriptions to the Firebase Cloud Messaging (FCM) streams.
Thanks to this post I could discover that State objects remain alive after they are disposed, so if you leave active a timer, stream subscription or sth like that, it will continue executing and generating results. So it is very important to close or cancel that kind of State properties.
With respect to FCM stream subscriptions, they should be handled as following:

class _AppState extends State&lt;_App&gt; {
@override
void initState() {
...
FirebaseMessaging.instance.getInitialMessage().then(handleInteraction);
_suscrStreamFCMAppBackgnd = FirebaseMessaging.onMessageOpenedApp.listen(handleInteraction);
FirebaseMessaging.onBackgroundMessage(procesarNotificacion);
_suscrStreamFCMAppForegnd = FirebaseMessaging.onMessage.listen(_procesarNotificacionAppPrimerPlano);
for (final topic in TOPICS_FIREBASE) {
FirebaseMessaging.instance.subscribeToTopic(topic);
}
...
}
@override
void dispose() {
...
_suscrStreamFCMAppBackgnd?.cancel();
_suscrStreamFCMAppForegnd?.cancel();
...
}
}

So, the old State (unmounted) hadn't been reassociated to the StatefulWidget object, but it remained alive and executing code in the background beacuse of the stream subscriptions.

huangapple
  • 本文由 发表于 2023年7月4日 22:58:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76613864.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定