rrousselGit / state_notifier

ValueNotifier, but outside Flutter and with some extra perks
MIT License
311 stars 28 forks source link

Updating the state multiple times does not update the UI #45

Closed ablbol closed 3 years ago

ablbol commented 3 years ago

I have two questions about StateNotifier.

I am using StateNotifier (EditMyProfileViewModel) which holds a state (MyProfileModel) shown below and it has addImage function that is supposed to upload an image and do few other things. The addImage function updates the state 2 times, after Future calls, and I noticed that only the first update will change the UI. The other state change does not change the UI.

Do I need to do anything to make all 2 state changes update the UI. I use copyWith to change the state. Please make sure to read the comments in the code.

@immutable
class ImageModel {
  final String _fileName;
  final String _url;
  final File _file;
  final bool _deleting;
  final bool _uploading;
...
...
  ImageModel copyWith({
    String fileName,
    String url,
    File file,
    bool deleting,
    bool uploading,
  }) {
    return ImageModel(
      fileName: fileName ?? this._fileName,
      url: url ?? this._url,
      file: file ?? this._file,
      deleting: deleting ?? this._deleting,
      uploading: uploading ?? this._uploading,
    );
  }
}

@immutable
class MyProfileModel {
  final String name;
  final String email;
  final List<ImageModel> images;
...
....

  MyProfileModel copyWith({
    String name,
    String email,
    List<ImageModel> images,
  }) {
    return MyProfileModel(
      name: name ?? this.name,
      email: email ?? this.email,
      images: images ?? this.images,
    );
  }
}

class EditMyProfileViewModel extends StateNotifier<MyProfileModel> with EditProfileValidators, LocatorMixin {

  EditMyProfileViewModel(MyProfileModel myProfile) : super(myProfile);

  Future<void> addImage({
    @required ImageSource imageSource,
    @required Color primaryColor,
  }) async {
    assert(imageSource != null);
    assert(primaryColor != null);

    final imagePicker = read<ImagePickerService>();
    final storage = read<FirebaseStorageService>();
    final firestore = read<FirebaseFirestoreService>();

    // crop image
    PickedFile pickedImage = await imagePicker.pickImage(imageSource);
    File croppedImage = await imagePicker.cropImage(primaryColor, pickedImage);

    if (croppedImage != null) {
      String fileName = '${DateTime.now().microsecondsSinceEpoch}.jpg';

      // THIS IS A NEW IMAGE, SET UPLOADING TO TRUE
      ImageModel imageModel = ImageModel(
        fileName: fileName,
        file: croppedImage,
        uploading: true,
      );

      // ADD THE NEW IMAGE TO THE LIST OF IMAGES AND UPDATE THE STATE WITH A NEW COPY
      List<ImageModel> images = [...state.images, imageModel];

      // THIS STATE CHANGE WILL UPDATE THE UI
      state = state.copyWith(images: images);

      // UPLOAD IMAGE TO STORAGE
      String url = await storage.addImage(
        file: croppedImage,
        fileName: fileName,
      );

      // UPDATE DATABASE
      await firestore.addImage(
        fileName: fileName,
        url: url,
      );

      // UPDATE THE PREVIOUSLY ADDED IMAGE WITH A URL AND UPLOADING FALSE
      images = state.images.map((image) {
        if (image.fileName == fileName) {
          imageModel = imageModel.copyWith(uploading: false, url: url);
          return imageModel;
        }
        return image;
      }).toList();

      // UPDATE STATE WITH A NEW ONE.  THIS STATE CHANGE DOES NOT CHANGE THE UI EVENT THOUGH
      // I AM CREATING A NEW STATE AND NEW IMAGES LIST.
      state = state.copyWith(images: images);
    }
  }
}

In the UI, I have AlbumEditor widget. Please see comments.

class AlbumEditor extends StatelessWidget {
  const AlbumEditor({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    // THIS LINE UPDATES THE UI 2 TIMES.  BUT IT LISTENS TO ANY CHANGE IN MyProfileModel
    // I PREFER TO USE SELECT TO LISTEN TO CHANGES IN IMAGES ONLY
    // final images = context.watch<MyProfileModel>().images;

    // IF I USE THIS LINE INSTEAD, THE UI UPDATES THE FIRST TIME STATE CHANGES.  THERE IS NO UPDATE IN THE UI
    // IN THE SECOND STATE CHANGE
    final images = context
        .select<MyProfileModel, List<ImageModel>>((state) => state.images);

    //. PLEASE READ COMMENT BELOW
    return ColoredBox(
      color: Theme.of(context).colorScheme.surface,
      child: GridView.builder(
        padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 40.0),
        shrinkWrap: true,
        physics: const NeverScrollableScrollPhysics(),
        clipBehavior: Clip.none,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          childAspectRatio: 3.0 / 4.0,
          mainAxisSpacing: 15,
          crossAxisSpacing: 15,
          crossAxisCount: 3,
        ),
        itemCount: 9,
        itemBuilder: (context, index) {
          print('index - $index');

          // const DOES NOT WORK
         // I AM USING CONST HOPING THAT AlbumEditorImage DOES NOT REBUILD UNLESS IT NEEDS TO.
         // BUT FOR SOME REASON, AlbumEditorImage DOES NOT REBUILD WITH CONST. CODE BELOW
         // Provider.value DOES NOT HELP OVER HERE FOR SOME REASON
          return Provider<ImageModel>.value(
            value: index >= images.length ? null : images[index],
            child: const AlbumEditorImage(),
          );
        },
      ),
    );
  }
}

Here is the code for the AlbumEditorImage

class AlbumEditorImage extends StatelessWidget {
  const AlbumEditorImage();

  @override
  Widget build(BuildContext context) {
    // SHOULDN'T THIS LINE CAUSE REBUILD EVEN THOUGH I AM USING CONST ABOVE?
    final imageModel = context.watch<ImageModel>();

    // THE REST OF THE CODE IS NOT IMPORTANT

}
rrousselGit commented 3 years ago

Are you sure that your second state = state.copyWith(images: image) is reached?

Maybe you have an exception before in this method.

ablbol commented 3 years ago

Yes I am sure that the second state is reached. Since everyone is switching to Riverpod, I am switching too. I will try the StateNotifier over there. I will close this issue if you like. Thanks for your help.

ablbol commented 3 years ago

I switched into StateNotifier in Riverpod and found out that I have the same problem. So I investigated some more and found out that the problem is that in MyProfileModel I don't have a good implementation of these two functions:

int get hashCode {}
bool operator ==(Object o) {}

Once I implemented them appropriately, things worked out well. I know that Freeze give me all that but I decided not to use it for now while learning about Riverpod and Provider.

Lessons learned: make sure all of your models implement the two functions above while using Riverpod or Provider. I will go ahead and close the issue.