devShakib

Show a USDZ Model in the Room, from Flutter

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.

The viewer is not the hard part, and it is already done

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.

A Flutter asset is not a file

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.

Two questions that fail differently

There are two things to ask before offering AR at all, and collapsing them into

one is a bug:

if (!ArQuickLook.isSupported) return;              // iOS onlyif (!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.

Turn scaling off when the size is the point

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.

Errors that say which kind of no

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.

Several models, one session

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.

Scan a room, then walk back into it

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.

What is verified, and what is not

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.

Android, and being honest about it

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.

The short version

know it, and it handles placement, occlusion and shadows better than you will.

the specific file. canPreview asks Quick Look rather than trusting the

extension.

because they need different responses.

it back in front of you.

ar_quick_look is on pub.dev — MIT, no dependencies,

160/160 pub points, iOS.