Search code examples
androidflutterflutter-getxflutter-secure-storage

FlutterError (setState() called after dispose(): _welcomeScreen_1State#a1185(lifecycle state: defunct, not mounted) Error


I have an application. I added an introductory screen to this application that only appears at the entrance. This screen will appear when the user first opens the application. The next time they open, the promotional screen will not open again.

I've done it with Secure Storage and GetX whether the user has passed the promotion or not.

main.dart:

import 'package:flutter/material.dart';
import 'package:teen_browser/pages/home.dart';
import 'package:teen_browser/welcome_screens/welcomeMain.dart';
import 'package:get/get.dart';
import 'features/controllers/pagination-controller/pagination_controller.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

var skipStatus;
void main() {
  runApp(mainApp());
}

class mainApp extends StatefulWidget {
  mainApp({Key? key}) : super(key: key);
  @override
  State<mainApp> createState() => _mainAppState();
}

class _mainAppState extends State<mainApp> {
  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(fontFamily: 'Montserrat Regular'),
      home: passedTheWelcomeScreen(),
    );
  }
}

class passedTheWelcomeScreen extends StatefulWidget {
  passedTheWelcomeScreen({Key? key}) : super(key: key);
  @override
  State<passedTheWelcomeScreen> createState() => _passedTheWelcomeScreenState();
}

class _passedTheWelcomeScreenState extends State<passedTheWelcomeScreen> {
  final PaginationController _paginationController = Get.put(PaginationController());
  @override
  Widget build(BuildContext context) {
    return GetX<PaginationController>(
      init: _paginationController,
      initState: (initController) {
        initController.controller!.CheckSkipStatus();
      },
      builder: (controller) {
        if (controller.isSkipped.value == null) {
          return CircularProgressIndicator();
        } else if (controller.isSkipped.value == true){
          return HomeApp();
        } else {
          return welcomeMain();
        }
      },
    );
  }
}

pagination_controller.dart:

    import 'package:get/get.dart';
    import 'package:teen_browser/functions/secure_storage.dart';
    
    class PaginationController extends GetxController {
      RxnBool isSkipped = RxnBool(false);
      void CheckSkipStatus() async {
        final resp = await UserSecureStorage.isSkip();
        isSkipped.value = resp;
      }
    }

secure_storage.dart:

import 'dart:developer';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class UserSecureStorage {
  static const _storage = FlutterSecureStorage();
  static Future setField(String key, value) async {
    await _storage.write(key: key, value: value);
  }

  static Future<String?> getField(key) async {
    return await _storage.read(key: key);
  }

  static Future deleteField(String key) async {
    return await _storage.delete(key: key);
  }

  static Future deleteAll() async {
    return await _storage.deleteAll();
  }

  static Future<bool> isSkip() async {
    inspect(await getField("isSkipped"));
    var value = await getField('isSkipped');
    if (value != null) return true;
    inspect(value);
    return false;
  }
}

On the last promotion page, I stated that the user passed the definition as follows:

  await UserSecureStorage.setField("isSkipped", "true");
  controller.isSkipped.value = true;

If the user has not passed the promotion before, there is no problem. All promotional screens are coming and error etc. does not give. But after passing all the demo screens, that is, when the value of isSkipped is true, it gives an error.

The error I got:

FlutterError (setState() called after dispose(): _welcomeScreen_1State#7c0ca(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().)

I think the error is caused by the initState on the first demo page, which also shows the first demo page, welcomeMain1.dart, on the console.

welcomeScreen1.dart initState codes:

  void initState() {
    super.initState();
    print(_clickGoButtonErrorMessages[Random().nextInt(_clickGoButtonErrorMessages.length)]);
    if (controller.isSkipped.value == false) {
      Timer(const Duration(milliseconds: 3200), () {
        setState(() {
          _helloText = 0;
        });
      });
      Timer(const Duration(milliseconds: 3300), () {
        AssetsAudioPlayer.newPlayer().open(
          Audio("assets/audios/StopBroDontPassSound.mp3"),
          autoStart: true,
          showNotification: true,
        );
      });
      Timer(const Duration(milliseconds: 3400), () {
        setState(() {
          _helloTextState = true;
        });
      });
      Timer(const Duration(milliseconds: 3500), () {
        setState(() {
          _helloTextState = false;
        });
      });
      Timer(const Duration(milliseconds: 3900), () {
        setState(() {
          _helloText = 1;
        });
      });
      Timer(const Duration(milliseconds: 4200), () {
        setState(() {
          _helloTextState = true;
        });
      });
      Timer(const Duration(milliseconds: 5700), () {
        setState(() {
          _contentText = true;
        });
      });
      Timer(const Duration(milliseconds: 7000), () {
        setState(() {
          _letsGoButton = true;
        });
      });
      Timer(const Duration(milliseconds: 7300), () {
        setState(() {
          _skipButton = true;
        });
      });
      Timer(const Duration(milliseconds: 9000), () {
        setState(() {
          _clickGoButton = true;
          _clickSkipButton = true;
        });
      });
    }
    else {
      inspect("isSkipped true");
    }
  }

Once the user introduction has passed, it will always be redirected to the homepage, ie HomeApp. If the value of isSkipped is false, it means that the user first opened the application. This time, it directs you to the promotional screens.

My guess is that the application is trying to run the initState codes in welcomeMain1.dart while redirecting to HomeApp() and throws an error when it doesn't.

I hope I was able to explain my problem.When the user promotion passes, the value of isSkipped is true, ie "user promotion passed". I tested it by printing it to the console. How can I solve this problem?

Thanks for help.


Solution

  • There could be a race condition so that isSkipped is false long enough for the timers to start, then it switches to true and the widget is rebuilt but the timers are still running.

    You could add this before setState in your timer callbacks:

          Timer(const Duration(milliseconds: 3200), () {
            if (!mounted) {
              return;
            }
    
            setState(() {
              _helloText = 0;
            });
          });
    

    You could also save all timers as individual properties and cancel them in the widget's dispose method:

    class MyWidget extends StatelessWidget
    {
      late Timer _timer1;
      late Timer _timer2;
    
      void initState() {
        super.initState();
        if (controller.isSkipped.value == false) {
          _timer1 = Timer(const Duration(milliseconds: 3200), () {
            setState(() {
              _helloText = 0;
            });
          });
          _timer2 = ...
        }
        // ...
      }
    
      @override
      void dispose() {
        _timer2.cancel();
        _timer1.cancel();
        super.dispose();
      }
    
    
    }
    

    (The above is dry-coded, sorry for any errors. Storing the timers in an array is left as an exercise to the reader.)