flutter_media_viewer
A cached image and video gallery viewer for Flutter with Hero transitions, playback controls, thumbnails, zoom, swipe-to-dismiss, and metadata details. Video playback uses Orvexa Player while retaining the viewer's compact playback controls and gallery gestures.
Features
- Displays images and videos in a full-screen gallery.
- Opens with a Hero transition from a ready-to-use preview widget.
- Shares cached network images between previews and the viewer.
- Shows centered image download progress while retaining local placeholders.
- Supports network URLs, file-system paths, and
file:URIs. - Plays MP4 and HLS with compact play, pause, seek, mute, autoplay, and looping controls.
- Shows available video durations on previews and gallery thumbnails.
- Supports horizontal paging and a centered thumbnail strip.
- Supports pinch and animated double-tap zoom for images and videos.
- Displays Live Photos as still images with hold-to-play motion and a LIVE badge.
- Dismisses with an animated downward swipe.
- Shows structured technical and location metadata.
- Exposes optional share, delete, header, details, and custom actions.
- Opens as a route or a full-page modal bottom sheet.
- Accepts custom content and thumbnail builders.
- Inherits the active application theme and supports scoped viewer themes.
Installation
flutter pub add flutter_media_viewer
Import the public library:
import 'package:flutter_media_viewer/flutter_media_viewer.dart';
Requirements
Use a Flutter toolchain that satisfies Flutter >=3.38.1 and Dart ^3.12.2.
Native video playback uses Orvexa's platform backends:
- Android requires minimum SDK 21, compile SDK 36 or later, Java 17, and the
android.permission.INTERNETpermission for remote media. - iOS requires iOS 13 or later and CocoaPods for Orvexa's native plugin. Flutter projects using Swift Package Manager can use Flutter's CocoaPods fallback.
- macOS requires the
com.apple.security.network.cliententitlement when the sandbox is enabled and remote media is used. - Windows bundles its native playback libraries; Linux requires
libmpv. - Web uses the browser's supported codecs and HLS playback. Media servers must allow cross-origin access; HTTPS pages require HTTPS media. File-system paths and arbitrary request headers are available only on native platforms.
See Orvexa's platform requirements and native library notices for platform setup and codec details. The host app remains responsible for its media URLs and authorization.
Quick Start
Create one or more media items and render a FlutterMediaViewerPreview:
final List<FlutterMediaViewerItem> items = <FlutterMediaViewerItem>[
const FlutterMediaViewerItem.image(
id: 'photo-1',
source: 'https://example.com/photo.jpg',
thumbnailSource: 'https://example.com/photo-thumbnail.jpg',
aspectRatio: 4 / 3,
title: 'Coast',
subtitle: 'Sunday, 4:53 PM',
),
];
FlutterMediaViewerPreview(
items: items,
initialIndex: 0,
borderRadius: BorderRadius.circular(12),
fit: BoxFit.cover,
);
FlutterMediaViewerPreview renders the selected item, owns the Hero transition,
and opens the viewer when tapped. Set useModalBottomSheet to false to open it
as a route instead. Previews use a 3 / 4 aspect ratio by default. Override the
preview independently for square grids or other layouts without changing the
media's intrinsic viewer ratio:
FlutterMediaViewerPreview(
items: items,
aspectRatio: 1,
);
Open Manually
Use showModal when the preview already handles its own tap gesture:
await FlutterMediaViewer.showModal<void>(
context,
items: items,
initialIndex: selectedIndex,
config: FlutterMediaViewerConfig(
showThumbnails: items.length > 1,
closeTooltip: 'Close',
),
);
Use FlutterMediaViewer.show for route presentation, or place
FlutterMediaViewer directly in an existing widget tree.
Fullscreen And System Bars
On iOS and supported Android configurations, hiding the viewer controls also requests immersive fullscreen. Showing controls, beginning a dismissal gesture, covering the viewer with another route, or closing it restores the system bars. A cancelled dismissal returns to fullscreen if the controls were hidden.
manageSystemUi defaults to true. Hosts that manage system bars themselves can
disable it. Visible controls and dismissal use restoreSystemUiMode, which
defaults to SystemUiMode.edgeToEdge; supply a different mode and, for manual
mode, restoreSystemUiOverlays when needed. Flutter does not expose the previous
system UI mode for automatic restoration. Desktop and web window modes are not
changed.
Android can restrict fullscreen requests: apps targeting API 35 require the appropriate edge-to-edge migration, and API 36 or later ignores modes other than edge-to-edge. See Flutter's system UI documentation.
Video
Create a video item with its source and an optional thumbnail:
const FlutterMediaViewerItem.video(
id: 'video-1',
source: 'https://example.com/video.mp4',
thumbnailSource: 'https://example.com/video-thumbnail.jpg',
aspectRatio: 9 / 16,
);
When no poster image is supplied, built-in previews and gallery thumbnails use a
muted, paused first frame through Orvexa. This does not start
playback. It may load video data and allocate a decoder, so supplying
thumbnailSource or thumbnailLocalPlaceholderSource is recommended for large
galleries. Hidden gallery strips release their fallback players.
A supplied preview placeholder is kept instead of loading a first frame, so
the preview does not create a video player or request video data. Custom
thumbnailBuilder widgets also take precedence in previews and gallery strips.
The built-in player pauses when its page is no longer active. Playback behavior
is configured through FlutterMediaViewerConfig:
const FlutterMediaViewerConfig(
enableZoom: false,
enableVideoAutoplay: true,
enableVideoLooping: true,
showVideoControls: true,
startVideosMuted: true,
);
Disable enableZoom for custom gesture-heavy content when needed.
Built-in videos start muted by default; set startVideosMuted to
false when sound should start immediately.
The original compact control bar remains the default: play/pause, seek progress, and mute. Existing icon builders and tooltips still customize these controls. Video surfaces use the gallery's layout, gestures, and system UI behavior.
Live Photos
Provide an independently displayable still image and its paired motion file:
const FlutterMediaViewerItem.livePhoto(
id: 'live-photo-1',
source: 'https://example.com/photo.jpg',
motionSource: 'https://example.com/motion.mov',
thumbnailSource: 'https://example.com/thumbnail.jpg',
aspectRatio: 3 / 4,
);
The preview shows a compact Live Photo symbol instead of a play button or duration. Tap that symbol to play motion without opening the viewer. Long presses remain available to the enclosing application's context menu. The thumbnail navigation strip stays still and does not create players.
The viewer plays motion once on opening, independently of video autoplay.
Set enableLivePhotoAutoplay: false to require a long press instead. Holding the
photo replays its motion; releasing it restores the still image. The LIVE menu
supports Live, Loop, and Off. Motion is muted and pauses when the page or app
becomes inactive. motionLocalPlaceholderSource
can point to a downloaded motion file while motionSource retains the remote
fallback. The host remains responsible for authorization, download, and saving
the original pair to the platform photo library.
Use livePhotoMotionLoader to resolve a private or expired motion URL to an
authorized URL or local file URI when playback is requested. It runs for each
active item, not only the initial gallery item. The still remains visible while
resolution is pending; failed resolution can be retried by replaying the photo.
Use livePhotoModePicker to present your own menu and return a
FlutterMediaViewerLivePhotoMode, or null on dismissal. Without an override,
the package presents a Cupertino action sheet. Loop repeats the motion file;
Bounce and Long Exposure are not offered because they require additional media
processing. Labels are configurable through livePhotoLabel,
livePhotoLoopLabel, and stillPhotoLabel.
Zoom hides all viewer controls until the original scale is restored, preserving whether controls were visible before zooming.
Cached Previews And Placeholders
Use FlutterMediaPreview when only the media preview is needed. Use
FlutterMediaHero to wrap a completely custom preview while retaining the
viewer transition.
Each item can define cache keys, request headers, and file-system placeholders:
const FlutterMediaViewerItem.image(
id: 'photo-2',
source: 'https://example.com/photo-2.jpg',
thumbnailSource: 'https://example.com/photo-2-thumbnail.jpg',
localPlaceholderSource: '/temporary/photo-2.jpg',
thumbnailLocalPlaceholderSource: '/temporary/photo-2-thumbnail.jpg',
cacheKey: 'photo-2',
thumbnailCacheKey: 'photo-2-thumbnail',
httpHeaders: <String, String>{
'Authorization': 'Bearer token',
},
);
The local placeholder remains visible while the remote source loads and fades smoothly into the cached remote image.
Image loading shows a centered progress indicator on opening and paging. When
the image stream reports its total download size, the indicator reflects received
bytes; otherwise it remains indeterminate until the image is decoded. Cached
images do not restart a loading animation on ordinary rebuilds. loadingBuilder
can replace the default indicator without removing the underlying placeholder.
Theming
The viewer inherits the active ThemeData.colorScheme by default, including
the application's current light or dark mode.
Use themeMode only when a viewer must use a specific brightness independently
from the surrounding application:
const FlutterMediaViewerConfig(
themeMode: ThemeMode.dark,
);
Use FlutterMediaViewerThemeData for a complete per-viewer color scheme:
const FlutterMediaViewerConfig(
theme: FlutterMediaViewerThemeData(
colorScheme: ColorScheme.dark(
primary: Color(0xFF8AB4F8),
),
mediaBackgroundColor: Colors.black,
),
);
Add the same extension to an application's ThemeData.extensions to configure
all viewers under that theme:
ThemeData(
colorScheme: const ColorScheme.dark(),
extensions: const <ThemeExtension<dynamic>>[
FlutterMediaViewerThemeData(
colorScheme: ColorScheme.dark(),
),
],
);
The optional backgroundColor and controlsBackgroundColor configuration
values remain available as focused overrides. Leaving them unset allows the
viewer theme and application color scheme to resolve them.
Relative Sources
Absolute HTTP(S) URLs are recommended. Relative sources can be resolved with
sourceUrlBuilder:
final FlutterMediaViewerConfig config = FlutterMediaViewerConfig(
sourceUrlBuilder: (String source) {
return Uri.parse('https://cdn.example.com/').resolve(source).toString();
},
);
The same resolver is used for full media and thumbnail sources.
Metadata
Use FlutterMediaViewerMetadata for the built-in details panel:
const FlutterMediaViewerItem.image(
id: 'photo-3',
source: 'https://example.com/photo-3.jpg',
metadata: FlutterMediaViewerMetadata(
dateText: 'Sunday, September 6, 2026 at 4:53 PM',
fileName: 'IMG_3957.HEIC',
device: 'Phone camera',
format: 'HEIF',
camera: 'Wide camera',
summary: '12 MP - 3024 x 4032 - 3.3 MB',
badges: <String>['LIVE'],
exposure: <String>['ISO 125', '26 mm', 'f/1.8', '1/97 s'],
locationName: 'Coast',
latitude: 37.77,
longitude: -122.42,
),
);
All metadata values are display-ready. Empty fields and sections are omitted. The package validates coordinates but does not extract EXIF data, request location permission, geocode coordinates, or select a map provider.
Use locationPreviewBuilder to render a map preview and onLocationPressed to
handle the location row. Use detailsBuilder to replace the complete details
panel.
A Map<String, String> can be supplied as metadata when a simple list of
localized labels and values is sufficient.
Optional Actions
Share and delete controls are shown only when their callbacks are configured:
final FlutterMediaViewerConfig config = FlutterMediaViewerConfig(
onSharePressed: handleShare,
onDeletePressed: handleDelete,
onMorePressed: handleMore,
);
Callbacks receive the active FlutterMediaViewerOverlayInfo. File sharing,
confirmation, and deletion remain explicit so the package never changes stored
media by itself.
The package uses Hugeicons and a platform-aware BackButtonIcon as fallbacks.
Every built-in action icon can be replaced without rebuilding the action bars:
final FlutterMediaViewerConfig config = FlutterMediaViewerConfig(
onSharePressed: handleShare,
closeIconBuilder: (BuildContext context) => const Icon(Icons.arrow_back),
shareIconBuilder: (BuildContext context) => const Icon(Icons.ios_share),
infoIconBuilder: (BuildContext context) => const Icon(Icons.info_outline),
deleteIconBuilder: (BuildContext context) => const Icon(Icons.delete_outline),
playIconBuilder: (BuildContext context) => const Icon(Icons.play_arrow),
pauseIconBuilder: (BuildContext context) => const Icon(Icons.pause),
muteIconBuilder: (BuildContext context) => const Icon(Icons.volume_off),
unmuteIconBuilder: (BuildContext context) => const Icon(Icons.volume_up),
);
Configuration
| Option | Default | Description |
|---|---|---|
showControlsInitially |
true |
Shows controls when the viewer opens. |
showThumbnails |
true |
Shows thumbnail navigation for galleries. |
showHeader |
true |
Shows non-empty item titles and subtitles. |
showCloseButton |
true |
Shows the platform-aware back button. |
enableSwipeDownToDismiss |
true |
Enables downward drag dismissal. |
backgroundColor |
null |
Overrides the media background, which defaults to black. |
controlsBackgroundColor |
null |
Overrides the themed controls and details surface. |
themeMode |
null |
Optionally forces light, dark, or system brightness. |
theme |
null |
Overrides the inherited FlutterMediaViewerThemeData. |
constrainedMediaBorderRadius |
24 px |
Radius applied only when tall media is constrained. |
imageFit |
BoxFit.contain |
Fit used by built-in media. |
thumbnailFit |
BoxFit.cover |
Fit used by built-in thumbnails. |
enableZoom |
true |
Enables InteractiveViewer. |
minScale |
1 |
Minimum zoom scale. |
maxScale |
4 |
Maximum zoom scale. |
enableVideoAutoplay |
true |
Autoplays the active video. |
enableVideoLooping |
true |
Loops built-in videos. |
showVideoControls |
true |
Shows playback, timeline, and mute controls. |
startVideosMuted |
true |
Starts built-in videos muted. |
showBottomActionBar |
true |
Allows configured bottom actions. |
sourceUrlBuilder |
null |
Resolves relative sources. |
topActionsBuilder |
null |
Builds additional top actions. |
bottomActionsBuilder |
null |
Builds additional bottom actions. |
onSharePressed |
null |
Shows and handles the share action. |
onDeletePressed |
null |
Shows and handles the delete action. |
onMorePressed |
null |
Shows the upper-right menu button; the host supplies its actions. |
moreTooltip |
More |
Accessible menu button label. |
livePhotoLabel / stillPhotoLabel |
LIVE / Still |
Localized Live Photo mode labels. |
livePhotoLoopLabel |
Loop |
Localized looping mode label. |
enableLivePhotoAutoplay |
true |
Plays active Live motion once, independently of video autoplay. |
livePhotoMotionLoader |
null |
Lazily resolves an item's motion to an authorized URL or local file URI. |
livePhotoModePicker |
null |
Replaces the Live mode action sheet with an application-owned menu. |
loadingBuilder |
null |
Replaces the built-in video loading indicator. |
detailsBuilder |
null |
Replaces the details panel. |
locationPreviewBuilder |
null |
Builds an optional location preview. |
onLocationPressed |
null |
Handles the location row. |
The close, share, info, delete, play, pause, muted, and unmuted icons each have
an optional WidgetBuilder in FlutterMediaViewerConfig. Leaving a builder
unset keeps the package fallback icon.
All visible labels and tooltips can be supplied through
FlutterMediaViewerConfig for localization.
Custom Content
Use the base FlutterMediaViewerItem constructor with contentBuilder and
thumbnailBuilder for unsupported formats, DRM, or specialized rendering.
Built-in image and video rendering is used when those builders are omitted.
Layout Behavior
Pass the media width-to-height aspectRatio whenever it is available. The
built-in image and video renderers can resolve it after loading when omitted.
Tall media is constrained only when its full-width height would overlap the top
or bottom controls. Its intrinsic aspect ratio determines the resulting width,
with rounded corners and a subtle border in that constrained state. Media that
fits remains full width. Opening details expands the active media over the
available upper area with BoxFit.cover; closing details restores its previous
frame and fit. Hiding the controls removes the constrained border and radius,
expands the media canvas to the available viewer area, and uses
BoxFit.contain for both images and videos.
Example
The example directory demonstrates images, videos, metadata, thumbnails,
custom builders, actions, and both presentation modes.
License
MIT License. See LICENSE.