Flutter Bloc Remove Item From Grid

Viewed 480

I have a widget that is controlled by the state of the bloc. As a start, a list of URLs is passed to the widget, which is used to create the initial grid. Then, once URLs must be added or removed, the bloc events are called which updates the list and yields a new state. My problem however is that when an item is removed, the list which gets printed to the screen is correct (with the item removed), but the grid view seems to only remove the last image instead of the deleted ones. What am I doing wrong here?

Related question (I did change from Gridview. count to Gridview.builder), but to no avail.

The widget:

class ItemImageGrid extends StatelessWidget {


  final List<String> urls;

  const ItemImageGrid({Key key, this.urls }) : super(key: key);


  @override
  Widget build(BuildContext context) {
    return Container(
      child: BlocBuilder<ItemGridBloc, ItemGridState>(
        builder: (context, state) {
          if(state is ItemGridInitialState){
            print("state is ItemGridInitialState");
            return _showCarouselAndStartLoading(context);

          }else  if(state is ImagesUpdatedState){
            print("state is ImagesUpdatedState");
            return _showGrid(context, state.urls);

          }
          return Center(child: CircularProgressIndicator());
        },
      ),
    );
  }

  Widget _showCarouselAndStartLoading(BuildContext context){
    BlocProvider.of<ItemGridBloc>(context).add( // add or dispatch??? try both to see difference - seems dispatch cannot be used here...
      LoadImageEvent(urls),
    );
    return Center(child: CircularProgressIndicator());
  }



  Widget _showGrid(BuildContext context, List<String> urls) {

    if(urls.isEmpty){
      return Center(
          child: Text("No photos yet")
      );
    }

  return GridView.builder(
    itemCount: urls.length,
      physics: NeverScrollableScrollPhysics(),
      shrinkWrap: true,
      // You must use the GridDelegate to specify row item count
      // and spacing between items
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
       // childAspectRatio: 1.0,
        crossAxisSpacing: 5.0,
        mainAxisSpacing: 5.0,
      ),
      itemBuilder: (BuildContext context, int index) {
        return  BlocProvider(
          create: (context) => CachedImageBloc( ),
          child: GestureDetector(
            onTap: () => {
              Navigator.of(context).push(
                  MaterialPageRoute(
                      builder: (context2) {
                        //   List<String> clonedList;
                        return BlocProvider(
                          create: (context) => new CachedImageBloc( ),
                          child: new BasicImagePreview(
                            imageFilePath: urls[index],
                            //  bloc: BlocProvider.of<InspectionItemAddEditBloc>(context),
                            //   image_paths: imagePaths,
                            onDelete: () =>{
             
                              BlocProvider.of<ItemGridBloc>(context).add(ImageRemovedEvent( urls[index])),

                            },
                          ),
                        );
                      }))
            },
            child: LocalImageViewer(
                url:  urls[index],
                errorAssetPath: 'assets/error_loading.png' ),
          ),

        );
      },
  );


  }
  
}

The bloc:

class ItemGridBloc extends Bloc<ItemGridEvent, ItemGridState> {

  List<String> urls;

  ItemGridBloc();

  @override
  Stream<ItemGridState> mapEventToState(
      ItemGridEvent event,
      ) async* {
    print('event = $event');

    if(event is ImageAddedEvent){
      yield* _mapImageAddedEventToState(event);
    }
    else if(event is ImageRemovedEvent){
      yield* _mapImageRemovedEventToState(event);
    } else if(event is LoadImageEvent){
      yield* _mapLoadImageEventToState(event);
    }
  }

  @override
  ItemGridState get initialState => ItemGridInitialState();

  Stream<ItemGridState> _mapImageAddedEventToState(ImageAddedEvent event) async*{

    urls.add(event.url);
    yield ImagesUpdatedState(List.of(urls));
  }

  Stream<ItemGridState>_mapImageRemovedEventToState(ImageRemovedEvent event) async*{

    print('urls b4 remove:');
    urls.forEach((element) {print('$element');});

    print('removing url: ${event.url}');
    urls.remove(event.url);
    print('urls after remove:');

    urls.forEach((element) {print('$element');});

    yield ImagesUpdatedState(urls);

  }

  Stream<ItemGridState> _mapLoadImageEventToState(LoadImageEvent event) async*{
    urls = List.of(event.urls);
    yield ImagesUpdatedState(event.urls);
  }
}

Event class:

abstract class ItemGridEvent extends Equatable {
  const ItemGridEvent();
}

class ImageAddedEvent extends ItemGridEvent{

  final String url;

  ImageAddedEvent(this.url);

  @override
  List<Object> get props => [url];

}

class LoadImageEvent extends ItemGridEvent{

  final List<String> urls;

  LoadImageEvent(this.urls);

  @override
  List<Object> get props => [urls];

}

class ImageRemovedEvent extends ItemGridEvent{

  final String url;

  ImageRemovedEvent(this.url);

  @override
  List<Object> get props => [url];

}

State class:

abstract class ItemGridState extends Equatable {
  const ItemGridState();
}

class ItemGridInitialState extends ItemGridState {
  @override
  List<Object> get props => [];
}

class ImagesUpdatedState extends ItemGridState {

  final List<String> urls;

  ImagesUpdatedState(this.urls);

  @override
  List<Object> get props => [urls];
}
3 Answers

After many hours searching I discovered the answer was related to KEYS in Flutter.

In short Flutter compares widgets only by Type and not state. Thus when the state is changed of the List represented in the GridView, Flutter doesn't know which children should be removed as their Types are still the same and checks out. The only issue Flutter picks up is the number of items, which is why it always removes the last widget in the Grid.

Therefore, if you want to manipulate lists in Flutter which contains stateful children, assign a Key to the top level widget of each child. A more detailed explanation is available in this article.

The only change I made to the code was the following in generating the items:

itemBuilder: (BuildContext context, int index) {
        return  BlocProvider(
          key: UniqueKey(), // assign the key
          create: (context) => CachedImageBloc( ),
          child: GestureDetector(
            onTap: () => {
              Navigator.of(context).push(
                  MaterialPageRoute(
                      builder: (context2) {
                        //   List<String> clonedList;
                        return BlocProvider(
                          create: (context) => new CachedImageBloc( ),
                          child: new BasicImagePreview(
                            imageFilePath: urls[index],
                            //  bloc: BlocProvider.of<InspectionItemAddEditBloc>(context),
                            //   image_paths: imagePaths,
                            onDelete: () =>{
             
                              BlocProvider.of<ItemGridBloc>(context).add(ImageRemovedEvent( urls[index])),

                            },
                          ),
                        );
                      }))
            },
            child: LocalImageViewer(
                url:  urls[index],
                errorAssetPath: 'assets/error_loading.png' ),
          ),

        );
      },

Thank you so much for your answer TM00.

My ListView.builder wasnt updating properly when deleting and thanks to you, I got it working.

For anyone else who runs into the issue of a list view or grid view builder not removing the correct items using BLoC:

Just wrap the item your building in a container and add a UniqueKey

ListView.builder(
                      itemBuilder: (context, index) => 
                          Container(
                            key: UniqueKey(), 
                            child: Widget(),
                      itemCount: array.length,
                    );

in the bloc when you have removed an item u should pass a new instance of the list to the ImagesUpdatedState

why ? because you are passing a reference of the object to your state and bloc will compare the new state with the previous one and it will see no difference between the previous state urls property and the new one so it won't rebuild the widget passing the reference of object is dengares beacuse when ever you change object property it will reflect on the all of the refrence you have used

Related