> ## 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, because the SDK reads the accelerometer during a
    reading.

    **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 {
      try {
        await NeurofitVitals.activate(licenseKey);
      } on PlatformException catch (e) {
        // e.code is VitalsErrorCode.licenseInvalid; e.message names the check that failed.
        debugPrint('Activation failed: ${e.code} ${e.message}');
        return false;
      }

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

      if (await NeurofitVitals.hasCameraPermission()) return true;
      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`,
    `VitalsErrorCode.cameraPermissionDenied`, ...); `message` is the native description. See
    [License keys](/license-keys).
  </Step>

  <Step title="Run a reading">
    The plugin runs one session at a time: `start()` creates it, and it ends with `CompletedEvent`, `FailedEvent`
    or `stop()`. Listen to `NeurofitVitals.events` **before** calling `start()`; the wrapper does not buffer
    events.

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

    class ReadingController {
      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 {
        _events = NeurofitVitals.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.
              this.preview.value = preview;
            case CancelledEvent():
              notice.value = "Let's try that again."; // the session starts again by itself
            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 NeurofitVitals.start(); // camera + torch on, then positioning
      }

      Future<void> cancel() => NeurofitVitals.cancelReading();

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

    `start()` fails with a `PlatformException` whose `code` is `notActivated`, `cameraPermissionDenied`,
    `unsupportedDevice`, on iOS also `cameraUnavailable`, or `internalError` while a session is already running.
    On Android a camera that cannot be opened arrives afterwards as a `FailedEvent` with code `cameraUnavailable`.
    `cancelReading()` applies while measuring only. Once `MeasurementCompleteEvent` has arrived, a stopped session
    still delivers its `CompletedEvent` (or `FailedEvent`) on the events stream.
  </Step>

  <Step title="Use the result">
    ```dart theme={null}
    void showResult(VitalsResult r) {
      if (r.quality == SignalQuality.withheld) {
        // HRV 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);
      // A low breathing-rate confidence is a weaker estimate; show it as approximate.
      final about = r.breathingRateConfidence == BreathingRateConfidence.low ? 'about ' : '';
      debugPrint('HR $hr bpm, HRV (RMSSD) $rmssd ms, breathing $about$rr brpm');
    }
    ```

    `VitalsResult` mirrors the result listed on the
    [React Native quickstart](/quickstart-react-native#event-and-result-shapes) field for field, with
    `capturedAt` as a `DateTime`, `quality` as `SignalQuality` and `breathingRateConfidence` as
    `BreathingRateConfidence`. 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, but leaving it does not end the session.
Backgrounding, or another app taking the camera, cancels a reading in progress with a `CancelledEvent` whose
`reason` is `CancelReason.interrupted`, and the session resumes positioning by itself when the app is back. Lock
the reading screen's orientation while a session runs (`SystemChrome.setPreferredOrientations`) and keep the
screen awake (for example with the `wakelock_plus` package).

To keep the camera positioning without starting a reading (an intro or a sheet covers the screen, say), call
`NeurofitVitals.setAutoStartEnabled(false)`, and `setAutoStartEnabled(true)` when the user is ready. A reading
already measuring is unaffected. The setting applies to the current session and to every later `start()` until
you change it.

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 Stream<VitalsEvent> get events;                  // listen before start()
  static Future<void> activate(String licenseKey);
  static Future<DeviceSupport> deviceSupport();
  static Future<bool> hasCameraPermission();             // never prompts
  static Future<bool> requestCameraPermission();         // true when granted
  static Future<void> start();
  static Future<void> stop();
  static Future<void> cancelReading();
  static Future<void> setAutoStartEnabled(bool enabled); // default true
}
```

| Dart type | Mirrors | Notes |
| - | - | - |
| `VitalsEvent` | `VitalsEvent` | Sealed class; subclasses `GuidanceEvent`, `StartedEvent`, `PreviewEvent`, `HeartbeatEvent`, `CancelledEvent`, `MeasurementCompleteEvent`, `CompletedEvent`, `FailedEvent` |
| `Guidance`, `CancelReason`, `SignalLevel`, `SignalQuality`, `BreathingRateConfidence` | same | Enums with the native case names |
| `LivePreview`, `VitalsResult`, `DeviceSupport` | same | Immutable classes built from the channel maps (`fromMap`); `LivePreview.rmssdMs` is null until about 20 s and `LivePreview.signalLevel` until the reading's first scored preview |
| `VitalsError` | `VitalsError` / `VitalsException` | `code`, `message`; the payload of `FailedEvent.error` |
| `VitalsErrorCode` | `VitalsError` case names | String constants for `PlatformException.code` and `VitalsError.code` |

## 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.