Add a complete runnable example app (replaces examples/ snippets) - #7
Add a complete runnable example app (replaces examples/ snippets)#7iamEtornam wants to merge 1 commit into
Conversation
Replaces the FlutterFlow-style examples/ snippets with a complete, runnable example/ Flutter app (path dependency on the plugin) covering all features: objects-on-planes, gestures, web object + updateNode, local/web/file-system objects, screenshots, debug options, and (Firebase-gated) cloud anchors + external model management. - Cleaned the ported code (fixed throwing firstWhere, dead AlertDialogs, print->debugPrint, mounted guards) and used a working web/embedded GLB instead of the broken bundled Chicken_01.gltf. - Cloud demos simplified off the discontinued geoflutterfire2 (download latest anchor); degrade gracefully without Firebase config. - Android minSdk 28, iOS NSCameraUsageDescription, plane-texture + embedded Duck.gltf assets bundled. - Repointed README example table to example/lib/demos/*. Verified: flutter analyze (0 errors, plugin + example), flutter test, and flutter build apk --debug all pass.
There was a problem hiding this comment.
Code Review
This pull request restructures the examples by removing the old FlutterFlow custom widgets and introducing a complete, runnable Flutter demo application in the example/ directory. The new app showcases various plugin features, including plane detection, object gestures, programmatic updates, and Firebase-backed cloud anchors. The review feedback highlights a critical pattern across almost all demo screens where asynchronous operations (such as loading models, adding anchors, or querying Firebase) are followed by state mutations or setState calls without checking if the widget is still mounted. To prevent potential memory leaks and runtime crashes when navigating away from these screens, it is highly recommended to add if (!mounted) return; guards before updating the state.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (await arAnchorManager!.addAnchor(anchor) != true) { | ||
| _snack('Adding anchor failed'); | ||
| return; | ||
| } | ||
| anchors.add(anchor); | ||
|
|
||
| final node = ARNode( | ||
| type: NodeType.webGLB, | ||
| uri: kDuckGlbUrl, | ||
| scale: Vector3(0.2, 0.2, 0.2), | ||
| position: Vector3(0, 0, 0), | ||
| rotation: Vector4(1, 0, 0, 0), | ||
| data: {'onTapText': 'Ouch, that hurt!'}, | ||
| ); | ||
| if (await arObjectManager!.addNode(node, planeAnchor: anchor) == true) { | ||
| nodes.add(node); | ||
| setState(() => _readyToUpload = true); | ||
| } else { | ||
| _snack('Adding node failed'); | ||
| } |
There was a problem hiding this comment.
The asynchronous calls addAnchor and addNode can complete after the widget has been unmounted. Mutating state (anchors.add, nodes.add) and calling setState without checking if the widget is still mounted can lead to memory leaks or runtime exceptions.
if (await arAnchorManager!.addAnchor(anchor) != true) {
_snack('Adding anchor failed');
return;
}
if (!mounted) return;
anchors.add(anchor);
final node = ARNode(
type: NodeType.webGLB,
uri: kDuckGlbUrl,
scale: Vector3(0.2, 0.2, 0.2),
position: Vector3(0, 0, 0),
rotation: Vector4(1, 0, 0, 0),
data: {'onTapText': 'Ouch, that hurt!'},
);
if (await arObjectManager!.addNode(node, planeAnchor: anchor) == true) {
if (!mounted) return;
nodes.add(node);
setState(() => _readyToUpload = true);
} else {
_snack('Adding node failed');
}| await arAnchorManager!.uploadAnchor(anchors.last); | ||
| setState(() => _readyToUpload = false); |
There was a problem hiding this comment.
| _firebase.getObjectsFromAnchor(anchor, (snapshot) { | ||
| for (final doc in snapshot.docs) { | ||
| final node = ARNode.fromMap(doc.data()); | ||
| arObjectManager!.addNode(node, planeAnchor: anchor); | ||
| nodes.add(node); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The callback for getObjectsFromAnchor is executed asynchronously when the Firestore query completes. If the widget is unmounted in the meantime, calling arObjectManager!.addNode and mutating nodes will cause errors. A mounted guard is required.
| _firebase.getObjectsFromAnchor(anchor, (snapshot) { | |
| for (final doc in snapshot.docs) { | |
| final node = ARNode.fromMap(doc.data()); | |
| arObjectManager!.addNode(node, planeAnchor: anchor); | |
| nodes.add(node); | |
| } | |
| }); | |
| _firebase.getObjectsFromAnchor(anchor, (snapshot) { | |
| if (!mounted) return; | |
| for (final doc in snapshot.docs) { | |
| final node = ARNode.fromMap(doc.data()); | |
| arObjectManager!.addNode(node, planeAnchor: anchor); | |
| nodes.add(node); | |
| } | |
| }); |
| await _firebase.downloadLatestAnchor((snapshot) { | ||
| if (snapshot.docs.isEmpty) { | ||
| _snack('No uploaded anchors found'); | ||
| return; | ||
| } | ||
| final doc = snapshot.docs.first; | ||
| final data = doc.data(); | ||
| final cloudId = data['cloudanchorid'] as String?; | ||
| if (cloudId == null) return; | ||
| _downloadsInProgress[cloudId] = data; | ||
| arAnchorManager!.downloadAnchor(cloudId); | ||
| }); | ||
| setState(() => _readyToDownload = false); | ||
| } |
There was a problem hiding this comment.
The downloadLatestAnchor call is asynchronous. Both the callback and the subsequent setState must be guarded with mounted checks to prevent crashes if the widget is disposed before the network request completes.
await _firebase.downloadLatestAnchor((snapshot) {
if (!mounted) return;
if (snapshot.docs.isEmpty) {
_snack('No uploaded anchors found');
return;
}
final doc = snapshot.docs.first;
final data = doc.data();
final cloudId = data['cloudanchorid'] as String?;
if (cloudId == null) return;
_downloadsInProgress[cloudId] = data;
arAnchorManager!.downloadAnchor(cloudId);
});
if (!mounted) return;
setState(() => _readyToDownload = false);| if (await arAnchorManager!.addAnchor(anchor) != true) { | ||
| _snack('Adding anchor failed'); | ||
| return; | ||
| } | ||
| anchors.add(anchor); | ||
|
|
||
| final node = ARNode( | ||
| type: NodeType.webGLB, | ||
| uri: _selectedModel.uri, | ||
| scale: Vector3(0.2, 0.2, 0.2), | ||
| position: Vector3(0, 0, 0), | ||
| rotation: Vector4(1, 0, 0, 0), | ||
| data: {'onTapText': 'I am a ${_selectedModel.name}'}, | ||
| ); | ||
| if (await arObjectManager!.addNode(node, planeAnchor: anchor) == true) { | ||
| nodes.add(node); | ||
| } else { | ||
| _snack('Adding node failed'); | ||
| } |
There was a problem hiding this comment.
The asynchronous calls addAnchor and addNode can complete after the widget has been unmounted. Mutating state (anchors.add, nodes.add) without checking if the widget is still mounted can lead to memory leaks or runtime exceptions.
if (await arAnchorManager!.addAnchor(anchor) != true) {
_snack('Adding anchor failed');
return;
}
if (!mounted) return;
anchors.add(anchor);
final node = ARNode(
type: NodeType.webGLB,
uri: _selectedModel.uri,
scale: Vector3(0.2, 0.2, 0.2),
position: Vector3(0, 0, 0),
rotation: Vector4(1, 0, 0, 0),
data: {'onTapText': 'I am a ${_selectedModel.name}'},
);
if (await arObjectManager!.addNode(node, planeAnchor: anchor) == true) {
if (!mounted) return;
nodes.add(node);
} else {
_snack('Adding node failed');
}| if (await arObjectManager!.addNode(node) == true) { | ||
| setState(() => fileSystemNode = node); | ||
| } else { | ||
| _snack('Adding file-system object failed'); | ||
| } |
There was a problem hiding this comment.
Calling setState after the asynchronous addNode call without checking if the widget is still mounted can cause runtime exceptions if the user navigates away while the file-system model is loading.
if (await arObjectManager!.addNode(node) == true) {
if (!mounted) return;
setState(() => fileSystemNode = node);
} else {
_snack('Adding file-system object failed');
}| final didAddAnchor = await arAnchorManager!.addAnchor(anchor); | ||
| if (didAddAnchor != true) { | ||
| _snack('Adding anchor failed'); | ||
| return; | ||
| } | ||
| anchors.add(anchor); | ||
|
|
||
| final node = ARNode( | ||
| type: NodeType.webGLB, | ||
| uri: kDuckGlbUrl, | ||
| scale: Vector3(0.2, 0.2, 0.2), | ||
| position: Vector3(0, 0, 0), | ||
| rotation: Vector4(1, 0, 0, 0), | ||
| ); | ||
| final didAddNode = await arObjectManager!.addNode(node, planeAnchor: anchor); | ||
| if (didAddNode == true) { | ||
| nodes.add(node); | ||
| } else { | ||
| _snack('Adding node failed'); | ||
| } |
There was a problem hiding this comment.
The asynchronous calls addAnchor and addNode can complete after the widget has been unmounted. Mutating state (anchors.add, nodes.add) without checking if the widget is still mounted can lead to memory leaks or runtime exceptions.
final didAddAnchor = await arAnchorManager!.addAnchor(anchor);
if (didAddAnchor != true) {
_snack('Adding anchor failed');
return;
}
if (!mounted) return;
anchors.add(anchor);
final node = ARNode(
type: NodeType.webGLB,
uri: kDuckGlbUrl,
scale: Vector3(0.2, 0.2, 0.2),
position: Vector3(0, 0, 0),
rotation: Vector4(1, 0, 0, 0),
);
final didAddNode = await arObjectManager!.addNode(node, planeAnchor: anchor);
if (didAddNode == true) {
if (!mounted) return;
nodes.add(node);
} else {
_snack('Adding node failed');
}| final didAddAnchor = await arAnchorManager!.addAnchor(anchor); | ||
| if (didAddAnchor != true) { | ||
| _snack('Adding anchor failed'); | ||
| return; | ||
| } | ||
| anchors.add(anchor); | ||
|
|
||
| final node = ARNode( | ||
| type: NodeType.webGLB, | ||
| uri: kDuckGlbUrl, | ||
| scale: Vector3(0.2, 0.2, 0.2), | ||
| position: Vector3(0, 0, 0), | ||
| rotation: Vector4(1, 0, 0, 0), | ||
| ); | ||
| final didAddNode = await arObjectManager!.addNode(node, planeAnchor: anchor); | ||
| if (didAddNode == true) { | ||
| nodes.add(node); | ||
| } else { | ||
| _snack('Adding node failed'); | ||
| } |
There was a problem hiding this comment.
The asynchronous calls addAnchor and addNode can complete after the widget has been unmounted. Mutating state (anchors.add, nodes.add) without checking if the widget is still mounted can lead to memory leaks or runtime exceptions.
final didAddAnchor = await arAnchorManager!.addAnchor(anchor);
if (didAddAnchor != true) {
_snack('Adding anchor failed');
return;
}
if (!mounted) return;
anchors.add(anchor);
final node = ARNode(
type: NodeType.webGLB,
uri: kDuckGlbUrl,
scale: Vector3(0.2, 0.2, 0.2),
position: Vector3(0, 0, 0),
rotation: Vector4(1, 0, 0, 0),
);
final didAddNode = await arObjectManager!.addNode(node, planeAnchor: anchor);
if (didAddNode == true) {
if (!mounted) return;
nodes.add(node);
} else {
_snack('Adding node failed');
}| final didAddAnchor = await arAnchorManager!.addAnchor(anchor); | ||
| if (didAddAnchor != true) { | ||
| _snack('Adding anchor failed'); | ||
| return; | ||
| } | ||
| anchors.add(anchor); | ||
|
|
||
| final node = ARNode( | ||
| type: NodeType.webGLB, | ||
| uri: kDuckGlbUrl, | ||
| scale: Vector3(0.2, 0.2, 0.2), | ||
| position: Vector3(0, 0, 0), | ||
| rotation: Vector4(1, 0, 0, 0), | ||
| ); | ||
| final didAddNode = await arObjectManager!.addNode(node, planeAnchor: anchor); | ||
| if (didAddNode == true) { | ||
| nodes.add(node); | ||
| } else { | ||
| _snack('Adding node failed'); | ||
| } |
There was a problem hiding this comment.
The asynchronous calls addAnchor and addNode can complete after the widget has been unmounted. Mutating state (anchors.add, nodes.add) without checking if the widget is still mounted can lead to memory leaks or runtime exceptions.
final didAddAnchor = await arAnchorManager!.addAnchor(anchor);
if (didAddAnchor != true) {
_snack('Adding anchor failed');
return;
}
if (!mounted) return;
anchors.add(anchor);
final node = ARNode(
type: NodeType.webGLB,
uri: kDuckGlbUrl,
scale: Vector3(0.2, 0.2, 0.2),
position: Vector3(0, 0, 0),
rotation: Vector4(1, 0, 0, 0),
);
final didAddNode = await arObjectManager!.addNode(node, planeAnchor: anchor);
if (didAddNode == true) {
if (!mounted) return;
nodes.add(node);
} else {
_snack('Adding node failed');
}| final didAdd = await arObjectManager!.addNode(node); | ||
| if (didAdd == true) { | ||
| setState(() => _node = node); | ||
| } else { | ||
| _snack('Adding node failed'); | ||
| } |
There was a problem hiding this comment.
Calling setState after the asynchronous addNode call without checking if the widget is still mounted can cause runtime exceptions if the user navigates away while the web model is loading.
final didAdd = await arObjectManager!.addNode(node);
if (didAdd == true) {
if (!mounted) return;
setState(() => _node = node);
} else {
_snack('Adding node failed');
}
What
Adds a complete, runnable Flutter example app under
example/(path dependency on the plugin) and removes the oldexamples/snippets, whose content is now covered by the app. The plugin README's example table is repointed toexample/lib/demos/*.Demos (
example/lib/demos/)addAnchor,addNodeon anchor,onNodeTaphandlePans/handleRotation,onPanEnd/onRotationEnd, transform syncaddNode/removeNode+updateNode(programmatic rotate/scale)localGLTF2(bundled asset),webGLB,fileSystemAppFolderGLB, shuffleARSessionManager.snapshot()initGoogleCloudAnchorMode,uploadAnchor/downloadAnchor+ FirestoreNotes / deviations
examples/snippets, cleaned up: fixed the throwingfirstWhere+ always-true null checks, removed deadAlertDialogs,print→debugPrint, addedmountedguards.Chicken_01.gltfis broken (missing.bin), so a self-containedassets/models/Duck.gltfis bundled for the local-asset demo, plusassets/images/triangle.pngforcustomPlaneTexturePath.geoflutterfire2(conflicts with currentcloud_firestore); they download the most-recent anchor instead of a geo-radius query. Without a Firebase config they show a graceful "initialization failed" screen — the other 6 demos run with zero setup. Firebase project still required for those two (seecloudAnchorSetup.md).minSdk 28, iOSNSCameraUsageDescription.Verification
flutter analyze→ 0 errors in both the plugin root (the old 82examples/errors are gone) andexample/.flutter test→ passing (home-menu smoke test).flutter build apk --debug→ succeeds (compiles the plugin's Kotlin, ARCore/sceneview, Firebase, path_provider on AGP 9.0.1 / Kotlin 2.3.20).Not verified
pod install/signing).