Skip to content

Add a complete runnable example app (replaces examples/ snippets) - #7

Open
iamEtornam wants to merge 1 commit into
mainfrom
feat/example-app
Open

Add a complete runnable example app (replaces examples/ snippets)#7
iamEtornam wants to merge 1 commit into
mainfrom
feat/example-app

Conversation

@iamEtornam

Copy link
Copy Markdown
Owner

What

Adds a complete, runnable Flutter example app under example/ (path dependency on the plugin) and removes the old examples/ snippets, whose content is now covered by the app. The plugin README's example table is repointed to example/lib/demos/*.

Demos (example/lib/demos/)

Screen Feature
Objects on planes plane detection, addAnchor, addNode on anchor, onNodeTap
Object gestures handlePans/handleRotation, onPanEnd/onRotationEnd, transform sync
Web object + updateNode addNode/removeNode + updateNode (programmatic rotate/scale)
Local & web objects localGLTF2 (bundled asset), webGLB, fileSystemAppFolderGLB, shuffle
Screenshots ARSessionManager.snapshot()
Debug options live toggles for feature points / planes / world origin
Cloud anchors initGoogleCloudAnchorMode, uploadAnchor/downloadAnchor + Firestore
External model management Firestore-managed model list + cloud anchors

Notes / deviations

  • Ported from the old examples/ snippets, cleaned up: fixed the throwing firstWhere + always-true null checks, removed dead AlertDialogs, printdebugPrint, added mounted guards.
  • Models load as web/embedded GLB — the bundled Chicken_01.gltf is broken (missing .bin), so a self-contained assets/models/Duck.gltf is bundled for the local-asset demo, plus assets/images/triangle.png for customPlaneTexturePath.
  • Cloud demos dropped the discontinued geoflutterfire2 (conflicts with current cloud_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 (see cloudAnchorSetup.md).
  • Platform wiring: Android minSdk 28, iOS NSCameraUsageDescription.

Verification

  • flutter analyze0 errors in both the plugin root (the old 82 examples/ errors are gone) and example/.
  • flutter test → passing (home-menu smoke test).
  • flutter build apk --debugsucceeds (compiles the plugin's Kotlin, ARCore/sceneview, Firebase, path_provider on AGP 9.0.1 / Kotlin 2.3.20).

Not verified

  • Runtime AR + cloud-anchor behavior — needs a physical ARCore/ARKit device and a real Firebase/Cloud-Anchor project.
  • iOS build — not compiled here (needs pod install/signing).

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +163 to +182
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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');
    }

Comment on lines +199 to +200
await arAnchorManager!.uploadAnchor(anchors.last);
setState(() => _readyToUpload = false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling setState after an asynchronous operation without checking if the widget is still mounted can cause runtime exceptions if the user navigates away before the upload completes.

    await arAnchorManager!.uploadAnchor(anchors.last);
    if (!mounted) return;
    setState(() => _readyToUpload = false);

Comment on lines +228 to +234
_firebase.getObjectsFromAnchor(anchor, (snapshot) {
for (final doc in snapshot.docs) {
final node = ARNode.fromMap(doc.data());
arObjectManager!.addNode(node, planeAnchor: anchor);
nodes.add(node);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
_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);
}
});

Comment on lines +240 to +253
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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);

Comment on lines +177 to +195
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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');
    }

Comment on lines +184 to +188
if (await arObjectManager!.addNode(node) == true) {
setState(() => fileSystemNode = node);
} else {
_snack('Adding file-system object failed');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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');
    }

Comment on lines +102 to +121
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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');
    }

Comment on lines +94 to +113
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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');
    }

Comment on lines +120 to +139
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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');
    }

Comment on lines +118 to +123
final didAdd = await arObjectManager!.addNode(node);
if (didAdd == true) {
setState(() => _node = node);
} else {
_snack('Adding node failed');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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');
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant