Flutter Image Widget won't update on change state
Asked Answered
E

5

6

I am creating an Image Editor application using sliders, something like Instagram, and I am using library image/image.dart. The problem is that once you move the slider it updates the image but just that time, if you move it again, it won't update.

I have set everything as expected, setState() functions as flutter asks, but I don't know why it won't update again.

import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';

import 'package:image/image.dart' as br;
import 'package:path_provider/path_provider.dart';
import 'package:image_picker/image_picker.dart';

class ImageManager extends StatefulWidget {

  @override
  State<StatefulWidget> createState() {
    return _ImageManagerState();
  }
}

class _ImageManagerState extends State<ImageManager> {
  File imageFile;
  br.Image image;
  Image _imageWidget;

  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();

    return directory.path;
  }

  // get tmp file
  Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/tmp.jpg');
  }

  // pick image on button click
  Future<void> _pickImage(ImageSource source) async{
    File selectedFile = await ImagePicker.pickImage(source: source);
    br.Image selectedImage;
    if (selectedFile != null){
      selectedImage = br.decodeImage(selectedFile.readAsBytesSync());
      br.grayscale(selectedImage);

      selectedFile.writeAsBytesSync(br.encodeJpg(selectedImage));
    }

    setState((){
      image = selectedImage;
      imageFile = selectedFile;
      _imageWidget = Image.file(imageFile);
    });
  }

  // MAIN PROBLEM, UPDATING THE CONTRAST WILL ONLY DO IT ONCE
  Future<void> updateContrast(value) async{
    File contrastFile = imageFile;
    br.Image contrast = br.decodeImage(contrastFile.readAsBytesSync());
    contrast = br.adjustColor(contrast, contrast: value);
    contrastFile.writeAsBytesSync(br.encodeJpg(contrast));

    // Save the thumbnail as a jpg.
    File path = await _localFile;
    path.writeAsBytesSync(br.encodeJpg(contrast));
    setState(() {
      image = contrast;
      imageFile = contrastFile;
      if(path != null){
        _imageWidget = Image.file(path);
        print(value);
      }

    });
  }

  // 
  Widget _buildImage(BuildContext context){
    return Column(
      children: [
        Container(
          width: MediaQuery.of(context).size.width,
          height: MediaQuery.of(context).size.width,
          child: _imageWidget,
        ),
        Column(
          children: [
            Container(
              padding: const EdgeInsets.only(left: 8, right: 8),
              child: Column(
                children: <Widget>[
                  // contrast
                  Text("Contraste"),
                  Padding(
                    padding: const EdgeInsets.only(bottom: 4.0, top: 0.0),
                    child: Container(
                      child: Slider(
                        min: 0.0,
                        max: 1.0,
                        divisions: 100,
                        value: _contrast,
                        activeColor: Colors.blue[500],
                        inactiveColor: Colors.blue[50],
                        label: "${(_contrast *100).round()}",
                        onChanged: (value) async{
                          changeContrast(value);
                        },
                        onChangeEnd: (value) async{
                          updateContrast(value);
                        },
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ]
    );
  }


I expect the image to update every time the slider is changed.

Ellett answered 11/11, 2019 at 0:14 Comment(4)
where is the method changeContrast you are using ?Iquitos
Oh, I forgot to upload but it is just a function that set the variable _contrast to the current value on the slider.Ellett
and do you call setState there?Iquitos
Yes, sorry for not adding that part, it goes like this: void changeContrast(contrast){ setState(() => _contrast = contrast); }Ellett
E
3

I solved the issue, which is pretty weird to me, but if someone can explain it, I would appreciate it.

Future<void> adjustImage() async{
  File toAdjustFile = imageFile;
  br.Image toAdjust = br.decodeImage(toAdjustFile.readAsBytesSync());
  toAdjust = br.adjustColor(toAdjust, contrast: _contrast, brightness: _brightness, exposure: _exposure);
  setState(() {
    _imageWidget = Image.memory(br.encodeJpg(toAdjust));
  });
}

I refactored my function and set the widget to another constructor, Image.memory().

Ellett answered 11/11, 2019 at 1:49 Comment(4)
I have a similar situation... it's just an image selection (gallery/camera) with an Image.file to show the selected picture. When the images has changed, the Image.file don't update and always shows the same result. Your solution with Image.memory works for me. I believe that the Image.file uses some cache and don't refresh the images.Emf
Yeah, I actually had to do the same with the selection of the image, since it will not update correctly, had to use the Image.memory too, hope it worked for you.Ellett
Issue is still open github.com/flutter/flutter/issues/17419Capua
So, Image.file() uses ResizeImage.resizeIfNeeded() which in turn uses FileImage() which is the provider for the image data. The FileImage.hashCode() method uses int get hashCode => hashValues(file?.path, scale); As such, when chaning the path of an image file I believe its at this point that it fails. (I'm assuming the hasCode is used to detect if the file is dirty and needs reloading). Conversely, Image.memory uses MemoryImage whose hashcode is based on the bytes of the image. As such it will detect a change.Future
G
11

imageCache.clear() will do the job.

I was also not able to reload the image to save locally on the screen. From debugging, I observed the old image is in fact deleted, and new image is copied there, but nothing was changing on the screen. Following is the code you need.

So, in the body of Scaffold, I have made a FutureBuilder that calls another function reload(), which then loads the file and return the image.

FutureBuilder(
future: reload(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
    if(snapshot.connectionState == ConnectionState.done){
        return snapshot.data;
    }  
    else{
        return CircularProgressIndicator();
    }
},
),

Here is the reload() function:

reload() async {
String path = "path of your image";
File profileImage = File("$path/name.jpg");

if(profileImage.existsSync() == false){
  return Text("File Not Found");
}
else{
  imageCache.clear();
  return Image.file(profileImage);
}
}

I checked Flutter's github, and jamesncl has already suggested this.

Groan answered 13/6, 2020 at 18:53 Comment(0)
E
3

I solved the issue, which is pretty weird to me, but if someone can explain it, I would appreciate it.

Future<void> adjustImage() async{
  File toAdjustFile = imageFile;
  br.Image toAdjust = br.decodeImage(toAdjustFile.readAsBytesSync());
  toAdjust = br.adjustColor(toAdjust, contrast: _contrast, brightness: _brightness, exposure: _exposure);
  setState(() {
    _imageWidget = Image.memory(br.encodeJpg(toAdjust));
  });
}

I refactored my function and set the widget to another constructor, Image.memory().

Ellett answered 11/11, 2019 at 1:49 Comment(4)
I have a similar situation... it's just an image selection (gallery/camera) with an Image.file to show the selected picture. When the images has changed, the Image.file don't update and always shows the same result. Your solution with Image.memory works for me. I believe that the Image.file uses some cache and don't refresh the images.Emf
Yeah, I actually had to do the same with the selection of the image, since it will not update correctly, had to use the Image.memory too, hope it worked for you.Ellett
Issue is still open github.com/flutter/flutter/issues/17419Capua
So, Image.file() uses ResizeImage.resizeIfNeeded() which in turn uses FileImage() which is the provider for the image data. The FileImage.hashCode() method uses int get hashCode => hashValues(file?.path, scale); As such, when chaning the path of an image file I believe its at this point that it fails. (I'm assuming the hasCode is used to detect if the file is dirty and needs reloading). Conversely, Image.memory uses MemoryImage whose hashcode is based on the bytes of the image. As such it will detect a change.Future
S
2

Use Image.memory instaead Image.file, A line from my code

Image.memory(
          File(widget.imagePath).readAsBytesSync(),),
Saccharo answered 26/11, 2022 at 6:34 Comment(1)
Thanks mate 👌This worked fine for me.Confectionery
D
0

For me, I just added UniqueKey to ListView.

            child: ListView(
              key: UniqueKey(),
Dinger answered 23/3 at 2:4 Comment(0)
P
0

As of April 2024, i get in same problem. In my case this appears when i have many images named xxxx_01.jpg, xxxx_02.jpg, xxxx_03.jpg, etc.

if I delete xxxx_02.jpg and rename xxxx_03.jpg to xxxx_02.jpg then old xxxx_02.jpg is displayed instead of new xxx_02.jpg. Clearly a cache problem.

For me the solution is to issue this :

imageCache.clear();
imageCache.clearLiveImages();

Explanation for first line is:

Evicts all pending and keepAlive entries from the cache.

This is useful if, for instance, the root asset bundle has been updated and therefore new images must be obtained.

Images which have not finished loading yet will not be removed from the cache, and when they complete they will be inserted as normal.

This method does not clear live references to images, since clearing those would not reduce memory pressure. Such images still have listeners in the application code, and will still remain resident in memory.

For the second line, explanation is:

Clears any live references to images in this cache.

An image is considered live if its [ImageStreamCompleter] has never hit zero listeners after adding at least one listener. The [ImageStreamCompleter.addOnLastListenerRemovedCallback] is used to determine when this has happened.

This is called after a hot reload to evict any stale references to image data for assets that have changed. Calling this method does not relieve memory pressure, since the live image caching only tracks image instances that are also being held by at least one other object.

Paludal answered 18/4 at 15:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.