# How to Create an Advanced Photo Gallery with Editing Features in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 50-70 min
- Compatibility: FlutterFlow Pro+ (code export required for custom package integration)
- Last updated: March 2026

## TL;DR

Build an advanced photo gallery in FlutterFlow by storing images in Firebase Storage and metadata in Firestore. Use a GridView for thumbnail display with Firebase Extension-generated thumbnails, a PageView with InteractiveViewer for full-screen zoom, Custom Widgets wrapping image_cropper and photofilters for editing, and long-press gesture detection for multi-select mode.

## Full-Featured Photo Gallery with In-App Editing

This tutorial builds a complete photo gallery experience in FlutterFlow. The gallery screen uses a GridView displaying thumbnail images generated by the Firebase Resize Images Extension — small versions of each photo that load quickly in a grid. Tapping a photo opens a full-screen PageView where users can swipe between photos and pinch to zoom using Flutter's built-in InteractiveViewer widget. An Edit button opens a bottom sheet with two Custom Widgets: one wrapping the image_cropper package for crop and rotate, and one wrapping the photofilters package for applying Instagram-style filters. Long-pressing a photo in the grid activates multi-select mode, showing checkboxes and a toolbar for bulk delete or share actions.

## Before you start

- FlutterFlow Pro account with Firebase Storage and Firestore enabled
- Firebase Resize Images Extension installed in Firebase Extensions
- Basic familiarity with FlutterFlow Custom Widgets
- A Firestore collection named photos with fields: imageUrl, thumbnailUrl, userId, createdAt

## Step-by-step guide

### 1. Install Firebase Resize Images Extension and Set Up Firestore Schema

In the Firebase Console, go to Extensions, search for Resize Images, and install it. Configure it to generate 200x200 thumbnails and store them with a _200x200 suffix in the same Storage path. When a user uploads photos/abc123.jpg, the extension automatically creates photos/abc123_200x200.jpg. In Firestore, create a photos collection with fields: imageUrl (String — full resolution URL), thumbnailUrl (String — the 200x200 URL), width (Integer), height (Integer), userId (String), createdAt (Timestamp), and isSelected (Boolean, default false — used for UI multi-select state). In FlutterFlow's Firestore panel, import the schema and create the photos Document Type.

**Expected result:** Firebase Extension is installed. Uploading a test image creates both a full-size and a thumbnail version in Firebase Storage.

### 2. Build the Thumbnail GridView Gallery Screen

Create a new page called Gallery. Add a Backend Query at the page level streaming the photos collection filtered by userId equals currentUser.uid, ordered by createdAt descending. Add a GridView widget set to 3 columns with 2px spacing. Inside the GridView, add an Image widget. Bind the image source to the thumbnailUrl field of each photo document — this loads the small 200x200 version, not the 5MB original. Add a GestureDetector wrapping each grid item to handle two gestures: On Tap (navigate to detail view, passing the photo document reference) and On Long Press (enter multi-select mode by updating an App State variable isMultiSelectMode to true and toggling the current photo's isSelected field). Add a Conditional Visibility overlay on each grid item showing a blue checkmark icon when isMultiSelectMode is true and the photo's isSelected is true.

**Expected result:** The gallery shows a smooth 3-column grid of thumbnails. Long-pressing activates multi-select with visible checkboxes.

### 3. Build the Full-Screen Viewer with Pinch-to-Zoom

Create a new page called PhotoDetail. Pass the photo document reference as a page parameter. At the page level, query the photos collection to get the full list for the current user (so users can swipe between photos). Add a PageView widget and set its initial page to the index of the passed photo in the list. Inside the PageView, add an InteractiveViewer widget — this is a built-in Flutter widget available in FlutterFlow under the Widgets panel. Set its minScale to 0.5 and maxScale to 4.0. Inside InteractiveViewer, add an Image widget bound to the 800x800 medium-resolution URL. Add a back arrow IconButton in the top-left and an Edit button IconButton in the top-right. The Edit button opens a Bottom Sheet containing the editing options.

**Expected result:** Tapping a photo opens a full-screen viewer. Users can pinch to zoom up to 4x and swipe to navigate between photos.

### 4. Add Crop and Filter Editing via Custom Widgets

In FlutterFlow, go to Custom Widgets and create two widgets. The first widget is named ImageCropWidget. In the pubspec dependencies, add image_cropper: ^5.0.0 and image_picker: ^1.0.4. The widget accepts a parameter imageUrl (String) and an onCropComplete callback. It uses ImageCropper().cropImage() to display the system crop UI, then returns the cropped file path via the callback. The second widget is named ImageFilterWidget. Add photofilters: ^3.0.0 as a dependency. This widget displays a horizontal scrollable list of filter previews (Clarendon, Gingham, Moon, etc.). When a filter is selected, it applies the filter to the image bytes and returns the modified bytes via an onFilterApplied callback. In the Bottom Sheet opened by the Edit button, show two tabs — Crop and Filters — that display these Custom Widgets.

```
// Custom Widget: ImageCropWidget (pubspec: image_cropper: ^5.0.0)
import 'package:image_cropper/image_cropper.dart';

class ImageCropWidget extends StatefulWidget {
  final String imageUrl;
  final Future<dynamic> Function(String croppedPath) onCropComplete;
  const ImageCropWidget({Key? key, required this.imageUrl, required this.onCropComplete}) : super(key: key);

  @override
  State<ImageCropWidget> createState() => _ImageCropWidgetState();
}

class _ImageCropWidgetState extends State<ImageCropWidget> {
  @override
  void initState() {
    super.initState();
    _startCrop();
  }

  Future<void> _startCrop() async {
    final cropped = await ImageCropper().cropImage(
      sourcePath: widget.imageUrl,
      uiSettings: [
        AndroidUiSettings(toolbarTitle: 'Crop Photo', lockAspectRatio: false),
        IOSUiSettings(title: 'Crop Photo'),
      ],
    );
    if (cropped != null) {
      widget.onCropComplete(cropped.path);
    }
  }

  @override
  Widget build(BuildContext context) {
    return const Center(child: CircularProgressIndicator());
  }
}
```

**Expected result:** The Edit bottom sheet opens with Crop and Filter tabs. Cropping opens the system crop UI. Filters show a horizontal scrollable preview strip.

### 5. Implement Multi-Select Mode with Bulk Actions

Add two App State variables: isMultiSelectMode (Boolean, default false) and selectedPhotoIds (List of Strings, default empty). On long press of any grid item, set isMultiSelectMode to true and add the tapped photo's document ID to selectedPhotoIds. When isMultiSelectMode is true, show a selection toolbar at the bottom of the Gallery page using Conditional Visibility. The toolbar contains three IconButtons: Share (uses the share_plus package to share selected images), Delete (calls a Cloud Function that batch-deletes selected Firestore documents and Storage files), and Cancel (resets isMultiSelectMode to false and clears selectedPhotoIds). Add a SelectAll button in the app bar that adds all visible photo IDs to selectedPhotoIds when tapped.

**Expected result:** Long-pressing activates multi-select. The toolbar appears with Share, Delete, and Cancel options. Tapping Cancel returns to normal gallery mode.

## Complete code example

File: `image_crop_widget.dart`

```dart
// Full Custom Widget: ImageCropWidget for FlutterFlow
// pubspec.yaml dependencies:
//   image_cropper: ^5.0.0
//   image_picker: ^1.0.4

import 'package:flutter/material.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';

class ImageCropWidget extends StatefulWidget {
  final String imageUrl;
  final double width;
  final double height;
  final Future<dynamic> Function(String croppedPath) onCropComplete;
  final Future<dynamic> Function() onCancel;

  const ImageCropWidget({
    Key? key,
    required this.imageUrl,
    required this.width,
    required this.height,
    required this.onCropComplete,
    required this.onCancel,
  }) : super(key: key);

  @override
  State<ImageCropWidget> createState() => _ImageCropWidgetState();
}

class _ImageCropWidgetState extends State<ImageCropWidget> {
  bool _loading = true;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _startCrop());
  }

  Future<void> _startCrop() async {
    try {
      final cropped = await ImageCropper().cropImage(
        sourcePath: widget.imageUrl,
        compressQuality: 90,
        uiSettings: [
          AndroidUiSettings(
            toolbarTitle: 'Crop Photo',
            toolbarColor: Colors.black,
            toolbarWidgetColor: Colors.white,
            lockAspectRatio: false,
            hideBottomControls: false,
          ),
          IOSUiSettings(
            title: 'Crop Photo',
            cancelButtonTitle: 'Cancel',
            doneButtonTitle: 'Done',
          ),
        ],
      );
      if (cropped != null) {
        await widget.onCropComplete(cropped.path);
      } else {
        await widget.onCancel();
      }
    } catch (e) {
      await widget.onCancel();
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: _loading
          ? const Center(
              child: CircularProgressIndicator(color: Colors.white),
            )
          : const SizedBox.shrink(),
    );
  }
}
```

## Common mistakes

- **Displaying full-resolution images (4000x3000, 5MB each) in GridView thumbnails** — Loading twelve 5MB images simultaneously in a grid causes out-of-memory crashes on mid-range devices and produces extremely slow scroll performance. Fix: Use the Firebase Resize Images Extension to auto-generate 200x200 thumbnails. Bind the GridView Image widget to the thumbnailUrl field, not imageUrl. Only load the full-resolution image when the user opens the detail view.
- **Saving the edited image over the original in Firebase Storage** — If the user applies a filter they later regret, there is no way to get the original back. Overwriting also breaks any shared URLs pointing to the original. Fix: Upload edited images as new files with an _edited or timestamp suffix. Update the Firestore document's editedUrl field. Keep the original imageUrl intact so users can revert.
- **Running crop and filter operations synchronously on the main UI thread** — Image processing is CPU-intensive. Running it synchronously blocks the UI thread, causing the app to freeze and potentially triggering an ANR (Application Not Responding) crash on Android. Fix: Wrap image processing in an async Future and show a CircularProgressIndicator overlay while it runs. The image_cropper package already handles this, but any custom filter processing should use compute() to run on an isolate.

## Best practices

- Always use thumbnails in grid views and only load full resolution on demand — this is the single biggest performance win in a photo gallery app.
- Store both imageUrl and thumbnailUrl in Firestore so you can switch between them based on context without additional Storage queries.
- Keep original photos intact — save edits as new files and track them in a separate editedUrl field.
- Limit the Backend Query for the gallery to the most recent 200 photos and implement infinite scroll for older content.
- Use Hero animations between grid thumbnails and the detail PageView for a polished transition that feels native.
- Add image upload progress tracking using Firebase Storage's upload task progress stream — show a loading indicator on the grid cell while the upload completes.
- Request Camera and Photo Library permissions gracefully — explain why the app needs them before the system permission dialog appears.

## Frequently asked questions

### Which image editing packages work best with FlutterFlow Custom Widgets?

image_cropper (v5+) for crop/rotate, photofilters for filter effects, and flutter_colorpicker for color adjustments are the most reliable options. They have pre-built UI, are actively maintained, and work on both iOS and Android. Add them in the Custom Widget's pubspec dependencies section.

### How do I handle video files alongside photos in the gallery?

Add a mediaType field (String) to your Firestore schema with values 'image' or 'video'. For videos, use the video_player package in a Custom Widget for thumbnail and playback. The GridView can conditionally show a play icon overlay on video items based on the mediaType field.

### Can users share photos from within the FlutterFlow app?

Yes. Use the share_plus package in a Custom Action. Pass the Firebase Storage download URL to Share.shareUri() for sharing a link, or download the image bytes first and use Share.shareXFiles() to share the actual image file.

### How do I implement photo albums or folders?

Add an albumId (String) field to your photos Firestore collection. Create a separate albums collection. Filter the gallery GridView Backend Query by albumId using a page parameter passed from an album list screen.

### The pinch-to-zoom resets every time I swipe to the next photo — how do I fix this?

This is expected behavior — each PageView child creates a new InteractiveViewer with default scale. Add a TransformationController to each InteractiveViewer and reset it in the PageView's onPageChanged callback. This gives smooth zoom reset between photos.

### How many photos can I store per user before performance degrades?

Firestore has no practical limit on documents per collection. The performance constraint is in the GridView — loading more than 200 thumbnails in a single query will slow page load. Use FlutterFlow's Infinite Scroll on the GridView to load 50 photos at a time and fetch more as the user scrolls down.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-an-advanced-photo-gallery-with-editing-features-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-an-advanced-photo-gallery-with-editing-features-in-flutterflow
