Build a virtual store in FlutterFlow by storing products in Firestore with a model3dUrl field pointing to .glb files in Firebase Storage. A Custom Widget wraps the model_viewer Flutter package to render interactive 3D models with rotate, zoom, and pan controls. Color variant chips swap the model and textures. An AR try-on button uses model_viewer's built-in AR mode on supported devices.
E-Commerce with Interactive 3D and AR Product Views
Standard product images show one angle. A 3D product viewer lets customers rotate, zoom, and inspect a product from every side — dramatically reducing return rates for items like furniture, shoes, and electronics. This tutorial adds a 3D product viewer to a FlutterFlow store app. Products are stored in Firestore with a model3dUrl field. A Custom Widget wrapping the model_viewer package renders .glb 3D files with built-in rotate, zoom, and auto-rotation. Color variant chips update both the 3D model URL and product images simultaneously. On Android and iOS devices with ARCore or ARKit support, an AR button places the product in the real world using model_viewer's built-in AR capabilities.
Prerequisites
- FlutterFlow Pro account with Firebase Storage and Firestore enabled
- At least one .glb 3D model file (free models available at sketchfab.com or poly.pizza)
- Basic familiarity with FlutterFlow Custom Widgets
- A Firestore products collection with at least one product document
Step-by-step guide
Set Up the Firestore Product Schema with 3D Model Fields
Set Up the Firestore Product Schema with 3D Model Fields
In Firestore, open your products collection (or create it) and add these fields to each product document: model3dUrl (String — Firebase Storage URL to the .glb file), variants (Array of Maps — each map has color (String), hexCode (String), model3dUrl (String for this color variant's model), imageUrls (Array of Strings for 2D photos of this variant)), isArEnabled (Boolean — whether AR is available), modelSizeKb (Integer — store the file size to warn users on mobile data). Upload your .glb files to Firebase Storage under a models/ folder. In FlutterFlow's Firestore panel, import the updated schema. Keep .glb files under 5MB — use tools like gltf-transform or Blender to compress models before uploading.
Expected result: Products in Firestore have model3dUrl fields pointing to .glb files in Firebase Storage. FlutterFlow shows the updated ProductsRecord Document Type.
Build the 3D Viewer Custom Widget
Build the 3D Viewer Custom Widget
In FlutterFlow, go to Custom Widgets and create a widget named ProductViewer3D. In the pubspec dependencies, add model_viewer_plus: ^1.6.0. The widget accepts parameters: modelUrl (String — the Firebase Storage URL to the .glb file), autoRotate (Boolean, default true), backgroundColor (Color). The widget renders a ModelViewer widget from the model_viewer_plus package. Set the src property to the modelUrl, enable ar mode, set autoRotate and autoRotateDelay. The ModelViewer widget renders the .glb file using the device's WebView (Android) or Quick Look (iOS) with built-in touch controls for rotate, zoom, and pan. Place this Custom Widget in the product detail page's upper section with a fixed height of 350px.
1// Custom Widget: ProductViewer3D2// pubspec: model_viewer_plus: ^1.6.03import 'package:flutter/material.dart';4import 'package:model_viewer_plus/model_viewer_plus.dart';56class ProductViewer3D extends StatefulWidget {7 final String modelUrl;8 final bool autoRotate;9 final Color backgroundColor;10 final double width;11 final double height;1213 const ProductViewer3D({14 Key? key,15 required this.modelUrl,16 this.autoRotate = true,17 this.backgroundColor = Colors.white,18 required this.width,19 required this.height,20 }) : super(key: key);2122 @override23 State<ProductViewer3D> createState() => _ProductViewer3DState();24}2526class _ProductViewer3DState extends State<ProductViewer3D> {27 @override28 Widget build(BuildContext context) {29 if (widget.modelUrl.isEmpty) {30 return Container(31 width: widget.width,32 height: widget.height,33 color: widget.backgroundColor,34 child: const Center(child: Text('No 3D model available')),35 );36 }37 return SizedBox(38 width: widget.width,39 height: widget.height,40 child: ModelViewer(41 src: widget.modelUrl,42 ar: true,43 arModes: const ['scene-viewer', 'webxr', 'quick-look'],44 autoRotate: widget.autoRotate,45 autoRotateDelay: 2000,46 cameraControls: true,47 backgroundColor: Color.fromARGB(48 widget.backgroundColor.alpha,49 widget.backgroundColor.red,50 widget.backgroundColor.green,51 widget.backgroundColor.blue,52 ),53 shadowIntensity: 1,54 shadowSoftness: 1,55 ),56 );57 }58}Expected result: The product detail page shows the 3D model rotating automatically. Users can touch to rotate manually, pinch to zoom, and drag to pan.
Implement Color Variant Switching
Implement Color Variant Switching
On the product detail page, below the 3D viewer, add a Row of color chip buttons. Each chip is a GestureDetector wrapping a Container with the variant's hexCode as its background color and a checkmark overlay for the selected variant. Create a Page State variable selectedVariantIndex (Integer, default 0). Bind the ProductViewer3D widget's modelUrl parameter to the currently selected variant's model3dUrl: use a Custom Function named getVariantModelUrl that takes the variants list and selectedVariantIndex and returns the model3dUrl string. When a color chip is tapped, update selectedVariantIndex. The 3D viewer, main product image, and price will all update via their data bindings. If a variant does not have its own .glb model, fall back to the base product's model3dUrl.
Expected result: Tapping a color chip updates the 3D model to that variant's version (if available) and changes the product images to match.
Add AR Try-On Mode
Add AR Try-On Mode
The model_viewer_plus package's ar: true parameter automatically adds an AR button to the 3D viewer on supported devices. On Android, it uses Google Scene Viewer via the scene-viewer AR mode. On iOS, it uses Apple Quick Look via the quick-look AR mode. These are OS-level AR experiences — no custom AR code required. Add a separate View in AR button below the 3D viewer that is conditionally visible only when the product's isArEnabled field is true. When tapped, call a Custom Action named launchArViewer that opens the .glb URL directly using the launch URL action with the special Google Scene Viewer URL format for Android and the .usdz file URL for iOS. Note that AR works best for physical products with accurate real-world dimensions — set the scale attribute in ModelViewer to match actual product dimensions.
Expected result: On ARCore/ARKit-enabled devices, the AR button places the product in the camera view at real-world scale. The OS AR viewer opens natively.
Optimize 3D Models for Mobile Performance
Optimize 3D Models for Mobile Performance
Unoptimized 3D models are the most common cause of poor performance in 3D stores. Use Blender (free) or the gltf-transform CLI to compress .glb files before uploading. Target file sizes: under 2MB for mobile, under 5MB for tablet/desktop. In Blender, use Draco compression when exporting to .glb — this reduces file sizes by 60-80% with minimal visual quality loss. In FlutterFlow, show the model file size (from the modelSizeKb Firestore field) as a download notice if it exceeds 3MB: 'Large 3D model (4.2MB) — load anyway?' with Yes/No buttons. Cache the loaded model by keeping the ProductViewer3D widget mounted in the widget tree rather than rebuilding it on tab switch.
Expected result: 3D models load within 2-3 seconds on a 4G connection. Models under 2MB load without a warning prompt.
Complete working example
1// Full Custom Widget: ProductViewer3D for FlutterFlow2// pubspec.yaml dependencies:3// model_viewer_plus: ^1.6.045import 'package:flutter/material.dart';6import 'package:model_viewer_plus/model_viewer_plus.dart';78class ProductViewer3D extends StatefulWidget {9 final String modelUrl;10 final bool autoRotate;11 final Color backgroundColor;12 final bool arEnabled;13 final double width;14 final double height;15 final Future<dynamic> Function()? onArLaunched;1617 const ProductViewer3D({18 Key? key,19 required this.modelUrl,20 this.autoRotate = true,21 this.backgroundColor = Colors.grey,22 this.arEnabled = false,23 required this.width,24 required this.height,25 this.onArLaunched,26 }) : super(key: key);2728 @override29 State<ProductViewer3D> createState() => _ProductViewer3DState();30}3132class _ProductViewer3DState extends State<ProductViewer3D> {33 bool _isLoading = true;3435 @override36 void initState() {37 super.initState();38 // Simulate model load time for UX feedback39 Future.delayed(const Duration(seconds: 3), () {40 if (mounted) setState(() => _isLoading = false);41 });42 }4344 @override45 void didUpdateWidget(covariant ProductViewer3D oldWidget) {46 super.didUpdateWidget(oldWidget);47 if (widget.modelUrl != oldWidget.modelUrl) {48 setState(() => _isLoading = true);49 Future.delayed(const Duration(seconds: 3), () {50 if (mounted) setState(() => _isLoading = false);51 });52 }53 }5455 @override56 Widget build(BuildContext context) {57 return SizedBox(58 width: widget.width,59 height: widget.height,60 child: Stack(61 children: [62 if (widget.modelUrl.isNotEmpty)63 ModelViewer(64 src: widget.modelUrl,65 ar: widget.arEnabled,66 arModes: const ['scene-viewer', 'webxr', 'quick-look'],67 autoRotate: widget.autoRotate,68 autoRotateDelay: 1500,69 cameraControls: true,70 shadowIntensity: 0.8,71 shadowSoftness: 0.8,72 exposure: '1.0',73 backgroundColor: Color.fromARGB(74 widget.backgroundColor.alpha,75 widget.backgroundColor.red,76 widget.backgroundColor.green,77 widget.backgroundColor.blue,78 ),79 )80 else81 Container(82 color: Colors.grey.shade200,83 child: const Center(84 child: Icon(Icons.view_in_ar_outlined, size: 64, color: Colors.grey),85 ),86 ),87 if (_isLoading)88 Container(89 color: Colors.black26,90 child: const Center(91 child: Column(92 mainAxisAlignment: MainAxisAlignment.center,93 children: [94 CircularProgressIndicator(color: Colors.white),95 SizedBox(height: 12),96 Text('Loading 3D model...', style: TextStyle(color: Colors.white)),97 ],98 ),99 ),100 ),101 ],102 ),103 );104 }105}Common mistakes when creating a Virtual Store with a 3D Product Viewer in FlutterFlow
Why it's a problem: Loading unoptimized 50MB+ 3D models directly from Firebase Storage
How to avoid: Compress all 3D models to under 5MB using Blender's Draco compression or the gltf-transform CLI before uploading. Store the file size in Firestore and warn users before loading large models.
Why it's a problem: Rebuilding the ProductViewer3D Custom Widget every time the parent page rebuilds
How to avoid: Wrap the ProductViewer3D widget in a FlutterFlow Component and keep it mounted. Use the modelUrl parameter change (via didUpdateWidget) to detect model switches rather than rebuilding the widget from scratch.
Why it's a problem: Assuming AR mode works on all devices without checking hardware support
How to avoid: Set isArEnabled per product in Firestore and conditionally show the AR button. Add a Custom Function that checks the device's platform version and shows a 'AR not available on this device' message when appropriate.
Best practices
- Compress all .glb files to under 5MB before uploading — use Blender with Draco compression or gltf-transform.
- Show a loading indicator for the first 3 seconds while the model initializes in the WebView.
- Store modelSizeKb in Firestore and warn users before loading models over 10MB on mobile data.
- Validate .glb files with the Khronos glTF Validator before uploading to catch rendering issues early.
- Provide 2D image fallbacks for products that do not yet have 3D models using Conditional Visibility.
- Set real-world scale dimensions in ModelViewer using the scale attribute so AR mode places objects at the correct size.
- Cache the page state after loading a model so navigating back to a product does not re-download the .glb file.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm adding a 3D product viewer to my FlutterFlow store app using the model_viewer_plus Flutter package. The product's .glb file URL is stored in Firestore. Write a complete Flutter Custom Widget class called ProductViewer3D that takes modelUrl (String), autoRotate (Boolean), and arEnabled (Boolean) parameters. Include a loading overlay, error handling for empty URLs, and proper AR mode configuration for both Android and iOS.
Create a FlutterFlow Custom Widget named ProductViewer3D using the model_viewer_plus package. The widget should display a rotating 3D model from a Firebase Storage URL. Add a loading indicator that shows for 3 seconds while the model initializes. Include ar:true in the ModelViewer configuration for AR try-on support.
Frequently asked questions
Which 3D file formats does model_viewer_plus support?
model_viewer_plus supports .glb and .gltf files. For iOS AR mode (Quick Look), you also need a .usdz version of the model. Use Blender or Reality Converter (macOS) to convert .glb to .usdz. Store both URLs in Firestore and serve the appropriate format based on the device platform.
Where can I get free 3D models for testing?
Sketchfab (sketchfab.com) offers thousands of free downloadable .glb models. Poly Pizza (poly.pizza) has simple low-poly models ideal for mobile. Google's model-viewer examples repo on GitHub includes optimized sample .glb files. Always check the license before using models in a commercial app.
Does the 3D viewer work in FlutterFlow web apps?
model_viewer_plus works on Flutter Web by using the model-viewer web component from Google. You need to add the model-viewer script tag to your web/index.html file. AR mode does not work in web browsers — it requires native iOS or Android app installation.
How do I add lighting and environment maps to the 3D viewer?
ModelViewer supports an environmentImage property that accepts an HDR image URL for lighting. Set skyboxImage for a visible 360-degree background, or set environmentImage with neutralBackground for product-style lighting. High-quality .hdr environment maps are available free at Poly Haven (polyhaven.com).
Can I let users take screenshots of the 3D model?
model_viewer_plus does not expose a screenshot API directly. Use Flutter's RepaintBoundary widget wrapped around the ModelViewer and call toImage() to capture the current frame. Alternatively, use the screenshot package to capture the widget area. Note that capturing WebView content may have limitations on some Android versions.
How do I handle devices where the 3D viewer crashes or shows a blank screen?
Wrap the ProductViewer3D widget in a try-catch error boundary using FlutterFlow's Custom Widget error handling. Provide a fallback Image widget that shows the 2D product photos when the 3D viewer fails. Track crash rates by logging failures to Firestore or Firebase Crashlytics.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation