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];
}
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' ),
),
);
},