Imagepicker is rebuilt when form does, losing the selected image

186

Solution 1

When the drawer or soft keyboard opened the state of the screen change and sometimes the build method reload automatically, please check this link for more information.

The build method is designed in such a way that it should be pure/without side effects. This is because many external factors can trigger a new widget build, such as:

Route pop/push Screen resize, usually due to keyboard appearance or orientation change Parent widget recreated its child An InheritedWidget the widget depends on (Class.of(context) pattern) change This means that the build method should not trigger an http call or modify any state.

How is this related to the question?

The problem you are facing is that your build method has side-effects/is not pure, making extraneous build call troublesome.

Instead of preventing build calls, you should make your build method pure, so that it can be called anytime without impact.

In the case of your example, you'd transform your widget into a StatefulWidget then extract that HTTP call to the initState of your State:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Future<int> future;

  @override
  void initState() {
    future = Future.value(42);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: future,
      builder: (context, snapshot) {
        // create some layout here
      },
    );
  }
}

I know this already. I came here because I really want to optimize rebuilds

It is also possible to make a widget capable of rebuilding without forcing its children to build too.

When the instance of a widget stays the same; Flutter purposefully won't rebuild children. It implies that you can cache parts of your widget tree to prevent unnecessary rebuilds.

The easiest way is to use dart const constructors:

@override
Widget build(BuildContext context) {
  return const DecoratedBox(
    decoration: BoxDecoration(),
    child: Text("Hello World"),
  );
}

Thanks to that const keyword, the instance of DecoratedBox will stay the same even if the build were called hundreds of times.

But you can achieve the same result manually:

@override
Widget build(BuildContext context) {
  final subtree = MyWidget(
    child: Text("Hello World")
  );

  return StreamBuilder<String>(
    stream: stream,
    initialData: "Foo",
    builder: (context, snapshot) {
      return Column(
        children: <Widget>[
          Text(snapshot.data),
          subtree,
        ],
      );
    },
  );
}

In this example when StreamBuilder is notified of new values, the subtree won't rebuild even if the StreamBuilder/Column does. It happens because, thanks to the closure, the instance of MyWidget didn't change.

This pattern is used a lot in animations. Typical uses are AnimatedBuilder and all transitions such as AlignTransition.

You could also store subtree into a field of your class, although less recommended as it breaks the hot-reload feature.

Solution 2

I started from the advices from abbas jafary and tried to restructure the screen to not rebuild the imagepicker automatically. I wasn't able to initialize it in a variable without some side changes, since i pass a key to the imagepicker itself. Here's the final code:

class ProductAddScreen extends StatelessWidget {
  static final GlobalKey<ProductAddUpdateFormState> _keyForm = GlobalKey();
  static final GlobalKey<ImageInputProductState> _keyImage = GlobalKey();
  final imageInput = ImageInputProduct(key: _keyImage);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          children: [
            SizedBox(
              height: MediaQuery.of(context).padding.top,
            ),
            TitleHeadline(
              title: 'Add',
              backBtn: true,
              trailingBtn: Icons.info,
              trailingBtnAction: () =>
                  Navigator.of(context, rootNavigator: true).push(
                MaterialPageRoute(builder: (context) => InfoScreen()),
              ),
            ),
            const SizedBox(height: 8),
            imageInput,
            ProductAddUpdateForm(key: _keyForm),
            const SizedBox(height: 16),
            ButtonWide(
              action: () => _keyForm.currentState.submit(
                  screenContext: context,
                  newImage: _keyImage.currentState.storedImage),
              text: 'Confirm',
            ),
          ],
        ),
      ),
    );
  }
}

I'm not sure it's a best practice, but it works and the imagepicker keeps its state no matter what.

Share:
186
m.i.n.a.r.
Author by

m.i.n.a.r.

Updated on December 28, 2022

Comments

  • m.i.n.a.r.
    m.i.n.a.r. over 1 year

    I'm using a stateless screen which contains two stateful widgets, an imagepicker and a form with many fields. When i open the keyboard, if i selected an image previously, it disappears and the entire imagepicker widget is reinitialized.

    This means that the only way to submit an image is select it when the keyboard is closed and never reopen it. I already tried setting a key and with other solutions i found here, but nothing works. I can't fully understand this behavior, and of course i need the image to stay there even if i open and close the keyboard.

    A fast solution could be simply move the imagepicker in the form itself, but i'd prefer to keep them in different widgets. I really need to understand what i'm missing.

    Main page:

    class ProductAddScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final GlobalKey<ProductAddUpdateFormState> _keyForm = GlobalKey();
        final GlobalKey<ImageInputProductState> _keyImage = GlobalKey();
        return Scaffold(
          body: SingleChildScrollView(
            child: Column(
              children: [
                SizedBox(
                  height: MediaQuery.of(context).padding.top,
                ),
                TitleHeadline(
                  title: 'Add',
                  backBtn: true,
                  trailingBtn: Icons.info,
                  trailingBtnAction: () =>
                      Navigator.of(context, rootNavigator: true).push(
                    MaterialPageRoute(builder: (context) => InfoScreen()),
                  ),
                ),
                const SizedBox(height: 8),
                ImageInputProduct(key: _keyImage),
                ProductAddUpdateForm(key: _keyForm),
                const SizedBox(height: 16),
                ButtonWide(
                  action: () => _keyForm.currentState.submit(
                      screenContext: context,
                      newImage: _keyImage.currentState.storedImage),
                  text: 'Confirm',
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Imagepicker:

    class ImageInputProduct extends StatefulWidget {
      final String preImage;
    
      ImageInputProduct({Key key, this.preImage = ''}) : super(key: key);
    
      @override
      ImageInputProductState createState() => ImageInputProductState();
    }
    
    class ImageInputProductState extends State<ImageInputProduct> {
      File _storedImage;
    
      // Get the selected file
      File get storedImage {
        return _storedImage;
      }
    
      // Take an image from camera
      Future<void> _takePicture() async {
        final picker = ImagePicker();
        final imageFile = await picker.getImage(
          source: ImageSource.camera,
          maxHeight: 1280,
          maxWidth: 1280, 
        );
        setState(() {
          _storedImage = File(imageFile.path);
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Container(
              height: 130,
              width: 200,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(8),
                border: Border.all(
                  width: 1,
                  color: Colors.black12,
                ),
              ),
              child: _storedImage == null
                  ? widget.preImage.isEmpty
                      ? Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(
                              Icons.image,
                              color:
                                  Theme.of(context).primaryColor.withOpacity(0.4),
                              size: 48,
                            ),
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                horizontal: 16,
                                vertical: 4,
                              ),
                              child: Text(
                                'No image selected',
                                textAlign: TextAlign.center,
                                style: Theme.of(context).textTheme.bodyText2,
                              ),
                            )
                          ],
                        )
                      : ClipRRect(
                          borderRadius: BorderRadius.only(
                            bottomLeft: Radius.circular(8),
                            topLeft: Radius.circular(8),
                            bottomRight: Radius.circular(8),
                            topRight: Radius.circular(8),
                          ),
                          child: Image.network(
                            widget.preImage,
                            fit: BoxFit.cover,
                            width: double.infinity,
                          ),
                        )
                  : ClipRRect(
                      borderRadius: BorderRadius.only(
                        bottomLeft: Radius.circular(8),
                        topLeft: Radius.circular(8),
                        bottomRight: Radius.circular(8),
                        topRight: Radius.circular(8),
                      ),
                      child: Image.file(
                        _storedImage,
                        fit: BoxFit.cover,
                        width: double.infinity,
                      ),
                    ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
              child: ButtonWideOutlined(
                action: _takePicture,
                text: 'Select image',
              ),
            ),
          ],
        );
      }
    }
    

    The form is just a standard form, and this question is already too long. I'd really appreciate any suggestion. The only thing i know is that initState is called in the imagepicker every time i open the keyboard (and therefore the form state changes).

  • m.i.n.a.r.
    m.i.n.a.r. about 3 years
    The "final" approach works, even if i had to do some tricks to make it work. I'll post a detailed answer, but your hints did the trick and this should be the accepted one