Flutter Navigation and the requirement for dispose() in the mean time

11,707

Solution 1

Instead of setState(() {...}), try if (mounted) { setState(() {...}) } for code that may be running after the user navigates away.

Solution 2

It turned out, it was not the dispose()-method in view1 that caused the image_picker from failure. It was the animation that was still running when image_picker was called.

I have finally a working solution by doing the following:

Inside view1 (where image_picker is originally called), add one line of code:

onPressed: () async {
  controller.dispose();  // !!!!!!!!! Adding this line helped !!!!!!!!!!!
  await navigateToImagePicker(context);
},

Also, delete (or comment out) the entire dispose()-method :

// @override
// void dispose() {
//   if (_debounce?.isActive ?? false) {
//     _debounce.cancel(); // if _debounce is active cancel it...
//   }
//   _debounce = Timer(const Duration(milliseconds: 200), () {});
//   controller.dispose();
//   _debounce.cancel();
//   super.dispose();
// }
Share:
11,707
iKK
Author by

iKK

Updated on December 09, 2022

Comments

  • iKK
    iKK over 1 year

    Trying to use image_picker in Flutter, I have the following issue:

    When navigation is supposed to pop back to Widget Nr1, I can no longer call setState() inside Widget Nr1. This is due to the fact that the dispose()method was called once the Navigation.push from Widget-Nr1 to Widget-Nr2 happened.

    It turns out that I absolutely need to call this dispose()method in order for the image_picker plugin to work correctly. (if I don't then the error ...was disposed with an active Ticker... happens, probably due to the fact that the image_picker plugin does something under the hood that desperately needs dispose() beforehand.

    Anyway, I feel like the snake bites its tail.

    As a summary I do the following (also see code below):

    • inside Widget Nr1: Pressing a FloatingAction-Button, pushes the Navigator to a Widget Nr2
    • both Widgets (Nr1 and Nr2) are Stateful Widgets
    • they both have a dispose-method (needed otherwise image_picker does not work)
    • Widget-Nr2 calls the image_picker plugin (letting the user take a photo with the camera and asking the user for some String-text describing the image)
    • the result (i.e. imageFile and some String-text) needs to be given back to Widget-Nr1 (using Navigation.pop)
    • the Widget-Nr1 actually does get this data (i.e. image plus some String-text)
    • but: it cannot call setState() anymore after the Navigation.pop most likely due to the fact that both Widgets had already called their dispose() method

    I get the error inside Widget-Nr1:

    Dart Error: Unhandled exception:
    setState() called after dispose()
    

    What can I do in order to make this work ?

    How can I use the result-data of the image_picker (that requires dispose() in Widget-1) as a Navigation.pop result again in Widget-1 and this in a way where setState() is still possible after all the Navigation ??

    Or is there another approach to go with ?

    Here is my code:

    StatefulWidget Nr1 (excerpt of it):

        child: FloatingActionButton(
          onPressed: () async {
            _imagePickerResult = await navigateToImagePicker(context);
            setState(() async {
                this.itemBins.add(ItemBin(
                    _imagePickerResult.locationName,
                    _imagePickerResult.locationImage));
            });
          },
          child: Icon(Icons.add),
        ),
    
        // ...
    
        Future<ImagePickerResult> navigateToImagePicker(BuildContext context) async {
          return await Navigator.push(
            context, MaterialPageRoute(builder: (context) => MyImagePickerView())
          );
        }
    
        // ...
    
        class ImagePickerResult {
          String locationName;
          Image locationImage;
    
          ImagePickerResult({this.locationName, this.locationImage});
        }
    

    StatefulWidget Nr2:

        import 'package:flutter/material.dart';
        import 'dart:io';
        import 'package:image_picker/image_picker.dart';
        import './../../models/image_picker_location.dart';
    
        class MyImagePickerView extends StatefulWidget {
          _MyImagePickerViewState createState() => _MyImagePickerViewState();
        }
    
        class _MyImagePickerViewState extends State<MyImagePickerView> {
          TextEditingController _myController = TextEditingController();
          File _imageFile;
          bool _pickImage = true;
    
          @override
          Widget build(BuildContext context) {
            if (_pickImage) {
              return FutureBuilder<File>(
                future: ImagePicker.pickImage(source: ImageSource.camera),
                builder: (BuildContext context, AsyncSnapshot<File> snapshot) {
                  if (snapshot.hasData) {
                    _pickImage = false;
                    _imageFile = snapshot.data;
                    return _showImage(snapshot.data);
                  } else {
                    return Scaffold(
                      body: Center(
                        child: Text('no image picker availalbe'),
                      ),
                    );
                  }
                },
              );
            } else {
              return _showImage(_imageFile);
            }
          }
    
          Widget _showImage(File imgFile) {
            return Scaffold(
              body: Stack(
                alignment: AlignmentDirectional.topStart,
                children: <Widget>[
                  Positioned(
                    left: 0.0,
                    bottom: 0.0,
                    width: MediaQuery.of(context).size.width,
                    height: MediaQuery.of(context).size.height,
                    child: Center(
                      child: imgFile == null
                          ? Text('No image selected.')
                          : Image.file(imgFile),
                    ),
                  ),
                  Positioned(
                    left: 16.0,
                    bottom: 70.0,
                    width: MediaQuery.of(context).size.width - 32.0,
                    height: 50.0,
                    child: Container(
                      color: Colors.grey[100],
                      child: TextField(
                        autofocus: false,
                        keyboardType: TextInputType.text,
                        autocorrect: false,
                        style: TextStyle(
                            color: Colors.black,
                            fontSize: 22.0,
                            fontWeight: FontWeight.w600),
                        decoration: InputDecoration(
                          hintStyle: TextStyle(
                              color: Colors.black38,
                              fontSize: 22.0,
                              fontWeight: FontWeight.normal),
                          hintText: "depart From :",
                          contentPadding: const EdgeInsets.fromLTRB(6.0, 13.0, 0, 12.0),
                          enabledBorder: UnderlineInputBorder(
                            borderSide: BorderSide(color: Colors.red, width: 2.0),
                          ),
                        ),
                        maxLines: 1,
                        textAlign: TextAlign.left,
                        controller: _myController,
                        onEditingComplete: () {
                          FocusScope.of(context)
                              .requestFocus(FocusNode()); // dismiss keyboard
                          Navigator.pop(
                            context,
                            ImagePickerResult(
                              locationName: _myController.text,
                              locationImage: Image.file(imgFile),
                            ),
                          );
                        },
                      ),
                    ),
                  ),
                ],
              ),
            );
          }
        }
    

    dispose-method of Widget Nr1:

      @override
      void dispose() {
        if (_debounce?.isActive ?? false) {
          _debounce.cancel(); // if _debounce is active cancel it...
        }
        _debounce = Timer(const Duration(milliseconds: 200), () {
          // security wait due to the fact that there are animations still running during setState()
        });
        // dispose AnimationController
        controller.dispose();
        _debounce.cancel();
        super.dispose();
      }
    

    dispose method of Widget-Nr2:

       @override
       void dispose() {
         _myController.dispose();
         super.dispose();
       }
    

    Here is the error-message if I don't make view1 do the dispose() before image_picker starts... (please note that there is an animation running at the moment the user wants to start image_picker and therefore the dispose() makes an artificial "wait" of 200ms before the segue to the image_picker takes place)....

    flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
    flutter: The following assertion was thrown while finalizing the widget tree:
    flutter: _HistoryViewState#a8eac(ticker active but muted) was disposed with an active Ticker.
    flutter: _HistoryViewState created a Ticker via its SingleTickerProviderStateMixin, but at the time dispose()
    flutter: was called on the mixin, that Ticker was still active. The Ticker must be disposed before calling
    flutter: super.dispose(). Tickers used by AnimationControllers should be disposed by calling dispose() on the
    flutter: AnimationController itself. Otherwise, the ticker will leak.
    flutter: The offending ticker was: Ticker(created by _HistoryViewState#a8eac(lifecycle state: created))
    flutter: The stack trace when the Ticker was actually created was:
    flutter: #0      new Ticker.<anonymous closure> 
    package:flutter/…/scheduler/ticker.dart:64
    flutter: #1      new Ticker 
    package:flutter/…/scheduler/ticker.dart:66
    flutter: #2      __HistoryViewState&State&SingleTickerProviderStateMixin.createTicker 
    package:flutter/…/widgets/ticker_provider.dart:93
    flutter: #3      new AnimationController 
    
  • iKK
    iKK about 5 years
    Thank you. I did not know the keyword State.mounted. It eliminated the crash after the Navigation.pop - however now the data given back to Widget-Nr1 does never get used (and shown) but just gets lost in the onPressed() async {...} callback. How can I keep the result-data (passed with the Navigation.pop) inside Widget-1 when State.mounted == false. Do I need to call initState() again after the Navigation.pop or is there another approach ? Is there some stateManagement needed (...maybe with Inherited Widget) or do you have another idea ? Thanks for any thought.
  • Gazihan Alankus
    Gazihan Alankus about 5 years
    Sorry, your question is too wordy and hard to understand with a glance. What do you want to do with that result-data? You can't affect the UI as your screen is not there anymore. Do you want to save it somewhere? Maybe do that outside of setState if mounted is false?
  • iKK
    iKK about 5 years
    I simply want the result-data brought back by the navigate.pop to be shown in the first view. But this mecano does not work for the image_picker for some reason, since dispose() needed to be called so that image_picker works at all. You are absolutely right that the first view is gone after having to call dispose(). After doing so, I guess, I need some sort of persistence or an inherited widget if I ever want to show the image and text that was picked in the second screen back in the first one. Normally, this navigate.push followed by an navigate.pop-with-result-data works fine - but not here.
  • iKK
    iKK about 5 years
    I will try to persist the image picked in second view and retrieve it back in the first view from the persistence (and therefore get rid of the navigate.pop result-data followed by setState() since this does no longer work...). Thank you for your help.
  • Gazihan Alankus
    Gazihan Alankus about 5 years
    You could try to pop after the async result comes back. Then you can return whatever you want. If your problem is solved, consider accepting the answer here. Good luck!
  • iKK
    iKK about 5 years
    That is what I do already (i.e. pop after result came back). I do the pop in the second view only if I have a result form the image_picker. Again, due to the requirement of dispose() of the first view (so that image_picker works), I can never pop-return from the second view to the first view and thereafter call setState() (since dispose() had already eliminated the first view). I need to re-build the first view from scratch after any navigation. It is really quite tricky. If you ever use the image_picker plugin, you will know what I mean ;)
  • Gazihan Alankus
    Gazihan Alankus about 5 years
    I have no problems doing push() in w1, then pop() in w2, and then setState() in the same async function in w1. Which tells me that your theory about what's going on is not accurate. I think that dispose is messing something up. Where did you learn that you have to do a debounce like that? Also minor point: don't give async functions to setState(): github.com/flutter/flutter/issues/3951#issuecomment-22163870‌​3
  • iKK
    iKK about 5 years
    Thank you very much for your explanations. Normally, I have no problem to do push() in w1 and then pop() in w2 (..and using the result from pop() back in w1 - I have done it many times). However, this @#!%%#^-image_picker makes my life hard :). I don't know why image_picker in w2 requires dispose() in w1. Maybe you find out why image_picker requires dispose() in w1 for it to work in w2 after push(). Did you try image_picker in w2 and sending results (i.e. its picked image and some text) back to w1. That is the mess I am not able to solve. Anyway, I gladly give you an upvote for trying to help.
  • Gazihan Alankus
    Gazihan Alankus about 5 years
    Can you show me what gave you the idea "I don't know why image_picker in w2 requires dispose() in w1"?
  • iKK
    iKK about 5 years
    See error-message that I get if I don't (at very bottom of my initial question above....). It said State(ticker active but muted) was disposed with an active Ticker - do you know what that means ?? Could the animation be the problem ?? (I realised that there is a 200ms delay inside the dispose() and if this is missing (i.e. when dispose() is commented) - this might actually be the root cause of it all... What do you think ?
  • iKK
    iKK about 5 years
    Oh gosh - I have found a solution (thanks to your last question): It was enough to dispose the animation-Controller (that was the root cause of the image_picker failure). I took the one line of code out of the w1-dispose() and added the line right before the image_picker call. And with it, I commented out the dispose()-method in w1. Thank you very much for your great support and help with this !!!
  • Gazihan Alankus
    Gazihan Alankus about 5 years
    Great! I'll come back to here when I need to use image_picker.