Loading devShakib…
Apple already ships the AR viewer your users know from Messages and Safari. Here is why presenting it beats rebuilding it, and the asset trap everyone hits…
A customer is looking at a chair on your product page and wants to know whether it fits. The honest answer involves their room, not your photography.
iOS has shipped the answer since iOS 12. AR Quick Look is the viewer that opens when you tap a USDZ file in Messages, Mail or Safari: it finds the floor, places the object at real-world scale, lets the user walk around it, and draws a contact shadow underneath so the thing looks like it is actually there.
The interesting question is not how to build that. It is why you would.
Sit down to build in-app AR properly and the list gets long quickly. Plane detection. Placing the object without it sinking through the floor or floating above it. Scaling gestures that do not fight the placement gesture. Occlusion, so the object goes behind a real table leg instead of painting over it. Contact shadows. The object/AR toggle. Sharing. Lighting that roughly matches the room.
Apple has tuned every one of those, on hardware they control, for years. More importantly, your users have already learned it. The gestures, the toggle in the corner, the way you tap to place — they know that interface from opening a model someone sent them in Messages. A viewer you build yourself is a viewer they have to learn, and it will be worse.
So the sensible move on iOS is to present Apple's and get out of the way:
await ArQuickLook.present('/path/to/chair.usdz');
The future completes when the user closes the viewer, so await covers the whole interaction rather than just the opening of it. That matters more than it sounds — it means "show the model, then do the next thing" is a straight line with no callback plumbing:
await ArQuickLook.present(path);
await showRatingPrompt();
That is the whole reason ar_quick_look exists. It is deliberately not an AR framework.
Here is the part that catches everyone, and it is worth understanding rather than working around.
Quick Look takes a file path. It is a system viewer; it opens things on disk. A Flutter asset is not on disk in any sense Quick Look can use — it lives inside the app bundle behind Flutter's asset system, reachable through rootBundle and nothing else. So this looks reasonable and cannot work:
await ArQuickLook.present('assets/models/chair.usdz'); // no such file
The fix is mechanical: read the asset bytes, write them to a real file in a temporary directory, hand over that path. Everybody who ships a bundled model writes that function, so the package writes it for you:
await ArQuickLook.presentAsset('assets/models/chair.usdz');
The copy is cached, so showing the same asset twice writes it once. And materializeAsset is public for when something else needs a real path — sharing a model, or handing it to another app — because the moment you have solved "asset to file" for AR you will want it again elsewhere.
There are two things to ask before offering AR at all, and collapsing them into one is a bug:
if (!ArQuickLook.isSupported) return; // iOS only
if (!await ArQuickLook.canPreview(path)) return; // this file specifically
isSupported is about the platform. canPreview is about the file in your hand — and it asks Quick Look rather than looking at the extension. That distinction has teeth: a text file renamed .usdz comes back false, which is exactly what you want before presenting a viewer that would otherwise open and fail in front of the user.
If your models are downloaded rather than bundled, canPreview is the check that catches a truncated download, an HTML error page saved with the wrong name, or a format Quick Look does not handle on that OS version. Extension-sniffing catches none of those.
Quick Look lets the user pinch to resize the object. For a decorative model that is a nice touch. For furniture it destroys the entire purpose:
await ArQuickLook.present(path, allowsContentScaling: false);
A sofa the customer can shrink to half-size is no longer telling them whether it fits through the door. If the reason the user opened AR was to judge dimensions, scaling is not a feature, it is a way to get the wrong answer confidently.
Leave it on for a sculpture. Turn it off for anything with a measurement.
Failures here are not interchangeable, and a bare PlatformException with a string message forces you to parse English to find out what happened.
ar_quick_look throws a sealed hierarchy instead — NotOnThisPlatformException, FileNotFoundException, UnsupportedFileException, NoHostException — so a switch over them is exhaustive and the compiler tells you when you have missed one:
try {
await ArQuickLook.present(path);
} on FileNotFoundException {
await redownload();
} on UnsupportedFileException {
showMessage('That model could not be opened.');
}
Each of those wants a genuinely different response. A missing file is recoverable — fetch it again. An unsupported file is not, and the user should be told plainly rather than watching a retry loop.
If you have variants — the same chair in four fabrics — you do not want four separate viewer launches:
await ArQuickLook.presentAll(paths, initialIndex: 2);
Quick Look handles the paging itself, so the user swipes between finishes inside the AR session instead of backing out to your list and starting placement over each time. Given that placement is the slowest part of the interaction, that is the difference between comparing three options and comparing one.
There is a pairing here worth pointing at, because the two halves fit together with no glue.
roomplan wraps Apple's RoomPlan: the user sweeps their phone around a room and gets back a structured model of it — walls, doors, windows, furniture — and a USDZ export. ar_quick_look takes a USDZ path. So:
controller.rooms.listen((room) async {
if (room.usdzPath != null) await ArQuickLook.present(room.usdzPath!);
});
Scan a room, then place the scan of that room back in front of you at table scale. For anything involving space — surveying, moving, fit-out, insurance — that loop is most of the product, and it is a dozen lines.
The AR camera experience needs a real device. A Simulator has no camera and no world tracking, so there is a hard limit on what can be tested automatically, and it would be dishonest to imply otherwise.
What runs on a Simulator and is tested there: the plugin registers, isSupported returns true on iOS, a missing file and an unreadable one each raise their own exception, and a text file renamed .usdz is correctly refused.
That covers the API contract and every failure path. The part it does not cover is the AR session itself — placement, tracking, occlusion — which is Apple's code rather than the plugin's, and which you should look at on a physical device before shipping regardless of which package you use.
There is no Android support here, and that is not a to-do item.
AR Quick Look is an iOS system viewer. Android's rough equivalent is Scene Viewer, which takes glTF/GLB rather than USDZ, has different capabilities, and is reached through an intent. Wrapping both behind one API would mean either lying about the differences or exposing them anyway through a union type — and the model format alone is a genuine fork in your asset pipeline, not an implementation detail a package can hide.
So this does one thing on one platform. isSupported tells you where you are, and you branch. That is a smaller promise, and it is one the package can keep.
isSupported for the platform, canPreview for the specific file. canPreview asks Quick Look rather than trusting the extension.roomplan: scan a room, then put it back in front of you.ar_quick_look is on pub.dev — MIT, no dependencies, 160/160 pub points, iOS.