Skip to main content
RapidDev - Software Development Agency
App Featuresmedia-content20 min read

How to Add a Dynamic Image Gallery with Filters to Your App (Copy-Paste Prompts)

A dynamic image gallery with filters needs four pieces: a Supabase table with a category column, CachedNetworkImage for lazy loading with shimmer placeholders, FilterChip widgets for category selection, and photo_view for full-screen pinch-to-zoom. FlutterFlow builds almost all of this visually with a Backend Query — only the masonry layout and full-screen viewer need Custom Widget blocks. Expect 2–4 hours with FlutterFlow for a standard grid, 3–7 days custom. Running costs start at $0/mo on the Supabase free tier.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Beginner

Category

media-content

Build with AI

2–4 hours with FlutterFlow

Custom build

3–7 days custom dev

Running cost

$0/mo at 100 users; $0–25/mo at 1K users; $25–100/mo at 10K users

Works on

Mobile

Everything it takes to ship a Dynamic Image Gallery with Filters — parts, prompts, and real costs.

TL;DR

A dynamic image gallery with filters needs four pieces: a Supabase table with a category column, CachedNetworkImage for lazy loading with shimmer placeholders, FilterChip widgets for category selection, and photo_view for full-screen pinch-to-zoom. FlutterFlow builds almost all of this visually with a Backend Query — only the masonry layout and full-screen viewer need Custom Widget blocks. Expect 2–4 hours with FlutterFlow for a standard grid, 3–7 days custom. Running costs start at $0/mo on the Supabase free tier.

What a Dynamic Image Gallery with Filters Actually Is

A dynamic image gallery with filters is a scrollable grid of images pulled from a backend, organized into categories the user can switch between using tabs or chip buttons. Tapping an image opens a full-screen viewer with pinch-to-zoom and swipe navigation between images in the active filter set. The feature appears in portfolio apps, product catalogues, art platforms, real estate listings, and any app where browsing images is a primary activity. The product decisions that matter: whether categories are single-select or multi-select, whether you want masonry (variable heights, Pinterest-style) or uniform grid, how images are paginated (page-based or infinite scroll), and whether the gallery is public or user-specific.

What users consider table stakes in 2026

  • Category filter chips or tabs at the top of the screen that highlight the active selection and smoothly transition the grid content when switched
  • Shimmer placeholder animation while each image loads — a blank white grid while images fetch looks like a crash
  • Tap image to open full-screen viewer with pinch-to-zoom; swipe left/right to navigate to the next image in the filtered set
  • Hero animation linking the thumbnail to the full-screen view so the transition feels native and fluid
  • Pull-to-refresh to reload the gallery without a hard restart
  • Empty state illustration with a clear message when no images match the selected filter or search query
  • Search bar that filters images by title or tag with a debounced query so the grid doesn't flicker on every keystroke

Anatomy of the Feature

Six components. FlutterFlow's visual builder handles the filter chips, GridView, and Supabase Backend Query natively. The full-screen viewer (photo_view) and masonry layout (flutter_staggered_grid_view) each need one Custom Widget registration, but both packages are well-documented for FlutterFlow.

Layers:UIData

Filter tab bar

UI

A horizontally scrollable Row of FilterChip widgets, one per category pulled from the gallery_categories Supabase table. The selected category is stored in page state (or App State for multi-screen persistence). Selecting a chip sets the active category and triggers a Backend Query refresh with .eq('category', selectedCategory). AnimatedSwitcher wraps the grid for a smooth content fade on filter change.

Note: In FlutterFlow, the chip's onSelected callback must explicitly call the Backend Query refresh action regardless of whether the chip is being selected or deselected — the default behavior only triggers on select, causing the gallery not to reset when a user taps the active chip to deselect it.

Image grid

UI

GridView.builder with crossAxisCount: 2 and childAspectRatio: 1.0 for a uniform grid. CachedNetworkImage (^3.4.x) handles lazy loading with a shimmer placeholder (Shimmer.fromColors from the shimmer package) while each image fetches. Images are paginated with Supabase range(0, 19) and loaded in additional pages of 20 as the user scrolls toward the bottom.

Note: For masonry layout (variable-height images, Pinterest-style), use flutter_staggered_grid_view (^0.7.x) with StaggeredGridView.countBuilder. This package requires a Custom Widget in FlutterFlow; the standard GridView.builder is available natively and covers most gallery use cases.

Full-screen viewer

UI

photo_view (^0.15.x) renders individual images with pinch-to-zoom (min scale 0.5, max scale 4.0). PhotoViewGallery.builder wraps a PageView for swipe navigation between images in the current filtered set. Hero widget on each thumbnail with tag: image.id provides the shared-element transition from grid to full-screen.

Note: Pass only the currently filtered image list to PhotoViewGallery — not the full unfiltered set. Loading hundreds of images into PhotoViewGallery on low-RAM Android devices causes memory pressure and crashes. Limit to the current page of 20–50 filtered results.

Image data layer

Data

Supabase table gallery_images with columns: title, description, image_url, thumbnail_url, category, tags (text[]), sort_order, is_published. A companion gallery_categories table drives the filter chip list. Queries use .eq('category', selectedCategory) for single-category filters and .contains('tags', [searchQuery]) for tag-based search.

Note: Always query thumbnail_url for the grid and only load image_url in the full-screen viewer. Thumbnails at 400px wide reduce grid bandwidth by 70–80% vs full-resolution images. If you don't have a thumbnail pipeline, Cloudinary's on-the-fly transformation URLs (append /w_400,q_auto to the image URL) generate thumbnails without pre-processing.

Search layer

UI

Flutter SearchBar widget (Material 3) at the top of the screen. Debounced 400 ms after the last keystroke using a Timer that cancels and restarts on each onChange event. On fire: executes a Supabase query with .ilike('title', '%query%') or .contains('tags', [query]) against the current filtered category. Clears to the full gallery when the search field is emptied.

Note: Supabase .contains() on a text[] column requires the column to be created as text[] (not text). If tags was created as a comma-separated text column, use .ilike('tags', '%query%') as a workaround until the column type is corrected — the two queries behave differently and one returns no results silently if the type is wrong.

Pull-to-refresh

UI

RefreshIndicator widget wrapping the GridView triggers a re-fetch from Supabase with the current active filter and search query. Resets the page counter to 0 so pagination restarts. In FlutterFlow, the RefreshIndicator onRefresh callback should call the Backend Query's refetch action.

Note: If using infinite scroll, pull-to-refresh must also reset the page offset variable to 0 before re-fetching. Failing to reset the offset causes the gallery to load page 2 instead of page 1 after a refresh.

The data model

Two tables: gallery_images for image data and gallery_categories for the filter list. Run this in the Supabase SQL editor. RLS keeps the gallery public for published images and restricts admin writes.

schema.sql
1create table public.gallery_categories (
2 id uuid primary key default gen_random_uuid(),
3 name text not null,
4 slug text not null unique,
5 sort_order int not null default 0
6);
7
8create table public.gallery_images (
9 id uuid primary key default gen_random_uuid(),
10 title text not null,
11 description text,
12 image_url text not null,
13 thumbnail_url text,
14 category text not null,
15 tags text[] not null default '{}',
16 sort_order int not null default 0,
17 is_published boolean not null default true,
18 created_at timestamptz not null default now()
19);
20
21alter table public.gallery_categories enable row level security;
22alter table public.gallery_images enable row level security;
23
24create policy "Public can read gallery categories"
25 on public.gallery_categories for select
26 to anon, authenticated
27 using (true);
28
29create policy "Public can read published images"
30 on public.gallery_images for select
31 to anon, authenticated
32 using (is_published = true);
33
34create policy "Authenticated admins can manage categories"
35 on public.gallery_categories for all
36 to authenticated
37 using (auth.jwt() ->> 'role' = 'admin')
38 with check (auth.jwt() ->> 'role' = 'admin');
39
40create policy "Authenticated admins can manage images"
41 on public.gallery_images for all
42 to authenticated
43 using (auth.jwt() ->> 'role' = 'admin')
44 with check (auth.jwt() ->> 'role' = 'admin');
45
46create index gallery_images_category_sort_idx
47 on public.gallery_images (category, sort_order)
48 where is_published = true;
49
50create index gallery_images_tags_idx
51 on public.gallery_images using gin (tags);

Heads up: The GIN index on tags makes .contains() queries on the text[] column fast even with thousands of images. The partial index on category + sort_order (where is_published = true) keeps category filter queries snappy without scanning unpublished rows.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Native iOS + AndroidFit for this feature:

FlutterFlow's Backend Query for Supabase, GridView component, and FilterChip can build this gallery visually with minimal custom Dart. The standard 2-column grid and filter chips require no custom code at all — only masonry layout and the full-screen photo_view viewer need Custom Widget registrations.

Step by step

  1. 1In Supabase, run the SQL schema above; set the Supabase URL and anon key in FlutterFlow Project Settings under API & Integrations
  2. 2Create a GalleryPage with a Backend Query on gallery_categories to populate the filter chip Row, and a separate Backend Query on gallery_images filtered by the selected category App State variable
  3. 3Add a horizontally scrollable Row of FilterChip widgets bound to the categories query result; in each chip's onSelected action, update the active category App State variable and call the images Backend Query's refetch action
  4. 4Add a GridView component with 2 columns; in each cell, place a CachedNetworkImage bound to thumbnail_url with a ShimmerEffect placeholder from FlutterFlow's built-in widget list
  5. 5For the full-screen viewer: add a Custom Widget using photo_view's PhotoViewGallery.builder; wrap each grid thumbnail in a Hero widget with uniqueTag set to the image ID; on thumbnail tap, navigate to the full-screen page passing the filtered image list and the tapped index
  6. 6Test on a physical device via the FlutterFlow mobile preview — the web preview does not replicate Flutter Hero animations or CachedNetworkImage shimmer behavior accurately

Where this path bites

  • flutter_staggered_grid_view masonry layout is not available as a native FlutterFlow widget — it requires a Custom Widget registration; for most galleries, the standard 2-column GridView is sufficient
  • FlutterFlow's built-in filter chip action fires only on select, not on deselect — the gallery doesn't update when a user taps the active chip to clear the filter; fix this by adding an explicit Backend Query refetch action to the onSelected callback for both true and false states
  • Infinite scroll requires a page counter variable and manual range calculation — FlutterFlow's Backend Query pagination helpers don't fully automate this; expect 20–30 minutes of action flow configuration
  • The FlutterFlow web preview does not accurately render CachedNetworkImage shimmer or Hero animations — always test on a physical device

Third-party services you'll need

The gallery runs almost entirely on Supabase. Cloudinary is optional for automated thumbnail generation if you don't have a pre-processing pipeline:

ServiceWhat it doesFree tierPaid from
Supabase StorageHosts gallery images and thumbnails; serves them over CDN-backed HTTPS for fast mobile loading1 GB storage, 2 GB bandwidth$25/mo (Pro) includes 100 GB storage + 200 GB bandwidth (approx)
CachedNetworkImageFlutter package for lazy loading images with shimmer placeholder and disk/memory caching — reduces repeated network fetchesOpen-source, freeFree
CloudinaryOptional: on-the-fly image resizing and WebP conversion via URL parameters; generates thumbnails without pre-processing your upload pipeline25 GB storage + bandwidth per monthPlus $99/mo for higher limits (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier covers small image sets easily. At 100 users browsing a gallery of 200 images (thumbnails ~50 KB each), monthly bandwidth is well under 2 GB. No paid tier required at this scale.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

CachedNetworkImage shows blank white on Supabase private bucket images

Symptom: If gallery images are stored in a private Supabase Storage bucket, CachedNetworkImage HTTP requests return 403 Forbidden — no error is thrown, the widget simply renders blank white where the image should be. This looks identical to a layout bug or a network timeout, making it difficult to diagnose without checking the Supabase Storage bucket policy.

Fix: Use a Supabase public bucket for gallery images. In Supabase Storage, set the bucket visibility to Public and enable public access. If images must stay private (e.g., user-specific protected content), generate signed URLs server-side with a 1-hour expiry and store them temporarily in the Flutter state — regenerate before they expire using a timer or on app foreground event.

FlutterFlow filter chips don't update the grid on deselect

Symptom: FlutterFlow's FilterChip onSelected action fires with a boolean value — true when selecting, false when deselecting. The common mistake is adding the Backend Query refetch action only to the 'true' branch. When a user taps the active chip to clear the filter, the action doesn't run, and the gallery stays filtered instead of showing all images.

Fix: In the FlutterFlow Action Flow editor, add the Backend Query refetch action to both the true and false branches of the onSelected callback. Also update the active category App State variable in both branches — set it to the category slug on true, and clear it to an empty string on false. The Supabase query should omit the .eq() filter when the category variable is empty.

PhotoViewGallery crashes on large unfiltered image sets

Symptom: A common mistake is initializing PhotoViewGallery.builder with the full gallery_images list (potentially hundreds of items) when a user taps any image. On low-RAM Android devices (2–3 GB), loading this many CachedNetworkImage instances into a PageView simultaneously causes high memory pressure and crashes. The crash is device-dependent — it works fine on a modern flagship phone during development.

Fix: Pass only the currently filtered and paginated list of images (the 20-item current page) to PhotoViewGallery. If the user swipes to the edge of the loaded set, load the next page lazily. Alternatively, limit the full-screen gallery to the visible on-screen items plus 5 on each side. This reduces peak memory usage from potentially hundreds of decoded images to under 15.

AnimatedSwitcher flickers when filter chips are tapped rapidly

Symptom: Tapping multiple filter chips quickly in succession queues multiple Supabase Backend Queries. When they return out of order — the 'Nature' query returns after the 'Architecture' query even though Architecture was tapped second — the grid briefly shows Nature content before flipping to Architecture. This looks like a race condition flicker.

Fix: Track the active request with a requestId counter (an integer incremented on each query start). When a query returns, compare its requestId to the current counter; discard the result if the IDs don't match. In FlutterFlow, use a page state integer variable as the requestId and conditionally apply query results only if the returned id matches the current state.

Supabase .contains() on a text column returns no results silently

Symptom: If the tags column was created as a text type (storing comma-separated values like 'nature,outdoor,travel') rather than a proper text[] (array) type, the Supabase .contains('tags', ['nature']) query returns zero results without any error. The tags filter appears to work — no exception, no 400 — but every query returns an empty array.

Fix: Check the tags column type in the Supabase Table Editor (it should show text[], not text). If it is incorrectly typed as text, use .ilike('tags', '%nature%') as an immediate workaround. To fix permanently, run ALTER TABLE gallery_images ADD COLUMN tags_array text[] GENERATED ALWAYS AS (string_to_array(tags, ',')) STORED to add a computed array column, then migrate queries to use the new column.

Best practices

1

Always store both thumbnail_url and image_url separately — loading full-resolution images in the grid burns 5–10x more bandwidth than thumbnails and makes the gallery feel slow even on fast connections

2

Set the category variable in App State (not page state) so the active filter persists when the user navigates to a detail screen and presses back

3

Wrap every grid thumbnail in a Hero widget with a unique tag (the image ID) — the shared-element transition to the full-screen viewer is one of the highest-impact polish details in a gallery app

4

Implement infinite scroll with a bottom-of-list detector rather than a 'Load More' button — users expect continuous scroll in mobile galleries

5

Show the empty state illustration with a specific message like 'No Architecture photos yet' rather than a generic 'Nothing here' — users need to understand whether their filter or search is too narrow

6

Debounce the search query at 400 ms so the gallery doesn't fire a Supabase request on every keystroke — a fast typist otherwise generates 10–15 queries for a 5-character search

7

Use the GIN index on the tags text[] column from the schema — without it, tag searches perform a full table scan and become noticeably slow past a few thousand images

When You Need Custom Development

FlutterFlow covers the core gallery experience — filtered grid, full-screen viewer, search, lazy loading — with minimal custom code. These requirements push beyond it:

  • AI-powered visual similarity search: 'find images similar to this one' using pgvector embeddings stored in Supabase — requires a server-side embedding pipeline (OpenAI CLIP or Google Vision) and pgvector nearest-neighbor queries that FlutterFlow cannot construct visually
  • User-uploaded gallery with image_picker, client-side compression, and watermarking before upload — the image processing pipeline (compress, watermark, generate thumbnail) requires custom Dart in a Custom Action beyond what FlutterFlow can assemble
  • Collections and user-curated albums with drag-to-reorder — drag-and-drop reordering inside a Flutter GridView requires ReorderableGridView and custom sort_order persistence logic that involves complex gesture handling
  • Time-limited signed URLs for DRM-protected images that expire after 1 hour — requires a background refresh mechanism that regenerates URLs before they expire, which involves app lifecycle hooks and background timers beyond FlutterFlow's visual action system

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

How do I add category filter tabs to a Flutter image gallery?

Fetch your categories from a Supabase gallery_categories table and display them as a horizontally scrollable Row of FilterChip widgets. Store the selected category slug in FlutterFlow App State or a page state variable. In each chip's onSelected callback, update the selected category and trigger a re-fetch of gallery_images with .eq('category', selectedCategory). To show 'All' images, use an empty string for the category value and omit the .eq() filter when it's empty.

What Flutter package gives the best masonry grid layout?

flutter_staggered_grid_view (^0.7.x) is the standard choice. Use StaggeredGridView.countBuilder with crossAxisCount: 3 and crossAxisCellCount varying per item to create Pinterest-style variable-height columns. One caveat: this package requires a Custom Widget registration in FlutterFlow. If you don't specifically need masonry, GridView.builder with a fixed childAspectRatio is simpler, requires no Custom Widget in FlutterFlow, and handles most gallery use cases well.

How do I implement a full-screen image viewer with pinch-to-zoom in Flutter?

Use the photo_view package (^0.15.x). For a single image: PhotoView(imageProvider: CachedNetworkImageProvider(imageUrl)). For swipe navigation between images: PhotoViewGallery.builder(itemCount: images.length, builder: (ctx, i) => PhotoViewGalleryPageOptions(imageProvider: CachedNetworkImageProvider(images[i].imageUrl))). Set minScale: PhotoViewComputedScale.contained and maxScale: 4.0 for reasonable zoom limits. In FlutterFlow, register photo_view as a Custom Widget.

How do I lazy load images in a Flutter GridView?

Use CachedNetworkImage (^3.4.x) as the image widget inside each GridView cell. It fetches images on-demand as they scroll into view and caches them to disk so repeat scrolling doesn't re-fetch. Add a placeholder using the placeholder builder with a Shimmer.fromColors widget from the shimmer package — this shows an animated grey shimmer where each image will appear. GridView.builder is inherently lazy (it only builds visible items), so combining it with CachedNetworkImage gives full lazy loading with no additional setup.

Can I filter a Supabase image gallery by multiple categories at once?

Yes. When multiple categories are selected, use Supabase's .in_('category', selectedCategoryList) instead of .eq(). In FlutterFlow, maintain a list variable of selected category slugs; add or remove slugs when chips are tapped; pass the list to the Backend Query's filter. For tag-based multi-filter (where an image can have multiple tags), use .overlaps('tags', selectedTagList) on a text[] column — this returns images that have at least one tag in common with the selected list.

How do I add a search bar to a Flutter image gallery?

Add a Flutter SearchBar widget (Material 3) at the top of the page. In FlutterFlow, connect onChange to an App State variable that holds the search query. Use a Timer to debounce: start a 400 ms timer on each onChange; cancel and restart it if another change fires before it completes; when it fires, re-execute the Backend Query with .ilike('title', '%searchQuery%'). To search tags, add .or('tags.cs.{searchQuery}') to the query — but this requires the tags column to be type text[] and the GIN index to be present.

What's the best way to handle pull-to-refresh in a Flutter grid?

Wrap the GridView in a RefreshIndicator widget. The onRefresh callback must be an async function that returns a Future — FlutterFlow's Backend Query refetch action satisfies this. For infinite scroll galleries, the onRefresh callback must also reset the page counter variable to 0 before calling refetch, otherwise the refresh loads page 2 instead of starting from the beginning. Show a LinearProgressIndicator at the top while refreshing to give users feedback on slower connections.

How do I animate the transition from a thumbnail to a full-screen image in Flutter?

Use Flutter's Hero widget. Wrap the grid thumbnail in Hero(tag: image.id, child: Image(...)) and wrap the full-screen image in Hero(tag: image.id, child: PhotoView(...)) using the same tag. Flutter automatically animates between them when you push the full-screen route. The animation works best with Navigator.push using a MaterialPageRoute — the Hero animation respects the route transition timing. In FlutterFlow, set the Hero tag to the image's unique ID on both the grid item widget and the full-screen page image widget.

RapidDev

Need this feature production-ready?

RapidDev builds a dynamic image gallery with filters into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.