Welcome to Our Website

Gallery App in Flutter with BLoC

Gallery app in flutter

In this blog we’ll create Gallery app in Flutter with BLoC pattern with view images in Grid and delete feature like gallery

Check out my other blog regarding Provider and Riverpod

What is flutter bloc?

Flutter bloc is one of the state management for Flutter applications. You can use it to handle all the possible states of your application in an easy way but it might create little boilerplate code.

Getting Started

Create a new Flutter project and name it whatever you want.

Now add the flutter_bloc dependency with photo_manager other following dependency in the pubspec.yaml file.

dependencies:

  photo_manager: ^1.3.10
  image_picker: ^0.8.4+4
  image_gallery_saver: ^1.7.1
  flutter_bloc: ^7.3.0

Note: photo_manager is used for fetching images from Gallery.

The architecture of app will be like following:

main

We will initialize GalleryCubit at very first to access it across app.

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<GalleryCubit>(
            create: (context) => GalleryCubit()),
      ],
      child: const MaterialApp(
        title: 'Photo Manager Demo',
        debugShowCheckedModeBanner: false,
        onGenerateRoute: RouteGenerator.generateRoute,
        initialRoute: Gallery.routeName,

      ),
    );
  }
}

gallery_cubit

Below is the code to cubit class.

class GalleryCubit extends Cubit<GalleryState> {
  GalleryCubit() : super(GalleryLoading());
  List<AssetEntity>? assets ;

  fetchAssets() async {

    final albums = await PhotoManager.getAssetPathList(onlyAll: true,type: RequestType.image);
    final recentAlbum = albums.first;
    final recentAssets = await recentAlbum.getAssetListRange(start: 0, end: 1000000,);
    assets = recentAssets;
    emit(GalleryFetchSuccess(recentAssets));

  }

  changeDeleteMode(bool value){
    if(assets!=null) {
      emit(GalleryFetchSuccess(assets!,deleteMode: value));
    }
  }
}

gallery_state

Below is the code to create state class.

abstract class GalleryState {}

class GalleryLoading extends GalleryState {
  GalleryLoading();
}

class GalleryFetchSuccess extends GalleryState {
  final List<AssetEntity> assets ;
  bool deleteMode = false;
  List<int> itemToDelete = [];

  GalleryFetchSuccess(this.assets, {this.deleteMode =false});
}

route_generator

Don’t forget to pass AssetEntity argument in ScreenImage .

class RouteGenerator {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    final args = settings.arguments;
    switch (settings.name) {
      case Gallery.routeName:
        return MaterialPageRoute(
          builder: (_) => const Gallery(),
        );
      case ScreenImage.routeName:
        return MaterialPageRoute(
          builder: (_) => ScreenImage(asset: args as AssetEntity,),
        );
      default:
        return MaterialPageRoute(
          builder: (_) => Container(),
        );
    }
  }
}

ui_component

Ui component to show alert dialog and some loading indicator.

class Ui {
  static showConfirmationAlert(BuildContext context,  VoidCallback? onConfirm) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return CupertinoAlertDialog(
          title: const Text("Are you sure to delete?"),
          actions: <Widget>[
            TextButton(
              child: const Text("NO"),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),

            TextButton(
              child: const Text("YES"),
              onPressed: () {
                Navigator.of(context).pop();
                if(onConfirm!=null) {
                  onConfirm();
                }
              },
            ),
          ],
        );
      },
    );
  }

  static Future<void> showLoadingDialog(
      BuildContext context) async {
    return showDialog<void>(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {

          return WillPopScope(
              onWillPop: () async => false,
              child: Container(color: Colors.white54.withAlpha(50),
                child: const Center(
                  child: CircularProgressIndicator(color: Colors.brown,),
                ),
              ));
        });
  }

gallery_item_widget

Single Image item to display it in Grid view latter.

class GalleryItem extends StatefulWidget {
  const GalleryItem(this.asset, this.index, {Key? key}) : super(key: key);

  final AssetEntity asset;
  final int index;

  @override
  _GalleryItemState createState() => _GalleryItemState();
}

class _GalleryItemState extends State<GalleryItem> {
  @override
  Widget build(BuildContext context) {
    AssetEntity asset = widget.asset;
    int index = widget.index;

    return FutureBuilder<Uint8List?>(
        future: asset.thumbData,
        builder: (_, snapshot) {
          var bytes = snapshot.data;
          if (bytes == null) return const Center(child:  CircularProgressIndicator());
          return BlocBuilder<GalleryCubit, GalleryState>(
              builder: (context, state) {
                var provider = state as GalleryFetchSuccess;
                return InkWell(
                  onTap: () {
                    if(provider.deleteMode){
                      setState(() {
                        if(state.itemToDelete.contains(index)){
                          state.itemToDelete.remove(index);
                        }else {
                          state.itemToDelete.add(index);
                        }
                      });

                    }else {
                      Navigator.of(context).pushNamed(ScreenImage.routeName,arguments:asset).then((value){
                        if(value!=null) {
                          BlocProvider.of<GalleryCubit>(context).fetchAssets();
                        }
                      });
                    }
                  },
                  child: Container(
                      padding: const EdgeInsets.all(1.2),
                      child: Stack(
                        children: [
                          Positioned.fill(
                              child: Image.memory(bytes,cacheHeight: 1000, cacheWidth: 600,
                                  fit: BoxFit.cover)),
                          if(provider.deleteMode)
                            Align( alignment: Alignment.topRight,
                              child: Checkbox(
                                value: state.itemToDelete.contains(index),
                                activeColor: Colors.brown,
                                onChanged: (value) {
                                  setState(() {
                                    if(state.itemToDelete.contains(index)){
                                      state.itemToDelete.remove(index);
                                    }else {
                                      state.itemToDelete.add(index);
                                    }
                                  });
                                },
                              ),
                            ),
                        ],
                      )),
                );
              }
          );
        }
    );
  }
}

screen_gallery

The screen of the app in which we are displaying all images available in mobile device in Grid view.

class Gallery extends StatefulWidget {
  const Gallery({Key? key}) : super(key: key);
  static const routeName = "screenGallery";

  @override
  _GalleryState createState() => _GalleryState();
}

class _GalleryState extends State<Gallery> {

  List<AssetEntity> assets = [];
  late BuildContext scaffoldContext;
  late GalleryCubit _galleryCubit;

  @override
  void didChangeDependencies(){
    _galleryCubit = BlocProvider.of<GalleryCubit>(context);
    PhotoManager.requestPermission().then((permitted){
      if (permitted) {
        _galleryCubit.fetchAssets();
      } else {
        showInSnackBar();
      }
    });
    super.didChangeDependencies();
  }

  void showInSnackBar() {
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text('Permission Denied'),
    ));
  }

  @override
  Widget build(BuildContext context) {

    Color commonColor = Colors.brown;

    return Scaffold(
      appBar: AppBar(
        title:  Text('PhotoApp' ,style: TextStyle(color: commonColor), ),
        backgroundColor: Colors.white,
        actions: [
          BlocBuilder<GalleryCubit, GalleryState>(builder: (context, state) {
            if (state is GalleryFetchSuccess) {
              return IconButton(
                  onPressed: () {
                    if (state.deleteMode) {
                      if(state.itemToDelete.isNotEmpty) {
                        Ui.showConfirmationAlert(context,  () async {
                          Ui.showLoadingDialog(context);

                          try{
                            await Future.wait(state.itemToDelete.map((element)async {
                              String ids =  assets[element].id;
                              await PhotoManager.editor.deleteWithIds([ids]);

                            }));
                          }catch(error){
                            debugPrint("ERROR: "+error.toString());
                          }
                          state.itemToDelete.clear();
                          _galleryCubit.changeDeleteMode(false);
                          _galleryCubit.fetchAssets();
                          Navigator.of(context).pop();
                      });
                      }
                      else{
                        _galleryCubit.changeDeleteMode(false);
                      }

                    } else {
                      _galleryCubit.changeDeleteMode(true);
                    }
                  },
                  icon: Icon(
                    state.deleteMode ? Icons.check : Icons.delete_forever_sharp,
                    color: commonColor,
                  ));
            } else {
              return const SizedBox();
            }
          })
        ],
      ),
      body: Builder(
          builder: (context) {
            scaffoldContext = context;
            return BlocBuilder<GalleryCubit, GalleryState>(
                builder: (context, state) {
                  if(state is GalleryLoading) {
                    return const Center(child:  CircularProgressIndicator());
                  }
                  else if(state is GalleryFetchSuccess){
                    assets.clear();
                    assets.addAll(state.assets);
                    return GridView.builder(
                      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
                      itemCount: assets.length+1,
                      itemBuilder: (_, index){
                        if(index==0) {
                          return IconButton(onPressed:(){
                            _takePhoto();
                          }, icon: Icon(Icons.camera_alt,color: commonColor,));
                        }
                        else {
                          AssetEntity asset = assets[index -1];
                          return GalleryItem(asset, index-1);

                    }
                  },
                );
                  }
                  else{return const SizedBox();}
              }
            );
          }
      ),
    );
  }

  void _takePhoto() async {
    ImagePicker().pickImage(source: ImageSource.camera)
        .then((recordedImage) async {
      if (recordedImage != null) {
        Ui.showLoadingDialog(context);
        await ImageGallerySaver.saveFile(recordedImage.path);

        Future.delayed(const Duration(seconds: 2),(){
          BlocProvider.of<GalleryCubit>(context).fetchAssets();
          Navigator.of(context).pop();
        });
      }
    });
  }
}

screen_single_image

Now screen in will the single image is going to display.

class ScreenImage extends StatelessWidget {
  const ScreenImage({Key? key, required this.asset,}) : super(key: key);

  final AssetEntity asset;
  final Color commonColor = Colors.brown;
  static const  String routeName = "imageView";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        iconTheme: IconThemeData(color: commonColor),
        title:  Text('PhotoApp' ,style: TextStyle(color: commonColor), ),
        backgroundColor: Colors.white,
        actions: [
          IconButton(onPressed: (){

            Ui.showConfirmationAlert(context,  () async {
              Ui.showLoadingDialog(context);

              try{
                await PhotoManager.editor.deleteWithIds([asset.id]);
              }catch(error){
                debugPrint("ERROR: "+error.toString());
              }
              Navigator.of(context).pop();
              Navigator.of(context).pop(true);
            });

          }, icon: const Icon(Icons.delete_forever_sharp,))
        ],
      ),
      body: Container(
        color: Colors.black,
        alignment: Alignment.center,
        child: FutureBuilder<File?>(
          future: asset.file,
          builder: (_, snapshot) {
            final file = snapshot.data;
            if (file == null) return Container();
            return Image.file(file);
          },
        ),
      ),
    );
  }
}

That’s it

After creating this app you are able to display images present in mobile device in this app and will also have feature like viewing image and also of deleting image too.

You can get source code from Github.

Hope you like the Gallery app in flutter with BLoC pattern. As i have mentioned earlier it might contain little boilerplate code but it is worth will making this type of app.