Add AR product visualization to FlutterFlow by using model_viewer_plus as a Custom Widget for in-app 3D model previewing in a web view, and ar_flutter_plugin for placing products in the user's physical space using ARKit and ARCore. Store Draco-compressed .glb model files in Firebase Storage. Support color variants by swapping material URLs and add a size reference overlay.
AR Product Visualization: 3D Preview and Real-World Placement
AR product displays let shoppers visualize how furniture, appliances, or decorative items will look in their space before buying. FlutterFlow implements this in two tiers: model_viewer_plus provides a 3D orbit viewer (spin, zoom, inspect from all angles) inside a WebView that works without AR hardware, and ar_flutter_plugin enables full ARKit/ARCore floor detection and model placement for supported devices. Both require 3D models in .glb (GL Binary) format — the most common mistake is using .obj or .fbx files, which neither library supports. Models must be compressed with Draco to keep file sizes under 5MB for fast loading.
Prerequisites
- FlutterFlow Pro plan with code export enabled
- model_viewer_plus and ar_flutter_plugin in pubspec.yaml
- 3D product models in Draco-compressed .glb format (exported from Blender or downloaded from Sketchfab)
- .glb files uploaded to Firebase Storage
- iOS: ARKit-capable device (iPhone 6s+). Android: ARCore-supported device
Step-by-step guide
Prepare and upload .glb model files to Firebase Storage
Prepare and upload .glb model files to Firebase Storage
Every AR product model must be in .glb (GL Binary) format — a binary-encoded version of glTF 2.0. Open your 3D model in Blender (free) and export as glTF Binary (.glb). Before exporting, apply Draco mesh compression: in Blender's glTF export panel, expand the Geometry section and enable Draco Mesh Compression. This typically reduces file size by 60-80% (a 20MB model becomes 4MB). Aim for models under 5MB for fast loading. Upload the .glb files to Firebase Storage under a path like products/[product_id]/model.glb. For color variants, create separate .glb files per color or modify the material URL dynamically. Store each variant's Storage URL in the Firestore product document as a map: model_urls.red, model_urls.blue, model_urls.natural.
Expected result: Firebase Storage shows .glb files under products/[id]/ with file sizes under 5MB each; download URLs are valid and accessible.
Build the 3D viewer custom widget with model_viewer_plus
Build the 3D viewer custom widget with model_viewer_plus
Create a Custom Widget named ProductViewerWidget. It accepts modelUrl (String), backgroundColor (Color), and autoRotate (bool) parameters. Import model_viewer_plus and use the ModelViewer widget which renders Google's model-viewer web component inside a WebView. Set the src parameter to the modelUrl. Enable auto-rotate, set camera-controls to true, and configure shadow-intensity for a realistic floor shadow. The model_viewer_plus widget works on iOS, Android, and Web without any AR hardware requirement — it is a 3D orbit viewer. Add a 360° rotate icon button overlay that toggles autoRotate to help users discover they can interact with the model. This is the fallback experience for users on devices that do not support ARKit/ARCore.
1import 'package:flutter/material.dart';2import 'package:model_viewer_plus/model_viewer_plus.dart';34class ProductViewerWidget extends StatefulWidget {5 final String modelUrl;6 final bool autoRotate;7 const ProductViewerWidget({8 super.key,9 required this.modelUrl,10 this.autoRotate = false,11 });12 @override13 State<ProductViewerWidget> createState() => _ProductViewerWidgetState();14}1516class _ProductViewerWidgetState extends State<ProductViewerWidget> {17 bool _autoRotate = false;1819 @override20 void initState() {21 super.initState();22 _autoRotate = widget.autoRotate;23 }2425 @override26 Widget build(BuildContext context) {27 return Stack(children: [28 ModelViewer(29 src: widget.modelUrl,30 alt: '3D product model',31 ar: true,32 autoRotate: _autoRotate,33 cameraControls: true,34 shadowIntensity: 0.8,35 backgroundColor: Colors.white,36 ),37 Positioned(38 bottom: 16, right: 16,39 child: FloatingActionButton.small(40 onPressed: () => setState(() => _autoRotate = !_autoRotate),41 child: Icon(_autoRotate ? Icons.pause : Icons.threesixty),42 ),43 ),44 ]);45 }46}Expected result: The 3D viewer renders the .glb model with orbit controls; spinning it shows the model from all angles.
Add AR placement with ar_flutter_plugin
Add AR placement with ar_flutter_plugin
Create a second Custom Widget named ARPlacementWidget. It accepts modelUrl (String) and scaleMeters (double — the real-world scale of the model in metres). Import ar_flutter_plugin and implement an ArSessionManager with a hit-test placement strategy. When the user taps the floor plane detected by ARKit/ARCore, an ArObject is added to the scene at the tapped position, with the modelUrl loaded from Firebase Storage. Add a placement instruction overlay at the top of the screen: 'Move your phone slowly to detect a flat surface, then tap to place the product.' Show this instruction until the first surface is detected (ArSessionManager.onPlaneDetected callback). Add a Remove button that calls ArObjectManager.removeNode(lastPlacedNode) to clear the placed model.
Expected result: On an ARKit/ARCore supported device, a flat surface is detected (shown by a plane grid overlay), and tapping places the 3D product model on the floor.
Implement color variant switching
Implement color variant switching
In the FlutterFlow page containing the ProductViewerWidget, add a horizontal Row of color swatch buttons (small CircleAvatars with each color). Store the available colors and their model URLs from the Firestore product document's model_urls map field. Bind each swatch button to an Action Flow that updates a page state variable selectedModelUrl. The ProductViewerWidget's modelUrl parameter is bound to this page state variable. When the user taps a different color swatch, the page state updates and the ModelViewer reloads with the new model URL. Add a brief fade animation on the viewer to signal the model is changing. If color variants are just material/texture changes (not shape changes), consider a single base model with environment lighting that makes color variants look accurate.
Expected result: Tapping a color swatch reloads the 3D viewer with the matching colored model within 1-2 seconds.
Add a size reference overlay in AR mode
Add a size reference overlay in AR mode
A size reference helps users understand the product's real-world dimensions. When the ARPlacementWidget is active, show a semi-transparent overlay card at the bottom of the screen displaying: the product's dimensions (width x depth x height in cm) loaded from Firestore, and a comparison like 'About the size of a standard dining table.' Add a ruler icon button that places a 1-metre reference cube next to the product in the AR scene — this gives users a spatial reference they intuitively understand. If ar_flutter_plugin is not available (older devices), show the dimensions in the ProductViewerWidget's 3D view instead, rendered as text labels positioned around the model using ModelViewer's hot-spot API.
Expected result: In AR mode, a dimensions card is visible at the bottom of the screen; the dimensions match the Firestore product data.
Complete working example
1import 'package:flutter/material.dart';2import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';3import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart';4import 'package:ar_flutter_plugin/datatypes/node_types.dart';5import 'package:ar_flutter_plugin/models/ar_node.dart';6import 'package:vector_math/vector_math_64.dart';78class ARProductViewer extends StatefulWidget {9 final String modelUrl;10 final double scaleMeters;11 const ARProductViewer({12 super.key,13 required this.modelUrl,14 this.scaleMeters = 1.0,15 });16 @override17 State<ARProductViewer> createState() => _ARProductViewerState();18}1920class _ARProductViewerState extends State<ARProductViewer> {21 ARSessionManager? _sessionManager;22 ARObjectManager? _objectManager;23 ARNode? _placedNode;24 bool _planeDetected = false;2526 @override27 Widget build(BuildContext context) {28 return Stack(children: [29 ARView(30 onARViewCreated: _onARViewCreated,31 planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical,32 ),33 if (!_planeDetected)34 const Positioned(35 top: 40, left: 20, right: 20,36 child: Card(37 color: Colors.black54,38 child: Padding(39 padding: EdgeInsets.all(12),40 child: Text(41 'Move your phone slowly to detect a flat surface, then tap to place',42 style: TextStyle(color: Colors.white),43 textAlign: TextAlign.center,44 ),45 ),46 ),47 ),48 if (_placedNode != null)49 Positioned(50 bottom: 40, right: 20,51 child: ElevatedButton.icon(52 onPressed: _removeModel,53 icon: const Icon(Icons.delete_outline),54 label: const Text('Remove'),55 style: ElevatedButton.styleFrom(backgroundColor: Colors.red),56 ),57 ),58 ]);59 }6061 void _onARViewCreated(62 ARSessionManager session,63 ARObjectManager objects,64 ARAnchorManager anchors,65 ARLocationManager location,66 ) {67 _sessionManager = session;68 _objectManager = objects;6970 session.onInitialize(71 showAnimatedGuide: true,72 showFeaturePoints: false,73 showWorldOrigin: false,74 handleTaps: true,75 );7677 objects.onInitialize();7879 session.onPlaneOrPointTap = (List<ARHitTestResult> results) async {80 if (_placedNode != null) return; // One model at a time81 final hit = results.firstWhere(82 (r) => r.type == ARHitTestResultType.plane,83 orElse: () => results.first,84 );85 final node = ARNode(86 type: NodeType.webGLB,87 uri: widget.modelUrl,88 scale: Vector3(89 widget.scaleMeters,90 widget.scaleMeters,91 widget.scaleMeters,92 ),93 position: Vector3.zero(),94 rotation: Vector4(1, 0, 0, 0),95 transformation: hit.worldTransform,96 );97 final added = await objects.addNode(node);98 if (added == true) setState(() => _placedNode = node);99 };100 }101102 Future<void> _removeModel() async {103 if (_placedNode == null) return;104 await _objectManager?.removeNode(_placedNode!);105 setState(() => _placedNode = null);106 }107108 @override109 void dispose() {110 _sessionManager?.dispose();111 super.dispose();112 }113}Common mistakes
Why it's a problem: Using .obj or .fbx format for 3D product models
How to avoid: Convert all models to .glb format using Blender (free), the Autodesk FBX to glTF converter, or an online converter like anyconv.com. Always verify the .glb renders correctly in the Khronos glTF Viewer (gltf.pmnd.rs) before adding it to the app.
Why it's a problem: Loading uncompressed .glb files that are 20-100MB in size
How to avoid: Compress every model with Draco mesh compression in Blender's glTF export panel. This reduces typical models from 20MB to 2-4MB. Also resize textures to 1024x1024 maximum — most product textures do not need higher resolution on mobile.
Why it's a problem: Not detecting ARKit/ARCore capability before showing the AR placement button
How to avoid: Check ARCore/ARKit availability at page load using ar_flutter_plugin's ARSessionManager.checkArCoreAvailability() (Android) or ARKit's isSupported flag. If not supported, hide the AR button and show only the 3D viewer.
Best practices
- Always use .glb format with Draco compression for 3D models — target file sizes under 5MB.
- Check ARKit/ARCore device compatibility at page load and gracefully fall back to the 3D orbit viewer.
- Limit AR scenes to one placed model at a time to maintain performance on mid-range devices.
- Show a surface detection instruction overlay until the AR plane is found — users with no AR experience need guidance.
- Display product dimensions in the AR view to help users understand real-world scale.
- Cache model URLs in App State after the first load to avoid re-fetching Firestore on every page visit.
- Test models on actual devices — AR features cannot be tested in the FlutterFlow web preview or iOS Simulator.
- Optimize textures to 1024x1024 maximum resolution — mobile screens cannot display higher resolution meaningfully.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building an AR product viewer in FlutterFlow using the ar_flutter_plugin package. The 3D models are .glb files stored in Firebase Storage. Write the complete Dart Custom Widget code for an AR viewer that: detects horizontal planes using ARCore/ARKit, places a 3D model at the tapped position using the model's Firebase Storage URL, supports one model at a time, shows an instruction overlay until the first plane is detected, and includes a Remove button to clear the placed model.
In FlutterFlow I have a product page with a Firestore document containing model_urls as a map (e.g., model_urls.red, model_urls.blue, model_urls.white) and dimensions as a map (width_cm, depth_cm, height_cm). Build the page layout that shows: a color swatch Row bound to the model_urls map keys, a ProductViewerWidget custom widget with its modelUrl bound to the selected swatch's URL, an 'View in Your Space' button that navigates to the AR placement page passing the selected model URL, and a dimensions card showing the width/depth/height values.
Frequently asked questions
Does model_viewer_plus work on all devices, or is it AR-only?
model_viewer_plus works on all modern devices as a 3D orbit viewer — no AR hardware required. It renders Google's model-viewer web component inside a WebView. The AR placement feature (the 'View in AR' button) is only available on ARCore/ARKit-supported devices, but the 3D spin-and-zoom view works universally. This makes it an excellent base experience with AR as an enhancement.
What is the difference between model_viewer_plus and ar_flutter_plugin?
model_viewer_plus provides a 3D viewer inside a WebView — it shows a model you can rotate and zoom, and has a basic 'View in AR' button powered by Android's Scene Viewer or iOS's Quick Look. ar_flutter_plugin provides full native AR control: you can detect planes, place models at specific positions, scale them, rotate them, and add multiple objects to a scene. Use model_viewer_plus for simple viewers and ar_flutter_plugin when you need full AR interaction control.
Where can I get free .glb 3D models for product visualization?
Sketchfab (sketchfab.com) has millions of models, many with free download licenses — filter by 'Downloadable' and 'glTF'. Google's Poly archive (now hosted on Poly Pizza) has simple low-poly models ideal for furniture and objects. For professional product visualization, you need custom models made by a 3D artist in Blender, Maya, or Cinema 4D, exported as .glb.
How do I make the AR model stay in the correct scale?
glTF 2.0 uses metres as the unit of measurement. If your Blender model is 1 metre tall and you want it to represent a 75cm chair, export it at 0.75 scale or set the scaleMeters parameter in ARNode to 0.75. Always check the model's real-world scale by placing a known reference (like a 1-metre cube) next to it in Blender before exporting. Models from Sketchfab often have incorrect scales and need adjustment.
Why does the AR model appear to float above the floor?
This happens when the model's origin point in Blender is at the centre of the mesh rather than at the bottom. In Blender, set the origin to the bottom face of the model before exporting (right-click > Set Origin > Origin to Bottom). Also ensure the model sits on the floor (Z=0) in Blender's coordinate space. After fixing, re-export and replace the .glb file in Firebase Storage.
Can AR product visualization increase my conversion rate?
Studies show AR product visualization reduces returns by 25-40% for furniture and home decor because buyers are more confident the product fits their space and matches their décor. IKEA Place (one of the first AR shopping apps) reported a 98% accuracy rate for visualising furniture at correct scale. The biggest benefit is for products over $100 where buyers are hesitant about size, colour, and fit.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation