Skip to main content

liveness_flutter 0.5.1 copy "liveness_flutter: ^0.5.1" to clipboard
liveness_flutter: ^0.5.1 copied to clipboard

Beta: Plug-and-play liveness detection for Flutter. Configure action sequences like blink and smile, capture images or video, and send verification results to your backend.

๐Ÿ›ก๏ธ liveness_flutter #

Face liveness checks for Flutter โ€” free, on-device, and built for your backend.

pub package License: MIT Platform No ML models Status: beta

Blink. Smile. Turn left. Verified. โœ…

The default screen: tap Verify, blink, turn left and smile inside an oval, then Passed with 100% confidence
Out of the box โ€” about 10 lines of code
A fully branded account-upgrade flow: account home, intro, a face check in a circle with nod and blink steps, then verified
Fully branded โ€” your design

๐Ÿงช Beta โ€” tested on real Android and iOS devices; the API may still change before 1.0 (details). Anyone can open an issue.

Add a face liveness check to your Flutter app in a few lines. The user blinks, smiles or turns their head when asked; the package checks a live person did it, then hands you the result โ€” and the photos, if you want them โ€” to send wherever you want.

LivenessDetector(
  config: const LivenessConfig(
    actions: [LivenessAction.blink, LivenessAction.smile, LivenessAction.lookLeft],
    shuffleActions: true,          // a new order every time
    capture: {CaptureType.images}, // photos your server can check
  ),
  onResult: (result) {
    if (result.success) sendToYourServer(result);
  },
)

Everything runs on the phone. No cloud service. No license fees. No model downloads. No account.

Requirements: Flutter 3.38+ ยท Android 7.0+ (API 24) ยท iOS 15.5+

โœจ Why this package? #

๐ŸŽฏ Proves a live person is there The user does random actions โ€” blink, smile, turn, nod, 13 to choose from โ€” in a new order every time, so a photo or a pre-recorded video can't follow along.
๐Ÿ•ต๏ธ Catches common tricks on the phone Frozen or injected camera feeds, an unnaturally still "face", a second or swapped face, bad lighting โ€” plus an optional screen-flash test against replayed videos. No ML model to download.
๐Ÿ“ธ Gives your server the evidence A photo at the peak of each action (eyes shut for a blink), or video, with hashes and an optional server-issued challenge, so your backend can verify instead of trusting the phone.
๐ŸŽจ Looks like your app Theme every color and word, or replace the screen with your own design โ€” see the branded fintech example.
๐Ÿชถ Free and light Pure Dart plus Google's on-device ML Kit. No per-check fees, no account, no server of ours.

๐Ÿค” Is this right for you? #

A great fit for sign-up and onboarding gates, confirming it's really the user before a sensitive action, marketplace and gig-worker checks, and KYC flows where your backend also reviews the photos.

Pair it with server checks for anything regulated or with money on the line. The phone's verdict alone can be tampered with: follow the server verification guide and review the photos server-side (face match, presentation-attack detection).

Not the right tool if you need certified liveness โ€” for example, a passed ISO/IEC 30107-3 presentation-attack evaluation such as iBeta's โ€” or protection against sophisticated masks and deepfakes. That takes a trained anti-spoof model and usually a paid provider. This package can plug such a model in but doesn't ship one.

โš™๏ธ How it works #

Camera, then ML Kit finds the face, then actions and checks, then onResult on the phone (offline); you send the result to your server, which verifies and decides
  1. The camera streams frames, and Google's ML Kit finds the face โ€” on the phone, offline.
  2. The package asks for each action in turn and checks it was really done, while watching for frozen feeds, a second face and poor light.
  3. onResult gives you one LivenessResult: pass or fail, a confidence score, and the photos if you asked for them.
  4. You send it to your server, which can verify it before trusting it.

๐Ÿ—บ๏ธ Pick your path #

You want toโ€ฆ Go to
Add a working liveness check to your app Simple setup โ€” about 10 minutes
See every example, or run one Examples
Make it look like your app (colors, text, your own screens) Make it look like your app and the fintech example
Use it for KYC or anything with real consequences Bind sessions to your server
Have an agent verify someone else Assisted mode
Quick answers (offline? app size? masks?) FAQ
Know the limits before shipping Honest notes

๐Ÿš€ Simple setup #

Four steps to a working check. Everything here uses defaults; the advanced sections cover the rest.

1. Install and set up the platforms #

dependencies:
  liveness_flutter: ^0.5.0

Needs Flutter 3.38+ (Dart 3.10+), the minimum for package:camera 0.12.

Android โ€” set minSdkVersion 24 in your app.

iOS โ€” add to ios/Runner/Info.plist (without it the app crashes on camera use):

<key>NSCameraUsageDescription</key>
<string>Camera is used for liveness verification.</string>

Also set platform :ios, '15.5' in ios/Podfile (the face detection library needs iOS 15.5+).

You don't need a permissions package: the camera plugin asks for access the first time. If the user says no, the session ends with failureReason == LivenessFailureReason.permissionDenied.

2. Show the liveness screen #

import 'package:liveness_flutter/liveness_flutter.dart';

class LivenessPage extends StatelessWidget {
  const LivenessPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LivenessDetector(
        config: const LivenessConfig(
          actions: [
            LivenessAction.blink,
            LivenessAction.smile,
            LivenessAction.lookLeft,
          ],
          shuffleActions: true,          // a new order every time
          capture: {CaptureType.images}, // photos your server can check
        ),
        onResult: (result) {
          // If the user pressed back, this screen is gone or animating
          // out, and popping would close the *previous* screen. Check first.
          if (!context.mounted) return;
          if (ModalRoute.of(context)?.isCurrent ?? false) {
            Navigator.pop(context, result);
          }
        },
      ),
    );
  }
}

// Somewhere in your app:
final result = await Navigator.push<LivenessResult>(
  context,
  MaterialPageRoute(builder: (_) => const LivenessPage()),
);

โš ๏ธ onResult is called exactly once per session โ€” even if the screen is closed before the check finishes (system back, route popped). In that case the result is cancelled with metadata['cancelledBy'] == 'dispose' and arrives just after the screen is gone, which is why the guard above matters. cancelledBy is 'user' for the close button and 'lifecycle' when the app goes to the background.

๐Ÿ’ก Why shuffle? If actions always come in the same order, someone could record a video of a person doing that exact sequence and play it to the camera. Random order means yesterday's recording won't match today's.

๐Ÿ›ก๏ธ Include at least one motion action (blink, nod, openMouth, drawCircleWithNose). Pose-only actions (smile, tiltLeft/tiltRight, lookUp/lookDown) can be faked with a photo tilted or swapped at the right moment.

3. Read the result #

The essentials of LivenessResult:

  • success โ€” did the person complete all actions in time?
  • failureReason โ€” why not (took too long, face left the screen, more than one face, static/injected input suspected, camera access denied, user cancelledโ€ฆ)
  • confidenceScore โ€” 0 to 1. A clean run on a real camera scores 0.9+.
  • images โ€” the photos, each labeled with its action and kind (reference, or peak: the moment the action was clearest, e.g. eyes shut for a blink)
  • sessionId โ€” a unique audit ID (e.g. LV-018F3A2B9C4E-D7E31F08)

debugPrint(result.toString()) prints a readable summary, and result.toJson() gives a JSON-safe one (no image bytes). The full list of fields is further down.

4. Send it to your server #

try {
  await HttpLivenessUploader(
    endpoint: Uri.parse('https://api.example.com/liveness'),
    headers: {'Authorization': 'Bearer โ€ฆ'},
    onProgress: (sent, total) => progress.value = sent / total,
  ).upload(result);
} on LivenessUploadException catch (e) {
  // Your server answered with an error (e.statusCode, e.body).
}

It sends a metadata field with result.toJson() (session ID, confidence, actions, a SHA-256 of every photoโ€ฆ), one file per photo named like blink_peak_812ms.jpg, and an X-Liveness-Session header. Any transport works instead: LivenessUploader.custom((result) async { ... }) wraps dio, S3, Firebase or anything else.

โณ Show a progress bar. With photos an upload can be several MB. A common pattern: close the camera screen straight away and upload from the previous screen with a LinearProgressIndicator driven by onProgress.

That's it โ€” you have a working liveness check. The complete runnable version of these steps is example/lib/main.dart. Before using it for anything with real consequences, read Bind sessions to your server and the honest notes.


๐Ÿ“š Examples #

Each file in example/ runs on its own on a physical device (simulators have no usable front camera):

Example Shows Run from example/
Minimal The simple setup above, in ~90 lines flutter run
Branded fintech KYC flow A complete, fully custom-designed verification flow (more below) flutter run -t lib/recipes/fintech/main.dart
Custom UI Your own overlay and instructions, with detection following your window flutter run -t lib/recipes/custom_ui.dart
Controller Your own Cancel / Try again buttons and live state flutter run -t lib/recipes/controller.dart
Server-bound A challenge from your backend, then upload with error handling flutter run -t lib/recipes/server_bound.dart --dart-define=BACKEND_URL=https://โ€ฆ
Test bench Every option behind a settings screen, for trying the package on a device flutter run -t lib/test_bench/main.dart

Clone the repository to get the example with its Android and iOS folders ready: git clone https://github.com/maykhid/liveness_flutter && cd liveness_flutter/example. (The copy on pub.dev leaves the platform folders out; the example README explains how to generate them.)

๐Ÿฆ Example: a fully branded KYC flow #

example/lib/recipes/fintech/ shows how far customization goes: a complete "upgrade your account" verification flow, the way a mobile bank or fintech app would ship it, built for a fictional brand ("AcmePay"). None of the package's default UI is visible โ€” only its detection.

A branded account-upgrade flow: account home, intro, a face check with nod and blink steps, then verified

The full flow on an iPhone.

  1. Account home โ€” balance card, current tier, "Upgrade to Tier 2".
  2. Intro โ€” where the user is in the upgrade (phone โœ“, BVN โœ“, face check), tips for passing first time, and what the photos are used for.
  3. Face check โ€” a light screen with a circular window, a progress ring split into one segment per action, and a card with an icon, the current step, a hint that turns red when something needs fixing, and a countdown. The circle is passed as targetRegion, so the face must be inside what's drawn. A branded page handles denied camera access.
  4. Verifying โ†’ success or failure โ€” success unlocks Tier 2; failure explains what went wrong for each LivenessFailureReason and offers Try again.

Run it:

cd example
flutter run -t lib/recipes/fintech/main.dart

Make it yours: everything brand-specific โ€” name, colors, wording, which actions it asks for โ€” is in brand.dart. Change primary to your color and hot reload. The "Verifying" step simulates your backend's approval: replace _serverApproves in outcome_screens.dart with your upload and your server's decision.


๐Ÿงฐ Advanced #

Independent topics โ€” read the ones you need.

๐ŸŽจ Make it look like your app #

You need this if the default dark oval and white text don't match your design.

Theme and text (no custom widgets): LivenessTheme sets colors, borders, text styles, the target's size, position and shape (oval, circle or rounded rectangle โ€” this is also where the face must be), the close button, and every piece of text through LivenessStrings:

LivenessDetector(
  theme: LivenessTheme(
    ovalShape: TargetShape.circle,
    progressColor: Colors.teal,
    strings: LivenessStrings(
      actionInstructions: {LivenessAction.blink: 'Clignez des yeux'},
      stepCounter: (current, total) => 'ร‰tape $current sur $total',
    ),
  ),
  ...
)

Text maps (actionInstructions, guidanceMessages, failureMessages) are merged over the English defaults, so you can override just a few entries. Both LivenessTheme and LivenessStrings have copyWith. For layout: instructionAlignment, closeButtonBuilder, and theme fields like closeIconColor and resultHoldDuration.

Your own widgets: overlayBuilder replaces the dimmed overlay and instructionBuilder the instruction area. Both receive the live LivenessSessionState and rebuild on every change. Useful fields:

Field What it is
phase searching, centering, performing an action, completed, failedโ€ฆ
currentAction, actionPlan the action now, and every action in this session's order (known from the first frame)
actionProgress, overallProgress 0โ€“1 for the current action and for the whole session
remaining, actionTimeout, sessionRemaining countdowns
faceInPosition, guidance whether the face is in place, and what's wrong if not (tooFar, lowLightโ€ฆ)
failureReason why it failed

If you draw your own window, tell detection where it is with targetRegion (normalized to the widget, e.g. Rect.fromLTWH(0.15, 0.2, 0.7, 0.5)); otherwise the theme's oval still decides where the face must be. Working code: custom UI recipe and the fintech example.

DetectorTuning sets how strict each action is (how big a smile counts, how far to turn, how long to holdโ€ฆ). Tested defaults, all adjustable.

๐ŸŽฎ Control it from outside: LivenessController #

You need this if you hide the built-in close button, want a Try again without leaving the screen, or need the state outside the widget.

final controller = LivenessController(); // create in initState, dispose in dispose

LivenessDetector(
  controller: controller,
  showCloseButton: false,
  config: ...,
  onResult: ...,
);

controller.cancel();         // onResult gets a cancelled result (cancelledBy: 'user')
await controller.restart();  // fresh session: new sessionId, new shuffle
controller.state;            // live LivenessSessionState (it's a ChangeNotifier)
controller.actionPlan;       // the actions in the order this session runs them

Every session still gets exactly one onResult: restarting a session that's still running delivers it as cancelled with cancelledBy: 'restart'. See the controller recipe.

You need this if a passing result unlocks anything valuable. On its own, the phone's verdict is unsigned: your server can't tell which actions it expected or whether the result was edited. Three hooks fix that:

  • LivenessConfig.challenge: your server issues a LivenessChallenge (single-use nonce, the actions in its chosen order, an expiry). The session runs exactly that order and echoes the nonce in result.nonce. An expired challenge fails with challengeExpired.
  • Image hashes: result.toJson() (and so the uploader's metadata) lists the SHA-256 of every photo and frame, so the server can prove the files weren't swapped.
  • LivenessConfig.attestor: plug in Play Integrity or App Attest through the LivenessAttestor interface. It signs a hash of sessionId|nonce|success|actions|image hashes; the token lands in result.attestation. The package doesn't implement the platform APIs.

Without a server challenge, randomActionCount: 3 makes each session pick 3 actions at random from your list and shuffle them โ€” from all 13 actions that's 1,716 possible sequences instead of 6.

Server verification guide โ†’ covers what your backend should check, with a payload-rebuild snippet. Working client code: the server-bound recipe.

Bring your own anti-spoof model (optional). The package bundles no ML model, but LivenessConfig.frameAnalyzers lets you plug one in (a TFLite/ONNX presentation-attack detector, or a cloud call):

class MyPadModel extends LivenessFrameAnalyzer {
  @override
  String get id => 'pad-v2';

  @override
  Future<double?> analyze(LivenessFrame frame) async {
    // frame.jpeg: the upright full frame; frame.faceBox: the face (0..1).
    return myModel.spoofProbability(frame.jpeg, frame.faceBox); // 0โ€“1
  }
}

It runs on the reference frame and on each action's evidence frame. Scores land in metadata['analyzers']['pad-v2'] and lower confidenceScore by analyzerWeight (default 0.5) ร— the mean spoof probability.

Uploads, in more detail: a non-2xx answer throws LivenessUploadException(statusCode, body); each attempt is limited by timeout (default 60 s); set maxRetries to retry network errors, timeouts and 5xx with exponential backoff (4xx is never retried).

๐Ÿ“ธ Photos, video, and "frame sequence" #

You need this if you want more evidence than one photo per action.

Nothing is captured unless you ask. Choose CaptureType.images (a photo at the peak of each action โ€” the best default), CaptureType.video (a real video file; reliable on iPhones, device-dependent on Android), or CaptureType.frameSequence (several photos a second; works on every phone). For KYC, capture at least images so your server has something to verify.

Capture guide โ†’ โ€” choosing between them, the Android video problem, and media size and cleanup settings.

๐ŸŒˆ Stop video replays: the color-flash challenge #

You need this if you worry about someone playing a video of a real person to the camera โ€” the hardest cheap attack on any action-based check.

enableFlashChallenge: true flashes red, green and blue in a random order after the actions. A real face reflects each color; a replayed video can't know the order. It depends on lighting (strong indoors, weak in daylight), so a failure lowers confidenceScore instead of rejecting the user.

Flash challenge guide โ†’ โ€” what to expect in each environment, how it decides, and how to use the result.

๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Assisted mode: verifying someone else #

You need this if a bank agent or field officer holds the phone and verifies another person โ€” common in branch onboarding and doorstep KYC.

cameraMode: LivenessCameraMode.assisted uses the back camera and the torch. The operator reads each instruction out loud, "left" and "right" mean the subject's, the flash challenge is skipped, and metadata['cameraMode'] tells your backend which mode was used.

Assisted mode guide โ†’ โ€” read it before using this mode.

๐Ÿ“ฆ Everything in the result #

Besides the essentials in step 3, a result carries the server challenge's nonce, the attestor's token, every capture's SHA-256, the frame sequence, the video path and detailed metadata. Full field reference โ†’

๐Ÿ”ง Developer tools #

  • Debug overlay โ€” showDebugOverlay: true shows live head angles, eye/smile probabilities, brightness, and static-feed guard counters on screen, and draws a green box where the detector thinks your face is (if it doesn't sit on your face, please open an issue with your device). Perfect for tuning DetectorTuning thresholds on real devices.
  • Per-action callbacks โ€” onActionStarted / onActionCompleted (sync or async; never awaited, so detection never stalls on your code).
  • Feedback & accessibility โ€” onFeedback fires on action started, half-way, completed, and session passed/failed, for your own sounds, haptics or text-to-speech. hapticFeedback: true adds built-in haptics (handy for eyesClosed, which users can't see finish). Instructions are a screen-reader live region, so each new one is announced.
  • Permission handling in the screen โ€” permissionDeniedBuilder: (context, retry) => ... shows your own page when camera access is denied; calling retry starts a fresh session.
  • Test bench โ€” example/lib/test_bench/ puts every option behind a settings screen, logs every callback live, and runs the server-side checks on each result.

โ“ FAQ #

Does it work offline? Yes. Face detection is Google's on-device ML Kit, and nothing is sent anywhere unless your code sends it.

Do I need a Firebase project or a paid ML Kit plan? No. ML Kit's on-device face detection is free and needs no account or project.

Does it detect masks or deepfakes? Not reliably on its own. The actions, the static-feed guard and the flash challenge stop the cheap attacks (photos, frozen feeds, many replayed videos); masks and deepfakes need a trained anti-spoof model, which you can plug in with frameAnalyzers, and server-side review.

How much does it add to my app? Mostly ML Kit's face-detection model, which ships inside your app. Check the real number for your build with flutter build apk --analyze-size (or ipa on iOS).

Does it store or upload anything? No. Photos are kept in memory until onResult; the video (if you record one) is a temporary file. Where the result goes is entirely up to your code.

Web or desktop? Not yet โ€” Android and iOS only.

๐Ÿ“– Honest notes โ€” read before shipping #

Plain-language notes on the rough edges. Every one has a setting you can change; nothing requires forking the package.

This is not bank-grade security on its own. All checking happens on the phone, and a determined attacker controls their own phone. Treat a passing result as a good first gate, and have your server double-check the photos/video you upload (compare against an ID photo, look for signs of screens or prints). That's exactly why this package captures media during the actions. Server challenges, image hashes and attestation make tampering detectable, but the face itself still has to be judged server-side โ€” see the server verification guide.

Memory adds up if you turn everything up. Photos are kept in memory until the result is delivered. Defaults use roughly 15โ€“35 MB per session. Raising the frame rate and photo size together can OOM cheap Android phones. frameSequenceMaxFrames caps the total as a safety net.

Every face and phone is a little different. How confidently the phone detects a smile or head turn varies with the camera, lighting, glasses, and the person. If one action fails too often for your users, loosen its setting in DetectorTuning. Most sensitive: fullTeethSmile (depends on lip-contour quality) and eyesClosed (brief false "eye open" flickers are ignored; eyesOpenTolerance controls how brief).

drawCircleWithNose is a fun extra, not a workhorse. People must move their whole head in a circle, and small/slow circles don't count. Expect more retries. Test on your users' actual phones before making it mandatory.

Left and right, up and down. lookLeft means the user's left. Calibrated for Android and iPhone; if some device gets it backwards, mirrorYaw: false flips it. invertPitch: true does the same for lookUp, lookDown and nod (the up/down sign hasn't been confirmed on every iPhone yet; showDebugOverlay shows the live pitch).

Give people time โ€” but not forever. Each action has a 15-second limit (actionTimeout), users get 10 seconds to return to a neutral face between actions (neutralTimeout), and the whole session ends after 2 minutes (sessionTimeout, null to disable). For users who find the actions difficult, use fewer/easier actions (blink, smile), longer timeouts, and requireNeutralBetweenActions: false. A second face in the background is ignored if it's small, and only fails the session if it stays for more than half a second (multipleFacesGrace).

It needs light โ€” but it tells the user. Too-dark, overexposed, and blurry frames pause the session with a hint like "Find better lighting". The action's timer keeps running while paused, so a room that stays too dark ends in actionTimeout rather than hanging. Thresholds: brightnessMin, brightnessMax, sharpnessMin; disable with enableQualityChecks: false.

Same person throughout? With ML Kit tracking (on unless an action needs contours), a face ID that changes while a face stays in view lowers the confidence score and shows up in metadata['identity_*'], along with jumps in rough face geometry. Set failOnFaceChange: true to fail such sessions outright. It's off by default because a very fast head turn can make some devices re-assign the ID. This is no substitute for server-side face matching.

The anti-spoof checks are honest heuristics, not magic. The static-feed guard (called the replay guard before 0.5; the setting is still enableReplayGuard) catches input that can't come from a live camera: pixel-identical frames fail the session, and near-identical frames with a frozen face box (a re-encoded still injected as a camera) lower the confidence score. Micro-motion flags unnaturally still sessions the same way. None of it detects a photo, screen or video held up to a real camera: the real camera adds real noise and the hand adds real motion. That's what the actions, the color flash, and server-side review of the captured media are for. If the guard ever misfires on a device (it shouldn't โ€” real sensors are noisy), enableReplayGuard: false turns it off.

๐Ÿงช Status: beta #

liveness_flutter is in beta. It's tested on real Android and iOS devices, but it's still evolving and the API may change before 1.0. Until then, a minor release (0.5 โ†’ 0.6) can include breaking changes; each one is listed under Breaking in the CHANGELOG. Depend on ^0.5.0 and you'll only get compatible updates.

Anyone can open an issue โ€” bugs, a phone where detection misbehaves, confusing docs, or ideas. For detection problems, include your phone model, OS version, and what showDebugOverlay: true shows.


Found a bug? Have an idea? Open an issue โ€” anyone can, and PRs are welcome too. ๐Ÿ™Œ

Made with โค๏ธ for developers who'd rather not pay per verification.

1
likes
160
points
759
downloads

Documentation

API reference

Publisher

verified publisherhenryifebunandu.dev

Weekly Downloads

Beta: Plug-and-play liveness detection for Flutter. Configure action sequences like blink and smile, capture images or video, and send verification results to your backend.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

camera, crypto, flutter, google_mlkit_face_detection, http, image, screen_brightness

More

Packages that depend on liveness_flutter