Gallery App in Flutter with BLoC

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.