Feature spec
BeginnerCategory
scanning-devices
Build with AI
1-3 hours with FlutterFlow
Custom build
1-2 days custom dev
Running cost
$0/mo (on-device scanning) · $0-25/mo with Supabase scan history
Works on
Everything it takes to ship a QR Scanner — parts, prompts, and real costs.
A QR scanner needs three pieces: a continuous camera detector (mobile_scanner in FlutterFlow — wraps ML Kit on Android and AVFoundation on iOS), a handler that routes the decoded value by type (URL, text, payment, WiFi), and a Supabase table for scan history with RLS. With FlutterFlow you can ship a working QR scanner in 1-3 hours for $0/month — ML Kit runs entirely on-device at no cost. The only recurring cost is Supabase for scan history storage, which starts free.
What a QR Scanner Feature Actually Is
A QR scanner opens the device camera, continuously detects QR codes in the frame without any tap, and routes the decoded content to the appropriate action. A URL opens in an in-app WebView or the system browser. A payment code triggers a checkout flow. An event ticket QR validates against your database. A WiFi code offers to join the network. The decoding itself is free and on-device — mobile_scanner on Flutter uses Google ML Kit on Android and Apple's AVFoundation on iOS, both built into the operating system. The product decisions are what happens with the result: how you handle each content type, whether you store a history, and how you prevent the same code from being processed multiple times per second.
What users consider table stakes in 2026
- Camera opens within one second of tapping the scan button — no perceptible delay before the viewfinder appears
- QR code is detected automatically in continuous mode without the user needing to tap to trigger a scan
- Visual confirmation when a QR is detected — a highlight overlay on the code or a green flash before any action is taken
- Deep link handling that correctly routes URLs to an in-app WebView and custom app schemes to the right screen
- Scan history listing the last 20 scans with timestamp, decoded value, and the action taken
- Torch/flashlight toggle button visible in the viewfinder overlay for scanning in low light
Anatomy of the QR Scanner Feature
Six components. The deep link handler and the permission fallback state are where AI-built versions most commonly fall short.
Camera viewfinder and QR detector
UImobile_scanner Flutter package provides a MobileScanner widget that streams the camera feed and continuously detects QR codes using ML Kit on Android and AVFoundation on iOS — all on-device, no API key, no internet required. On web output from FlutterFlow, the scanner falls back to html5-qrcode via a JSChannel bridge, but FlutterFlow web scanner output is unreliable.
Note: ML Kit and AVFoundation detect QR codes at hardware speed — typically under 100ms per frame. There is no server round-trip for detection; the entire decode happens on the device.
Deep link handler
BackendRoutes the decoded QR value by content type. If the value starts with http:// or https://, launch it in a WebView (webview_flutter package) or the system browser via url_launcher. If it matches a custom app scheme (myapp://screen/id), pass it to Flutter's GoRouter for in-app navigation. If it is a WiFi code (WIFI:S:...), parse and offer to connect. If it is plain text, copy to clipboard and display. Unknown types fall back to a display-and-copy screen.
Note: Route QR values through GoRouter before launching any WebView — handle custom app schemes first, then fall back to WebView for http/https. Custom schemes sent directly to WebView result in a 404.
Scan history store
DataSupabase table qr_scan_history storing user_id, qr_value, qr_type (url/text/payment/wifi/unknown), scanned_at, and action_taken. RLS policy restricts read and write to the authenticated user. An optional Hive local cache stores the last 20 scans for offline history access without a network request.
Note: qr_type is a text enum you define — it powers the history screen's filter tabs and helps users find specific scan types quickly.
Torch controller
UIMobileScannerController.toggleTorch() toggles the device flashlight on and off. An icon button in the viewfinder overlay (typically a lightning bolt icon) calls this method. Torch state is tracked in a Flutter Riverpod or Bloc provider so the button icon reflects the current state. The built-in FlutterFlow Scan action does not expose torch control — it requires a Custom Action.
Note: Torch availability varies by device. Check MobileScannerController.hasTorch before rendering the torch button — some tablets and front-facing cameras lack a flash.
Permission handler
UIpermission_handler package requests camera permission on first launch with a rationale dialog explaining why camera access is needed. On Android, if the user taps 'Never Ask Again', the app cannot re-request permission and must show an 'Open Settings' button calling openAppSettings(). On iOS, NSCameraUsageDescription in Info.plist is required or the app crashes on camera open.
Note: Check Permission.camera.isPermanentlyDenied before rendering the camera screen. If permanently denied, skip the viewfinder entirely and show the settings prompt — attempting to open the camera with permanently denied permission causes a MissingPluginException.
Scan feedback
UIOn successful QR detection: HapticFeedback.mediumImpact() provides tactile confirmation before any navigation occurs. An optional beep sound via the audioplayers package provides audio feedback. A 500ms duplicate detection window (bool flag or last-scanned-value ref) prevents the same code from triggering multiple actions per scan.
Note: Give haptic feedback before any async operation. If the deep link lookup or Supabase insert runs first, the user feels lag between pointing at the QR and getting confirmation — the camera should feel instant.
The data model
One table stores scan history per user with RLS. Run this in the Supabase SQL editor (Dashboard → SQL Editor → New query):
1create table public.qr_scan_history (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 qr_value text not null,5 qr_type text not null default 'unknown',6 scanned_at timestamptz not null default now(),7 action_taken text8);910alter table public.qr_scan_history enable row level security;1112create policy "Users can view own scan history"13 on public.qr_scan_history for select14 using (auth.uid() = user_id);1516create policy "Users can insert own scan history"17 on public.qr_scan_history for insert18 with check (auth.uid() = user_id);1920create policy "Users can delete own scan history"21 on public.qr_scan_history for delete22 using (auth.uid() = user_id);2324create index qr_scan_history_user_recent_idx25 on public.qr_scan_history (user_id, scanned_at desc);Heads up: The composite index on (user_id, scanned_at DESC) keeps the history screen fast even after thousands of scans. The qr_type column (url, text, payment, wifi, unknown) enables filter tabs on the history screen without a full table scan.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Best path: FlutterFlow has a native Scan Barcode / QR Action in the Actions panel that wraps mobile_scanner — point-and-click setup for iOS and Android with zero code for the basic scan flow.
Step by step
- 1Add a new Page with a full-screen camera view; in the Action editor on the Scan button, select Camera → Scan Barcode/QR and store the result in a Page State variable (type: String)
- 2Add a conditional action after the scan: check if the qr_value starts with 'http' and launch url_launcher, otherwise display the value in a bottom sheet
- 3Add a Supabase Backend Action to INSERT a row into qr_scan_history with user_id (from auth.currentUser.uid), qr_value (from Page State), qr_type (computed), and scanned_at (now())
- 4Enable camera permissions in Settings → App Details → iOS Permissions: add NSCameraUsageDescription with a description like 'Camera is used to scan QR codes' — without this string the app crashes on iOS
- 5Add a History page with a Supabase backend query fetching the current user's qr_scan_history rows newest first, bound to a ListView widget
Where this path bites
- The built-in Scan Barcode / QR action scans once per tap and closes the camera — continuous auto-detect mode requires a Custom Action using MobileScannerController with a stream listener
- Torch toggle is not exposed by the built-in action — requires a one-line Custom Action calling MobileScannerController.toggleTorch()
- QR type detection (URL vs WiFi vs vCard vs plain text) and type-specific routing require conditional logic in the Action editor or a Custom Action for complex parsing
Third-party services you'll need
QR scanning is entirely free and on-device. The only optional cost is Supabase for scan history storage:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| mobile_scanner (pub.dev) | Flutter package wrapping ML Kit (Android) and AVFoundation (iOS) for continuous hardware-accelerated QR detection | Open source, free | Free |
| Google ML Kit (Android) | On-device QR and barcode detection engine — free, bundled with Google Play Services | Free, no API key | Free |
| AVFoundation (iOS) | Apple's native camera and vision framework providing QR detection on iOS — built into the OS | Free, included in iOS | Free |
| permission_handler (pub.dev) | Flutter package for requesting and checking camera permission with permanent-denial handling | Open source, free | Free |
| Supabase | qr_scan_history table and RLS for per-user scan logs across sessions | Free tier: 2 projects, 500MB DB | Pro $25/mo (approx) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Entirely on-device scanning — ML Kit and AVFoundation are free with no usage limits. Supabase free tier easily handles scan history for 100 users. No third-party scanning API fees.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
FlutterFlow Scan Action fires once then stops
Symptom: FlutterFlow's built-in Scan Barcode / QR action opens the camera, waits for the first successful scan, then immediately closes the viewfinder. There is no option to keep scanning continuously — every subsequent scan requires the user to tap the scan button again. Apps built for inventory or check-in use cases where users scan many codes in sequence feel broken with this behaviour.
Fix: For continuous scanning, write a Custom Action in FlutterFlow using MobileScannerController with a barcodeStream listener that keeps the camera open. Set a processing lock bool to prevent multiple simultaneous actions from the same scan. This requires Dart code but is a short, well-documented pattern.
Camera permission permanently denied with no recovery
Symptom: On Android, if the user taps 'Don't Ask Again' when the permission dialog appears, the system will never show the permission dialog again — calling Permission.camera.request() silently returns denied. AI tools rarely handle this state and show a broken empty scanner screen instead of a recovery path.
Fix: Before rendering the camera widget, call Permission.camera.isPermanentlyDenied from permission_handler. If true, render a screen with an 'Open App Settings' button that calls openAppSettings() — this opens the device Settings screen where the user can manually re-enable camera access. In FlutterFlow, this is a Custom Action.
Same QR code detected multiple times fires duplicate actions
Symptom: mobile_scanner's BarcodeCapture stream delivers a result every frame the QR code is in view — typically 15-30 times per second. Without a guard, a single scan triggers a URL to open 30 times, inserts 30 history rows, and makes the app unusable. AI tools often omit the cooldown.
Fix: Use a boolean flag _isProcessing that is set to true on the first valid scan and reset after 1 second (or after the action completes). In the stream handler, return early if _isProcessing is true. Alternatively, store the last scanned value and timestamp in a ref and reject any scan of the same value within 500ms.
Deep links from QR codes fail silently in WebView
Symptom: When a QR code contains a custom deep link (myapp://products/123) and the app opens it in a webview_flutter WebView, the WebView does not understand the custom scheme — it attempts to load myapp://products/123 as a URL and shows an error page or blank screen. The user sees a broken result.
Fix: Route all QR values through your app's router before deciding how to handle them. Check for custom app schemes first using a RegExp or Uri.parse(). Pass matching schemes to GoRouter.go() for in-app navigation. Only fall back to webview_flutter for http:// and https:// URLs. Never send a custom scheme to a WebView.
iOS camera crash when NSCameraUsageDescription is missing
Symptom: Apple requires a NSCameraUsageDescription entry in the app's Info.plist explaining why camera access is needed. Flutter apps built and submitted without this string crash immediately when the camera is opened on iOS, with a CameraException thrown before the scanner even appears. FlutterFlow projects created before this became enforced may not have it set.
Fix: Add NSCameraUsageDescription in FlutterFlow under Settings → App Details → iOS Permissions. Write a specific usage string: 'Camera is used to scan QR codes for [your use case].' Generic descriptions like 'Camera access required' or missing entries lead to App Store rejection. Verify the string is present in Xcode before submitting.
Best practices
Implement a 500ms duplicate detection cooldown on every QR scan — mobile_scanner fires the callback dozens of times per second on a held code and will trigger your action repeatedly without a guard
Give haptic feedback (HapticFeedback.mediumImpact()) the instant a QR is detected, before any database insert or URL open — makes the scanner feel instantaneous even if the subsequent async operation takes 200ms
Route QR values by content type before acting on them — at minimum distinguish URLs (open WebView), plain text (display and copy), and your app's own codes (in-app navigation)
Show the torch toggle only when the device has a torch — check MobileScannerController.hasTorch and hide the button on devices without a flash rather than showing a broken toggle
Log every scan to history with the action taken — users frequently need to retrieve a URL they scanned an hour ago and have no other way to find it
Stop the camera stream when the scanner screen is popped from the navigation stack — active camera sessions consume significant battery and keep the camera LED lit
Write NSCameraUsageDescription with your specific use case before every App Store submission — generic or missing descriptions are one of the most common reasons for iOS review rejection
When You Need Custom Development
FlutterFlow covers the standard scan-once-and-act pattern. These requirements consistently push past what the visual builder delivers:
- Simultaneous multi-code detection in a single frame — warehouse inventory picking where a shelf label contains multiple barcodes to capture in one camera pass
- Offline QR validation against a locally stored dataset with no network access — event ticket validation in venues with poor connectivity
- Real-time QR scanning in a video stream with bounding box overlays for augmented reality applications — requires custom camera rendering beyond MobileScanner's standard viewfinder
- Proprietary or custom QR encoding schemes where the decoded payload requires a custom parser before routing
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
What is the easiest way to add QR scanning to a FlutterFlow app?
Use the built-in Scan Barcode / QR action in the Action editor under Camera → Scan Barcode/QR. It wraps mobile_scanner and works on both iOS and Android with no custom code. Store the result in a Page State variable, then add conditional actions to handle the scanned value. You will need one Custom Action to add torch control and another if you want continuous scanning instead of one-shot.
Why does the QR scanner fire multiple times for the same code?
mobile_scanner continuously detects QR codes at camera frame rate — typically 15-30 times per second while the code is in view. Without a guard, every detected frame triggers your action handler independently. Fix: use a bool flag _isProcessing set to true on the first valid scan and reset after 1 second, or store the last scanned value and reject repeats within 500ms.
How do I add a flashlight toggle to a QR scanner app?
The FlutterFlow built-in Scan action does not expose torch control. Create a Custom Action with this Dart code: MobileScannerController controller = MobileScannerController(); await controller.toggleTorch(); Call it from an icon button in the viewfinder overlay. Check MobileScannerController.hasTorch before rendering the button — some devices do not have a flash accessible to the front camera.
Can I build a QR scanner that works in a web browser without a native app?
Yes, using html5-qrcode in a React or Next.js web app built with Lovable or V0. It uses the browser's getUserMedia() API to access the camera and decodes QR codes client-side. Limitations on web: no torch access, no haptic feedback, and camera requires HTTPS and is blocked in preview iframes. For a native feel and hardware-speed scanning, FlutterFlow's mobile_scanner is the better choice.
How do I parse the different types of QR code content?
Parse the decoded string by prefix. URLs start with http:// or https:// — open in a WebView or url_launcher. WiFi codes start with WIFI:S: — parse the SSID and security type from the string. vCard contacts start with BEGIN:VCARD. Email addresses start with mailto:. Phone numbers start with tel:. Everything else is plain text. Handle each type with a different action route and display the type badge in the scan history.
Why does the camera open but the QR code is never detected?
Three common causes: (1) The camera is physically too close — most phone cameras need 10-20cm distance to focus on a QR code. Show a 'Move back slightly' hint. (2) The format configuration is wrong — if you configured a barcode scanner for EAN_13 only and then try to scan a QR code, nothing fires. Ensure QR_CODE is in the formats list. (3) Lighting is too low — the torch toggle exists for this. Prompt users to tap the torch button in dim environments.
How do I store a history of scanned QR codes in Supabase?
Create a qr_scan_history table with columns: id (uuid), user_id (uuid FK to auth.users), qr_value (text), qr_type (text), scanned_at (timestamptz, default now()), action_taken (text). Enable RLS with a policy restricting select and insert to auth.uid() = user_id. In FlutterFlow, add a Supabase Backend Action after the scan action to INSERT the row. Run the SQL schema from this page in your Supabase SQL editor.
What iOS Info.plist settings are required for camera access?
NSCameraUsageDescription is required — without it, the app crashes on iOS when the camera is first opened. In FlutterFlow, add it under Settings → App Details → iOS Permissions with a description specific to your use case. Example: 'Camera is used to scan QR codes for event check-in.' Apple's review team rejects apps with generic descriptions like 'For camera access' or apps where the stated purpose does not match the actual camera usage in the app.
Need this feature production-ready?
RapidDev builds a qr scanner into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.