๐ก๏ธ liveness_flutter
Face liveness checks for Flutter โ free, on-device, and built for your backend.
Blink. Smile. Turn left. Verified. โ
![]() Out of the box โ about 10 lines of code |
![]() 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
- The camera streams frames, and Google's ML Kit finds the face โ on the phone, offline.
- 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.
onResultgives you oneLivenessResult: pass or fail, a confidence score, and the photos if you asked for them.- 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()),
);
โ ๏ธ
onResultis called exactly once per session โ even if the screen is closed before the check finishes (system back, route popped). In that case the result iscancelledwithmetadata['cancelledBy'] == 'dispose'and arrives just after the screen is gone, which is why the guard above matters.cancelledByis'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 andkind(reference, orpeak: 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
LinearProgressIndicatordriven byonProgress.
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.
The full flow on an iPhone.
- Account home โ balance card, current tier, "Upgrade to Tier 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.
- 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. - Verifying โ success or failure โ success unlocks Tier 2; failure
explains what went wrong for each
LivenessFailureReasonand 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.
๐ Bind sessions to your server (recommended for KYC)
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 aLivenessChallenge(single-use nonce, the actions in its chosen order, an expiry). The session runs exactly that order and echoes the nonce inresult.nonce. An expired challenge fails withchallengeExpired.- Image hashes:
result.toJson()(and so the uploader'smetadata) 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 theLivenessAttestorinterface. It signs a hash ofsessionId|nonce|success|actions|image hashes; the token lands inresult.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: trueshows 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 tuningDetectorTuningthresholds on real devices. - Per-action callbacks โ
onActionStarted/onActionCompleted(sync or async; never awaited, so detection never stalls on your code). - Feedback & accessibility โ
onFeedbackfires on action started, half-way, completed, and session passed/failed, for your own sounds, haptics or text-to-speech.hapticFeedback: trueadds built-in haptics (handy foreyesClosed, 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; callingretrystarts 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.
Libraries
- liveness_flutter
- Plug-and-play, customizable liveness detection for Flutter.
