iLive Docs

Flutter quickstart

Get face liveness detection running in your Flutter app in under five minutes.

Get face liveness detection running in your Flutter app in under five minutes.

What you'll build

A single button that launches the iLive liveness verification flow and returns a verdict with confidence scores. Camera capture, face detection, and on-device analysis all run natively — no server needed.

Requirements

RequirementMinimum
Flutter3.10.0+
Dart3.0.0+
Android min SDK26 (Android 8.0)
iOS14.0+
JDK17 (for Android builds)

Step 1: Add the plugin

pubspec.yaml:

dependencies:
  ilive_flutter:
    path: path/to/ilive_flutter  # local path during development

Then run:

flutter pub get

Step 2: Android setup

2a. Include native SDK modules

The Flutter plugin needs the native Android SDK modules. Add them to your app's Android settings:

android/settings.gradle.kts:

// Add after the existing include(":app") line:
 
val iliveAndroidSdkDir = file("path/to/ilive-android-sdk")
if (iliveAndroidSdkDir.exists()) {
    include(":ilive-core")
    project(":ilive-core").projectDir = file("$iliveAndroidSdkDir/ilive-core")
    include(":ilive-ui")
    project(":ilive-ui").projectDir = file("$iliveAndroidSdkDir/ilive-ui")
}

Also add the version catalog so the SDK modules can resolve their dependencies:

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from(files("path/to/ilive-android-sdk/gradle/libs.versions.toml"))
        }
    }
}

2b. Set min SDK and fix duplicate classes

android/app/build.gradle.kts:

android {
    defaultConfig {
        minSdk = 26  // iLive requires API 26+
    }
}
 
// Fix duplicate class conflict between native dependencies
configurations.all {
    exclude(group = "org.tensorflow", module = "tensorflow-lite-api")
}

2c. Add camera permission

android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />

2d. Add SDK model assets

The native SDK ships with a set of model files that need to be copied into its assets directory. Your Android SDK bundle includes a models/ directory — copy its contents into ilive-android-sdk/ilive-core/src/main/assets/models/ before your first build. The SDK README lists the exact files.

2e. Set JDK 17

android/gradle.properties:

org.gradle.java.home=/path/to/jdk-17

Step 3: Basic integration (5 lines)

import 'package:ilive_flutter/ilive_flutter.dart';
 
// Launch the liveness flow
final result = await ILive.start();
 
// Check the verdict
if (result.verdict == Verdict.pass) {
  print('Verified! Confidence: ${(result.confidence * 100).toInt()}%');
}

Step 4: Full example

import 'package:flutter/material.dart';
import 'package:ilive_flutter/ilive_flutter.dart';
 
class VerificationPage extends StatefulWidget {
  const VerificationPage({super.key});
 
  @override
  State<VerificationPage> createState() => _VerificationPageState();
}
 
class _VerificationPageState extends State<VerificationPage> {
  LivenessResult? _result;
  bool _isVerifying = false;
 
  Future<void> _verify() async {
    setState(() => _isVerifying = true);
 
    try {
      final result = await ILive.start(
        config: const ILiveConfig(
          challengeCount: 3,
          passThreshold: 0.70,
          voicePromptsEnabled: false,
          photoExtractionEnabled: true,
        ),
      );
 
      setState(() {
        _result = result;
        _isVerifying = false;
      });
 
      _handleResult(result);
    } on ILiveError catch (e) {
      setState(() => _isVerifying = false);
      _showError('${e.code.name}: ${e.message}');
    }
  }
 
  void _handleResult(LivenessResult result) {
    switch (result.verdict) {
      case Verdict.pass:
        _showSuccess(result);
      case Verdict.retry:
        _showRetry(result.retryHint ?? 'Please try again');
      case Verdict.fail:
        _showFailure(result.failureReason ?? 'Verification failed');
    }
  }
 
  void _showSuccess(LivenessResult result) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text('Verified! ${(result.confidence * 100).toInt()}% confidence'),
      backgroundColor: Colors.green,
    ));
  }
 
  void _showRetry(String hint) {
    showDialog(
      context: context,
      builder: (_) => AlertDialog(
        title: const Text('Please Try Again'),
        content: Text(hint),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK')),
          TextButton(onPressed: () { Navigator.pop(context); _verify(); }, child: const Text('Retry')),
        ],
      ),
    );
  }
 
  void _showFailure(String reason) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text('Failed: $reason'),
      backgroundColor: Colors.red,
    ));
  }
 
  void _showError(String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text('Error: $message'),
      backgroundColor: Colors.red,
    ));
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Identity Verification')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_result != null) ...[
              Icon(
                _result!.verdict == Verdict.pass ? Icons.check_circle : Icons.cancel,
                size: 64,
                color: _result!.verdict == Verdict.pass ? Colors.green : Colors.red,
              ),
              const SizedBox(height: 8),
              Text(
                '${_result!.verdict.name.toUpperCase()} - ${(_result!.confidence * 100).toInt()}%',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
              const SizedBox(height: 24),
            ],
            FilledButton.icon(
              onPressed: _isVerifying ? null : _verify,
              icon: _isVerifying
                  ? const SizedBox(
                      width: 20, height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                    )
                  : const Icon(Icons.face),
              label: Text(_isVerifying ? 'Verifying...' : 'Verify Identity'),
            ),
          ],
        ),
      ),
    );
  }
}

Configuration

const config = ILiveConfig(
  // Challenge settings
  challengeCount: 4,                    // 3-6 challenges per session
  challengeTimeoutSeconds: 10,          // seconds per challenge
  challengeTypes: {                     // which challenges to use
    ChallengeType.blink,
    ChallengeType.smile,
    ChallengeType.turnLeft,
    ChallengeType.nod,
  },
 
  // Verdict thresholds
  passThreshold: 0.75,                  // minimum confidence for PASS
  retryThreshold: 0.45,                 // below this = FAIL
  antispoofFloor: 0.30,                 // anti-spoof veto threshold
  deepfakeFloor: 0.20,                  // deepfake veto threshold
 
  // Voice prompts
  voicePromptsEnabled: true,
  voiceLanguage: 'en-US',
 
  // Photo extraction
  photoExtractionEnabled: true,
  photoWidth: 480,
  photoHeight: 600,
  photoJpegQuality: 95,
 
  // Timeouts
  totalSessionTimeoutSeconds: 120,
);
 
final result = await ILive.start(config: config);

Working with the result

Reading scores

final result = await ILive.start();
 
// Overall confidence
print('Confidence: ${(result.confidence * 100).toInt()}%');
print('Verdict: ${result.verdict.name}');
 
// Per-layer breakdown
for (final score in result.layerScores) {
  print('${score.layer}: ${(score.score * 100).toInt()}% (weight: ${(score.weight * 100).toInt()}%)');
}
// Output:
//   antiSpoof: 92% (weight: 33%)
//   deepfake: 88% (weight: 27%)
//   faceConsistency: 95% (weight: 27%)
//   motion: 78% (weight: 13%)

Using the ICAO photo

if (result.verdict == Verdict.pass && result.icaoPhoto != null) {
  // Display the captured photo
  Image.memory(result.icaoPhoto!);
 
  // Upload to your server
  await uploadPhoto(result.icaoPhoto!);
 
  // Photo quality score (0.0 - 1.0)
  print('Photo quality: ${result.photoQualityScore}');
}

Server-side attestation

const config = ILiveConfig(
  attestationKey: 'base64-encoded-hmac-key',  // your HMAC-SHA256 key
);
 
final result = await ILive.start(config: config);
 
if (result.attestation != null) {
  // Send to your server for verification
  await verifyOnServer(
    payload: result.attestation!.payload,
    signature: result.attestation!.signature,
    algorithm: result.attestation!.algorithm,  // "HMAC-SHA256"
  );
}

Device support check

Before launching the flow, check if the device is compatible:

final support = await ILive.isDeviceSupported();
 
if (support.isSupported) {
  final result = await ILive.start();
} else {
  print('Device not supported: ${support.issues.join(", ")}');
}

Custom UI with LivenessEngine

For full control over the user interface:

// Create the engine. This also loads the on-device models by default
// (cheap create + heavier initialize are fused via `autoInitialize: true`).
// Pass `autoInitialize: false` and call `engine.initialize()` yourself if
// you want to render a "loading models" state.
final engine = await LivenessEngine.create(
  config: const ILiveConfig(passThreshold: 0.70),
);
 
// Listen to face tracking updates (~30fps)
engine.faceTrackingUpdates.listen((update) {
  // update.faceDetected, update.faceBounds, update.headPose, etc.
  setState(() => _lastUpdate = update);
});
 
// Listen to challenge events
engine.startChallenges().listen((event) {
  if (event is ChallengeStarted) {
    // Show challenge instruction
  } else if (event is ChallengeCompleted) {
    // Show success/failure feedback
  } else if (event is ChallengeAllComplete) {
    // All challenges done, evaluate
  }
});
 
// Get the verdict
final result = await engine.evaluateVerdict();
 
// Always dispose when done
await engine.dispose();

LivenessResult reference

FieldTypeDescription
sessionIdStringUnique session identifier
verdictVerdictpass, fail, or retry
confidencedoubleWeighted aggregate score (0.0–1.0)
layerScoresList<LayerScore>Per-layer breakdown
retryHintString?User-facing suggestion on retry
failureReasonString?Internal reason on fail
icaoPhotoUint8List?JPEG passport photo (480×600) on pass
photoQualityScoredouble?Photo quality (0.0–1.0)
attestationAttestation?Signed payload
encryptedFrameBundleFrameBundle?Encrypted frames for audit
metadataSessionMetadataDuration, device, SDK version

Face comparison

Compare the verified face against a reference photo (for example, an ID document):

final match = await ILive.compareFaces(
  referencePhoto: idDocumentBytes,
  probePhoto: result.icaoPhoto!,
);
 
if (match != null && match.isMatch) {
  print('Same person: ${(match.similarity * 100).toInt()}% match');
}

For faster repeat comparisons, store the face embedding from the liveness result:

// Store the embedding after the first verification
final embedding = result.faceEmbedding;  // 512-dimensional
 
// Later, compare against a new photo without reloading the model
final match2 = await ILive.compareFaceWithEmbedding(
  referenceEmbedding: embedding!,
  probePhoto: newPhotoBytes,
);
ParameterDefaultDescription
threshold0.45Minimum similarity for a match (0.0–1.0)

iOS setup

The Flutter plugin also supports iOS. Add to your ios/Podfile:

platform :ios, '14.0'
 
target 'Runner' do
  use_frameworks! :linkage => :static
  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

Copy the SDK model files into your iOS app bundle (same files as Android — see Step 2d). Add camera permission to ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera access is required for face liveness verification.</string>

For the full iOS setup walk-through, see the iOS quickstart.

Platform support

PlatformStatus
AndroidFully supported
iOSFully supported
WebNot supported (requires the web integration)
DesktopNot supported

Troubleshooting

MissingPluginException: The plugin isn't registered. Ensure ilive_flutter is in your pubspec.yaml and run flutter pub get. Supported on Android and iOS only.

Build error "Duplicate class org.tensorflow.lite": Add the exclusion to your android/app/build.gradle.kts (see Step 2b).

App crashes on "Start Liveness Check": SDK model files are missing. See Step 2d.

White screen after analysis: Check logcat for SDK errors — usually a model file is missing or corrupted.

Face not detected in portrait mode: This is handled automatically. Ensure you're using the latest SDK version.

PlatformException on iOS: Ensure the native pod dependencies are installed. Run cd ios && pod install after adding the plugin.