> ## Documentation Index
> Fetch the complete documentation index at: https://developer.neurofit.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter quickstart

> Use the neurofit_vitals Dart package, a thin MethodChannel and EventChannel wrapper over the native SDKs.

`neurofit_vitals` is a thin wrapper: every call is forwarded over a `MethodChannel` to the native
`NeurofitVitals` (Swift) or `com.neurofit.vitals` (Kotlin) SDK, and events arrive over an `EventChannel`. There
is no reading logic in Dart. Event names and payload keys are the same as the React Native and Capacitor
wrappers; the Dart types mirror them.

<Steps>
  <Step title="Add the package">
    The package is delivered with your license. Add it as a path dependency (or publish it to your private pub
    server).

    ```yaml pubspec.yaml theme={null}
    dependencies:
      neurofit_vitals:
        path: ../neurofit-vitals-sdk/ppgcore/sdk/wrappers/flutter
    ```

    Then `flutter pub get`. The plugin's podspec links `NeurofitVitals.xcframework` (binary license) or the
    Swift package sources (source license); the Android module depends on `com.neurofit:vitals-sdk:1.0.0`, so
    make that artifact resolvable from `mavenLocal()` or your artifact repository and set `minSdk` to 28 or
    higher in `android/app/build.gradle`. iOS needs a deployment target of 15.0 or higher.
  </Step>

  <Step title="Platform setup">
    **iOS**: add `NSCameraUsageDescription` to `ios/Runner/Info.plist` (see the [iOS quickstart](/quickstart-ios)).
    Adding `NSMotionUsageDescription` as well is recommended while `motionBreathing` is on.

    **Android**: the plugin's manifest declares `android.permission.CAMERA`, so nothing to add. See the
    [Android quickstart](/quickstart-android) for the optional `uses-feature` entries.
  </Step>

  <Step title="Activate and check the device">
    ```dart theme={null}
    import 'package:flutter/foundation.dart';
    import 'package:flutter/services.dart';
    import 'package:neurofit_vitals/neurofit_vitals.dart';

    Future<bool> prepareVitals(String licenseKey) async {
      final LicenseStatus status;
      try {
        status = await NeurofitVitals.activate(licenseKey);
      } on PlatformException catch (e) {
        // e.code is a VitalsErrorCode: licenseInvalid, licenseNotValidForApp, licenseUpdatesExpired
        debugPrint('Activation failed: ${e.code} ${e.message}');
        return false;
      }
      for (final w in status.warnings) {
        debugPrint('License warning: $w');
      }

      final DeviceSupport support = await NeurofitVitals.deviceSupport();
      if (!support.isSupported) {
        debugPrint('Vitals not supported: ${support.reason}');
        return false;
      }

      return NeurofitVitals.requestCameraPermission(); // true when granted
    }
    ```

    Every method fails with Flutter's `PlatformException`. Its `code` is one of the `VitalsErrorCode` string
    constants, which are the native error cases (`VitalsErrorCode.licenseInvalid`, `licenseNotValidForApp`,
    `licenseUpdatesExpired`, `cameraDenied`, ...) plus the wrapper-level `sessionNotFound`; `message` is the
    native description. See [License keys](/license-keys).
  </Step>

  <Step title="Run a reading">
    `createSession` returns the session handle as a `String`; wrap it in `VitalsSession(sessionId)` for an
    object-style API over the same calls. Listen to `session.events` **before** calling `start`; the wrapper does
    not buffer events. `session.events` is `NeurofitVitals.events` (every session) filtered to this session;
    every event carries its `sessionId`.

    ```dart theme={null}
    import 'dart:async';
    import 'package:flutter/foundation.dart';
    import 'package:neurofit_vitals/neurofit_vitals.dart';

    class ReadingController {
      VitalsSession? _session;
      StreamSubscription<VitalsEvent>? _events;

      final guidance = ValueNotifier<Guidance?>(null);
      final startProgress = ValueNotifier<double>(0);
      final preview = ValueNotifier<LivePreview?>(null);
      final notice = ValueNotifier<String?>(null);
      final result = Completer<VitalsResult>();

      Future<void> begin() async {
        final sessionId = await NeurofitVitals.createSession(
          const VitalsConfiguration(readingDuration: 60, autoRestartAfterCancel: true, motionBreathing: true),
        );
        final session = VitalsSession(sessionId);
        _session = session;

        _events = session.events.listen((VitalsEvent event) {
          switch (event) {
            case GuidanceEvent(:final guidance, :final startProgress):
              this.guidance.value = guidance;
              this.startProgress.value = startProgress;
            case PreviewEvent(:final preview):
              // preview.rmssdMs is null until about 20 s; preview.breathingRate until about 30 s.
              this.preview.value = preview;
            case CancelledEvent(:final reason):
              notice.value = 'Reading restarted: ${reason.name}'; // the session re-arms by itself
            case ReconfiguringCameraEvent(:final lowPowerMode):
              if (lowPowerMode) notice.value = 'Please turn off Low Power Mode.';
            case CompletedEvent(:final result):
              this.result.complete(result);
            case FailedEvent(:final error):
              this.result.completeError(error); // a VitalsError with code and message
            default:
              break;
          }
        });

        await session.start(); // camera + torch on, then positioning
      }

      Future<void> cancel() async {
        await _session?.cancelReading(reason: 'user_tapped_cancel');
      }

      Future<void> dispose() async {
        await _events?.cancel();
        await _session?.stop(); // idempotent; torch off, handle released
      }
    }
    ```

    `start` fails with a `PlatformException` whose `code` is the native error (`notActivated`, `alreadyRunning`,
    `cameraDenied`, `cameraRestricted`, `unsupportedDevice`, on iOS also `cameraUnavailable`); on Android a camera
    that cannot be opened arrives afterwards as a `FailedEvent` with code `cameraUnavailable`. `cancelReading`
    applies while measuring only.
  </Step>

  <Step title="Use the result">
    ```dart theme={null}
    void showResult(VitalsResult r) {
      if (r.quality == SignalQuality.withheld) {
        // Metrics may be null. Offer a retry rather than showing numbers.
        return;
      }
      final hr = r.heartRateBpm?.round();
      final rmssd = r.rmssdMs?.round();
      final rr = r.breathingRateBrpm?.toStringAsFixed(1);
      final estimate = (r.breathingRateConfidence ?? 0) < 0.5 ? ' (estimate)' : '';
      debugPrint('HR $hr bpm, HRV (RMSSD) $rmssd ms, breathing $rr brpm$estimate');
    }
    ```

    `VitalsResult` mirrors the camelCase result map listed on the
    [React Native quickstart](/quickstart-react-native#event-and-result-shapes) field for field (`capturedAt`
    as a `DateTime`, `quality` as `SignalQuality`); `diagnostics` is the snake\_case map passed through
    unchanged. Field meanings are on the [Metrics](/metrics) page.
  </Step>
</Steps>

## Camera preview

The camera preview is optional on every platform: a fingertip covers the lens during a reading. The V1 wrapper
does not render it; show the guidance text and the live values in your own widgets. A preview widget is
planned; see [Roadmap](/roadmap).

## Foreground, orientation and screen

A reading needs the app in the foreground for the whole minute: backgrounding (iOS) or the host activity's
`ON_STOP` (Android) during positioning or measuring ends the session with a `FailedEvent` whose `error.code` is
`interrupted`. Lock the reading screen's orientation while a session runs (`SystemChrome.setPreferredOrientations`)
and keep the screen awake (for example with the `wakelock_plus` package).

On Android the plugin works with the default `FlutterActivity` (it falls back to `ActivityCompat.requestPermissions`
for the camera prompt). Extending `FlutterFragmentActivity` (a `ComponentActivity`) is optional and opts into the
SDK-owned `ActivityResultRegistry` request; either way lock the orientation of the screen that asks.

## Dart API

```dart theme={null}
abstract final class NeurofitVitals {
  static Future<LicenseStatus> activate(String licenseKey);
  static Future<DeviceSupport> deviceSupport();
  static Future<bool> requestCameraPermission();
  static Future<String> createSession([VitalsConfiguration? config]);   // the session handle
  static Future<void> start(String sessionId);
  static Future<void> cancelReading(String sessionId, {String reason = 'host'});
  static Future<void> stop(String sessionId);
  static Stream<VitalsEvent> get events;                  // every session; prefer VitalsSession.events
}

/// Optional object-style convenience over a handle; every member forwards to the static calls.
class VitalsSession {
  const VitalsSession(this.sessionId);
  final String sessionId;
  Stream<VitalsEvent> get events;                         // this session only
  Future<void> start();
  Future<void> cancelReading({String reason = 'host'});
  Future<void> stop();
}
```

| Dart type | Mirrors | Notes |
| - | - | - |
| `VitalsConfiguration`, `AdvancedConfiguration` | `VitalsConfiguration` / `VitalsConfig` | `readingDuration`, `autoRestartAfterCancel`, `motionBreathing`, `advanced`; every field optional. `toMap()` sends only the fields you set; omitted fields keep the production defaults |
| `VitalsEvent` | `VitalsEvent` | Sealed class with `sessionId`; subclasses `StateEvent`, `GuidanceEvent`, `PreviewEvent`, `HeartbeatEvent`, `StartedEvent`, `CancelledEvent`, `ReconfiguringCameraEvent`, `RecoverableErrorEvent`, `MeasurementCompleteEvent`, `CompletedEvent`, `FailedEvent` |
| `Guidance`, `CancelReason`, `SignalLevel`, `AbortRisk`, `SignalQuality`, `RecoverableErrorKind`, `VitalsSessionStateKind`, `LicenseType` | same | Enums with the native case names; `CancelledEvent.hostReason` carries the host string |
| `LivePreview`, `VitalsResult`, `LicenseStatus`, `DeviceSupport` | same | Immutable classes built from the channel maps (`fromMap`); `DeviceSupport.fpsTier` (nullable), `LivePreview.rmssdMs` (null until about 20 s) and `LivePreview.breathingRate` (null until about 30 s) |
| `VitalsError` | `VitalsError` / `VitalsException` | `code`, `message`; the payload of `FailedEvent.error` and `StateEvent.error` |
| `VitalsErrorCode` | `VitalsError` case names | String constants for `PlatformException.code`, plus the wrapper-level `sessionNotFound` |

## Next steps

* [Reading lifecycle](/reading-lifecycle): what each event means and when it fires.
* [Metrics](/metrics): field definitions, units and validated accuracy.
* [Troubleshooting](/troubleshooting): permission, torch and frame-rate issues.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.