pikd_flutter_ar 0.8.0-beta.2 copy "pikd_flutter_ar: ^0.8.0-beta.2" to clipboard
pikd_flutter_ar: ^0.8.0-beta.2 copied to clipboard

Flutter bridge for the PIKD native AR SDK, providing ARKit and ARCore sessions, geospatial placement, navigation, interactions, and physics.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:pikd_flutter_ar/pikd_flutter_ar.dart';
import 'package:geolocator/geolocator.dart';
import 'package:pikd_flutter_api/api.dart';

/// **Minimal** Tier-0 AR-collect demo for the AR binding on its own: anonymous,
/// SDK-key only, no partner backend, no app chrome.
///
/// This is deliberately bare — camera, drops, tap-to-collect, one status line.
/// **For the full product demo** (all five prebuilt module screens, live↔sample
/// toggle, brand rebranding, Explore → AR collect) run the top-level **`demo/`**
/// app instead: `cd demo && flutter run`. Picking the wrong one is easy, so this
/// screen labels itself on-device too.
///
/// Shows the whole slice composed: the generated `pikd_flutter_api` data client
/// fetches/claims collectibles over `/sdk/v1`, and `pikd_flutter_ar` renders +
/// places them in AR. Runs on a physical device (ARKit/ARCore); analyzes clean
/// without one.
void main() => runApp(const DropDemoApp());

// Provisioned per environment: --dart-define=PIKD_SDK_KEY=... --dart-define=PIKD_BASE=...
const _base = String.fromEnvironment('PIKD_BASE', defaultValue: 'https://api.pikd.app/sdk/v1');
const _sdkKey = String.fromEnvironment('PIKD_SDK_KEY', defaultValue: 'pk_demo');

class DropDemoApp extends StatelessWidget {
  const DropDemoApp({super.key});
  @override
  Widget build(BuildContext context) =>
      const MaterialApp(title: 'PIKD Drop Demo', home: DropDemoScreen());
}

class DropDemoScreen extends StatefulWidget {
  const DropDemoScreen({super.key});
  @override
  State<DropDemoScreen> createState() => _DropDemoScreenState();
}

class _DropDemoScreenState extends State<DropDemoScreen> {
  late final CollectiblesApi _api;
  String _status = 'starting…';
  bool _arReady = false; // gates PikdArView: only mount after permissions granted
  Completer<void>? _sessionStarted; // completes on ArSessionStarted (see _start)

  // Location recipe (ADR 0005): the SDK core is plugin-free — the host provides
  // location. Here we wire `geolocator` behind PIKD's CallbackLocationProvider;
  // with PikdSdk you'd pass this as `PikdProviders(location: ...)`.
  final LocationProvider _location = CallbackLocationProvider(() async {
    if (!await Geolocator.isLocationServiceEnabled()) return null;
    var perm = await Geolocator.checkPermission();
    if (perm == LocationPermission.denied) {
      perm = await Geolocator.requestPermission();
    }
    if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) {
      return null;
    }
    final p = await Geolocator.getCurrentPosition();
    return (lat: p.latitude, lng: p.longitude);
  });

  @override
  void initState() {
    super.initState();
    final client = ApiClient(basePath: _base);
    // Tier-0: SDK key identifies the tenant; the user is anonymous.
    client.addDefaultHeader('x-pikd-sdk-key', _sdkKey);
    _api = CollectiblesApi(client);
    _start();
  }

  Future<void> _start() async {
    try {
      // Android needs an explicit runtime grant before the AR session/view can
      // start (iOS prompts at point-of-use). Gate on the grant, then mount the
      // view BEFORE initialize()/startArSession(): PikdArContainerView only
      // auto-starts its renderer when it attaches while the SDK is
      // NOT_INITIALIZED/INITIALIZED. If the session is already RUNNING when it
      // attaches, it no-ops and the camera never binds (black view).
      if (!await PikdAr.requestArPermissions()) {
        if (mounted) setState(() => _status = 'camera + location permission required');
        return;
      }
      if (mounted) setState(() => _arReady = true);
      await PikdAr.initialize(const PikdArConfig(
        userId: 'anon',
        apiBaseUrl: _base,
        authToken: _sdkKey,
        mockGps: true,
      ));

      // Subscribe BEFORE starting so the session-started event can't be missed.
      PikdAr.events.listen(_onEvent);

      // Don't start the session here — PikdArView owns it, auto-starting on
      // window attach on both platforms. Starting it here too is a real
      // double-start: both land in ARManager.runSession, which holds ONE pending
      // completion, so the view's (completion-less) start replaces ours and
      // nothing ever completes it — an await hangs forever, and the duplicate
      // start leaves native session state inconsistent across close/reopen.
      // Wait for the event the winning start emits instead.
      _sessionStarted = Completer<void>();
      // Already-running session emits no new event, so time out and continue.
      await _sessionStarted!.future.timeout(
        const Duration(seconds: 8),
        onTimeout: () => debugPrint('[example] no ArSessionStarted in 8s — continuing'),
      );

      // Query drops around the device's real location (via the provider above);
      // fall back to a demo location if it's unavailable.
      final here = await _location.currentLocation() ?? (lat: 51.5074, lng: -0.1278);
      if (mounted) {
        setState(() => _status =
            'near ${here.lat.toStringAsFixed(4)}, ${here.lng.toStringAsFixed(4)} — finding drops…');
      }
      final drops = await _api.getNearbyCollectibles(here.lat, here.lng, radiusM: 250) ?? const [];
      for (final d in drops) {
        // Pick the platform-appropriate 3D model (iOS renders USDZ, Android GLB).
        // Skip drops with no model for this platform — nothing to render.
        final modelUrl =
            defaultTargetPlatform == TargetPlatform.iOS ? d.iosUrl : d.androidUrl;
        if (modelUrl == null || modelUrl.isEmpty) continue;
        final asset = await PikdAr.loadAsset(modelUrl, d.id);
        await PikdAr.placeAssetOnSurface(asset.id);
        // Auto-play the model's first clip, as RN's <ARAsset> does. Only works
        // AFTER placement (native needs an instance to animate) and needs a
        // settle delay because placement finishes asynchronously — playing
        // immediately silently does nothing. Unawaited so one asset's delay
        // doesn't stall placing the rest.
        _playFirstAnimation(asset);
      }

      if (mounted) {
        setState(() => _status = '${drops.length} drops nearby — tap to collect');
      }
    } catch (e) {
      if (mounted) setState(() => _status = 'error: $e');
    }
  }

  /// Play the model's first animation clip, looped (mirrors RN's `<ARAsset>`
  /// auto-play, which uses `asset.animations[0]` when no clip is named).
  Future<void> _playFirstAnimation(Asset asset) async {
    if (asset.animations.isEmpty) return;
    await Future<void>.delayed(const Duration(milliseconds: 1200));
    if (!mounted) return;
    try {
      await PikdAr.playAssetAnimation(asset.id, asset.animations.first, loop: true);
    } catch (e) {
      debugPrint('[example] playAssetAnimation failed: $e');
    }
  }

  void _onEvent(ArEvent e) {
    if (e is ArSessionStarted && _sessionStarted?.isCompleted == false) {
      _sessionStarted!.complete();
      return;
    }
    if (e is Interaction && e.assetId != null) _collect(e.assetId!);
  }

  Future<void> _collect(String id) async {
    final res = await _api.collectCollectible(id); // anonymous: no x-pikd-user
    if (mounted) setState(() => _status = 'collected $id → ${res?.status}');
  }

  @override
  void dispose() {
    PikdAr.stopArSession();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          if (_arReady) const PikdArView() else const ColoredBox(color: Colors.black),
          Positioned(
            top: 60,
            left: 16,
            right: 16,
            child: Text(
              _status,
              style: const TextStyle(color: Colors.white, fontSize: 16),
            ),
          ),
          // Self-labelling: there are two runnable apps in this repo and landing
          // on the wrong one costs real time. Say which this is, on-device.
          const Positioned(
            bottom: 24,
            left: 16,
            right: 16,
            child: Text(
              'minimal AR-binding demo — for the full app (all modules, '
              'live/sample + brand toggles) run  demo/  at the repo root',
              textAlign: TextAlign.center,
              style: TextStyle(color: Colors.white70, fontSize: 11),
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
0
points
238
downloads

Publisher

verified publisherpikd.app

Weekly Downloads

Flutter bridge for the PIKD native AR SDK, providing ARKit and ARCore sessions, geospatial placement, navigation, interactions, and physics.

Homepage
Repository (GitHub)
View/report issues

Topics

#augmented-reality #arkit #arcore #geospatial #flutter-plugin

License

unknown (license)

Dependencies

flutter

More

Packages that depend on pikd_flutter_ar

Packages that implement pikd_flutter_ar