Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Set Up a Virtual Museum Tour in FlutterFlow

Create a virtual museum tour app with room-by-room navigation using PageView, interactive floor plan hotspots using Stack with positioned GestureDetectors, exhibit detail pages with image galleries and audio guides via the just_audio package, and a favorites bookmark system. Use InteractiveViewer for pinch-to-zoom on floor plans so hotspots remain tappable on small screens.

What you'll learn

  • How to build room navigation with PageView and floor plan images
  • How to create tappable exhibit hotspots on a floor plan using Stack and GestureDetector
  • How to add audio guide playback with the just_audio Custom Widget
  • How to implement a self-guided tour route with exhibit favorites and bookmarks
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read25-30 minFlutterFlow Pro+ (Custom Widget for audio player)March 2026RapidDev Engineering Team
TL;DR

Create a virtual museum tour app with room-by-room navigation using PageView, interactive floor plan hotspots using Stack with positioned GestureDetectors, exhibit detail pages with image galleries and audio guides via the just_audio package, and a favorites bookmark system. Use InteractiveViewer for pinch-to-zoom on floor plans so hotspots remain tappable on small screens.

Building a Virtual Museum Tour App in FlutterFlow

Virtual museum tours let users explore exhibits remotely with rich media and audio guides. This tutorial builds a self-paced tour app with room navigation, interactive floor plans with tappable exhibit markers, image galleries, audio narration, and a favorites system — perfect for museums, galleries, and heritage sites.

Prerequisites

  • A FlutterFlow project on the Pro plan (Custom Widget required for audio)
  • Firebase Storage for hosting exhibit images and audio guide files
  • Firestore database for exhibit and room data
  • Floor plan images for each museum room (PNG or JPG)

Step-by-step guide

1

Create the Firestore schema for rooms and exhibits

Create a `rooms` collection with fields: name (String), floorPlanImageUrl (String pointing to Firebase Storage), order (Integer for room sequence), description (String). Create an `exhibits` collection with fields: title (String), description (String), imageUrls (List of Strings for the gallery), audioGuideUrl (String pointing to an MP3 in Storage), roomId (String referencing the room), positionX (Double, percentage 0-1 of floor plan width), positionY (Double, percentage 0-1 of floor plan height), category (String: painting/sculpture/artifact/installation). Add a `favorites` subcollection under each user document with fields: exhibitId (String), savedAt (Timestamp). Register all collections in FlutterFlow's Data panel.

Expected result: Collections for rooms, exhibits, and user favorites are set up with exhibit positions stored as relative coordinates.

2

Build the room navigation with PageView and floor plan display

Create a MuseumTour page. Add a PageView widget at the top half of the screen. Bind it to a Backend Query on the rooms collection ordered by the order field. Each page represents one room. Inside each page, add a Stack widget. The first child of the Stack is an Image widget displaying the room's floorPlanImageUrl. Wrap the entire Stack in an InteractiveViewer Custom Widget to enable pinch-to-zoom so users can zoom into the floor plan on mobile devices. Below the PageView, add a Row with the room name Text and left/right arrow IconButtons for manual navigation between rooms. Add a SmoothPageIndicator (dots) to show the current room position.

Expected result: Users swipe through museum rooms in a PageView, each showing the floor plan image with pinch-to-zoom capability and room name below.

3

Add tappable exhibit hotspots on the floor plan

For each room's Stack, query the exhibits collection filtered by roomId matching the current room. For each exhibit, add a Positioned widget inside the Stack. Set left to positionX times the Stack width and top to positionY times the Stack height. Inside each Positioned, add a GestureDetector wrapping a Container styled as a colored circle (20x20 pixels, with a pulsing animation or a small exhibit icon). On tap, navigate to the ExhibitDetail page passing the exhibitId. Since FlutterFlow's visual builder does not support dynamic Positioned widgets natively, create a Custom Widget that takes the floor plan URL and a list of exhibit hotspot data as parameters, then renders the Stack with Image and Positioned GestureDetectors programmatically.

FloorPlanWithHotspots Custom Widget
1// Custom Widget: FloorPlanWithHotspots
2import 'package:flutter/material.dart';
3
4class FloorPlanWithHotspots extends StatelessWidget {
5 final double width;
6 final double height;
7 final String floorPlanUrl;
8 final List<Map<String, dynamic>> hotspots;
9 final Function(String exhibitId) onHotspotTap;
10
11 const FloorPlanWithHotspots({
12 Key? key,
13 required this.width,
14 required this.height,
15 required this.floorPlanUrl,
16 required this.hotspots,
17 required this.onHotspotTap,
18 }) : super(key: key);
19
20 @override
21 Widget build(BuildContext context) {
22 return InteractiveViewer(
23 minScale: 1.0,
24 maxScale: 3.0,
25 child: SizedBox(
26 width: width,
27 height: height,
28 child: Stack(
29 children: [
30 Image.network(floorPlanUrl,
31 width: width, height: height, fit: BoxFit.contain),
32 ...hotspots.map((h) => Positioned(
33 left: (h['positionX'] as double) * width,
34 top: (h['positionY'] as double) * height,
35 child: GestureDetector(
36 onTap: () => onHotspotTap(h['exhibitId']),
37 child: Container(
38 width: 28, height: 28,
39 decoration: BoxDecoration(
40 color: Colors.red.withOpacity(0.8),
41 shape: BoxShape.circle,
42 border: Border.all(
43 color: Colors.white, width: 2),
44 ),
45 child: const Icon(Icons.museum,
46 color: Colors.white, size: 14),
47 ),
48 ),
49 )),
50 ],
51 ),
52 ),
53 );
54 }
55}

Expected result: Colored hotspot markers appear on the floor plan at each exhibit's position. Tapping a marker navigates to that exhibit's detail page.

4

Create the exhibit detail page with image gallery and audio guide

Create an ExhibitDetail page that receives an exhibitId parameter. Query the exhibits collection for this document. At the top, add a PageView bound to the exhibit's imageUrls array — each page shows a full-width Image with a SmoothPageIndicator below for gallery navigation. Below the gallery, display the exhibit title as a heading Text, category as a styled Chip, and description in a scrollable Text widget. For the audio guide, add a Custom Widget using the just_audio package. Pass the audioGuideUrl as a parameter. The widget renders play/pause IconButton, a Slider for seeking, and a duration Text. Add a heart IconButton in the AppBar for bookmarking — on tap, create or delete a document in the user's favorites subcollection.

Expected result: The exhibit page shows a swipeable image gallery, descriptive text, an audio guide player with play/pause and seek, and a favorite bookmark button.

5

Build the self-guided tour route and favorites list

Add a Guided Tour tab or button on the MuseumTour page. Query all exhibits ordered by roomId then by a tourOrder field (add this to the schema). Display them in a numbered ListView as a suggested walking route. Each item shows the exhibit title, room name, a thumbnail image, and a checkmark that the user taps when they have visited. Track visited exhibits in a Page State list of exhibitIds. Show a LinearPercentIndicator at the top: visitedCount / totalExhibits. For the favorites, add a Favorites page with a GridView bound to the current user's favorites subcollection. Each card shows the exhibit thumbnail and title. Tap to navigate to the exhibit detail.

Expected result: Users can follow a suggested tour route with progress tracking and maintain a personal list of favorite exhibits.

Complete working example

FloorPlanWithHotspots + AudioGuidePlayer Custom Widgets
1// Custom Widget: FloorPlanWithHotspots
2// Renders floor plan image with tappable exhibit markers
3import 'package:flutter/material.dart';
4
5class FloorPlanWithHotspots extends StatelessWidget {
6 final double width;
7 final double height;
8 final String floorPlanUrl;
9 final List<Map<String, dynamic>> hotspots;
10 final Function(String exhibitId) onHotspotTap;
11
12 const FloorPlanWithHotspots({
13 Key? key, required this.width, required this.height,
14 required this.floorPlanUrl, required this.hotspots,
15 required this.onHotspotTap,
16 }) : super(key: key);
17
18 @override
19 Widget build(BuildContext context) {
20 return InteractiveViewer(
21 minScale: 1.0, maxScale: 3.0,
22 child: SizedBox(width: width, height: height,
23 child: Stack(children: [
24 Image.network(floorPlanUrl,
25 width: width, height: height, fit: BoxFit.contain),
26 ...hotspots.map((h) => Positioned(
27 left: (h['positionX'] as double) * width,
28 top: (h['positionY'] as double) * height,
29 child: GestureDetector(
30 onTap: () => onHotspotTap(h['exhibitId']),
31 child: Container(
32 width: 28, height: 28,
33 decoration: BoxDecoration(
34 color: Colors.red.withOpacity(0.8),
35 shape: BoxShape.circle,
36 border: Border.all(color: Colors.white, width: 2)),
37 child: const Icon(Icons.museum, color: Colors.white, size: 14),
38 ),
39 ),
40 )),
41 ]),
42 ),
43 );
44 }
45}
46
47// Custom Widget: AudioGuidePlayer
48// Pubspec: just_audio: ^0.9.36
49import 'package:just_audio/just_audio.dart';
50
51class AudioGuidePlayer extends StatefulWidget {
52 final double width;
53 final double height;
54 final String audioUrl;
55 const AudioGuidePlayer({
56 Key? key, required this.width, required this.height,
57 required this.audioUrl,
58 }) : super(key: key);
59 @override
60 State<AudioGuidePlayer> createState() => _AudioGuidePlayerState();
61}
62
63class _AudioGuidePlayerState extends State<AudioGuidePlayer> {
64 final _player = AudioPlayer();
65 @override
66 void initState() { super.initState(); _player.setUrl(widget.audioUrl); }
67 @override
68 void dispose() { _player.dispose(); super.dispose(); }
69
70 @override
71 Widget build(BuildContext context) {
72 return SizedBox(width: widget.width, child: Row(children: [
73 StreamBuilder<bool>(
74 stream: _player.playingStream,
75 builder: (_, snap) {
76 final playing = snap.data ?? false;
77 return IconButton(
78 icon: Icon(playing ? Icons.pause_circle : Icons.play_circle, size: 40),
79 onPressed: () => playing ? _player.pause() : _player.play(),
80 );
81 }),
82 Expanded(child: StreamBuilder<Duration>(
83 stream: _player.positionStream,
84 builder: (_, snap) {
85 final pos = snap.data ?? Duration.zero;
86 final dur = _player.duration ?? Duration.zero;
87 return Slider(
88 value: pos.inSeconds.toDouble(),
89 max: dur.inSeconds.toDouble().clamp(1, 9999),
90 onChanged: (v) => _player.seek(Duration(seconds: v.toInt())));
91 })),
92 ]));
93 }
94}

Common mistakes

Why it's a problem: Using a single large floor plan image with tiny hotspots that are impossible to tap on mobile

How to avoid: Wrap the floor plan in an InteractiveViewer widget that enables pinch-to-zoom. Make hotspot markers at least 28x28 pixels so they remain tappable even before zooming.

Why it's a problem: Storing hotspot positions as absolute pixel coordinates

How to avoid: Store positions as percentages (0.0 to 1.0) of the image dimensions. Calculate actual position as positionX * containerWidth and positionY * containerHeight.

Why it's a problem: Creating a new AudioPlayer instance every time the exhibit page opens

How to avoid: Dispose the AudioPlayer in the widget's dispose() method. If you want audio to persist across pages, lift the player to a global singleton in App State.

Best practices

  • Use InteractiveViewer for pinch-to-zoom on floor plans to make hotspots accessible on small screens
  • Store hotspot coordinates as percentages for responsive positioning across all device sizes
  • Add a visual pulse animation to hotspot markers so users know they are tappable
  • Preload exhibit images using cached_network_image for smooth gallery swiping
  • Dispose AudioPlayer instances when navigating away to prevent memory leaks and audio overlap
  • Show a tour progress indicator so users know how much of the museum they have explored
  • Add accessibility features: text-to-speech for exhibit descriptions for visually impaired users

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a virtual museum tour app in FlutterFlow. I need room-by-room navigation with PageView, interactive floor plans with tappable exhibit hotspots using Stack and GestureDetector, exhibit detail pages with image galleries and audio guides using just_audio, and a favorites system. Show me the Custom Widget code for the floor plan with hotspots and the audio player.

FlutterFlow Prompt

Create a museum tour page with a PageView of rooms. Each room shows a floor plan image with colored circle markers for exhibits. Tapping a marker navigates to an exhibit detail page with image gallery, description, and an audio playback bar.

Frequently asked questions

Can I add 360-degree panoramic views of museum rooms?

Yes. Use a Custom Widget with the panorama_viewer or photo_view package to display 360-degree images. Embed it in the room page alongside or instead of the static floor plan.

How do I add multiple languages for exhibit descriptions and audio guides?

Store descriptions as a map keyed by language code: {en: 'English text', es: 'Spanish text'}. Store separate audioGuideUrls per language. Read the user's selected language from App State to display the correct version.

Can visitors track which exhibits they have already viewed?

Yes. Store viewed exhibit IDs in a Page State list or Firestore subcollection. Change the hotspot color from red to green for visited exhibits and show a progress percentage.

How do I handle offline mode for the tour?

Pre-download exhibit images and audio files to local storage when the user starts the tour. Use cached_network_image for images and just_audio's caching for audio files. Store exhibit data in local App State.

Can I add augmented reality features to the museum tour?

FlutterFlow has limited AR support. For AR exhibit overlays, you would need a Custom Widget using the ar_flutter_plugin package or export the project and integrate ARCore/ARKit natively.

Can RapidDev help build a museum tour app with advanced features?

Yes. RapidDev can implement 360-degree panoramas, AR exhibit overlays, multi-language support, offline mode, accessibility features, ticketing integration, and analytics for exhibit engagement.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

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.