← Flutter / Dart

Flutter / Dart · 18 / 18

Scanning and OCR: suggestion, confirmation and persistence

Build a testable capture workflow and separate recognised text from confirmed data.

  • Large
  • Advanced
  • 120 min

Prerequisites: Restore encrypted backups safely

OCR recognises text from an image. A recognised word is not a confirmed reading. Photos may contain serial numbers, dates and several numbers. Meter Reading Log therefore generates candidates and requires user confirmation. Vehicle File also uses a document scanner for PDF receipts without automatically analysing invoices. We model the shared workflow through a port and first test with controlled recognition.

Example files

01Define the recognition boundary

OcrPort returns recognised text lines. The learning adapter supplies fixed lines rather than a camera photo. This lets us run the domain workflow on any computer. It does not prove actual OCR quality.

capture.dart · dartOpen file
abstract interface class OcrPort {
  Future<List<String>> recognize(String image);
}

class LearningOcr implements OcrPort {
  @override
  Future<List<String>> recognize(String image) async => [
    'Meter 12345',
    'Reading 64820.5 kWh',
  ];
}

class CaptureService {
  CaptureService(this.ocr);
  final OcrPort ocr;
  final saved = <String>[];
  Future<List<String>> propose(String image) async {
    final lines = await ocr.recognize(image);
    final pattern = RegExp(r'\b\d{1,6}(?:[.,]\d{1,3})?\b');
    return [
      for (final line in lines)
        for (final match in pattern.allMatches(line)) match.group(0)!,
    ];
  }

  void confirm(String candidate, {required bool accepted}) {
    if (!accepted) return;
    if (!RegExp(r'^\d{1,6}(?:[.,]\d{1,3})?$').hasMatch(candidate))
      throw ArgumentError('Invalid value');
    saved.add(candidate.replaceAll(',', '.'));
  }
}

Future<void> main() async {
  final service = CaptureService(LearningOcr());
  final candidates = await service.propose('synthetic-photo');
  if (service.saved.isNotEmpty) throw StateError('Saved before confirmation');
  service.confirm(candidates.last, accepted: false);
  if (service.saved.isNotEmpty) throw StateError('Saved after rejection');
  service.confirm(candidates.last, accepted: true);
  print(service.saved);
}

Run / check

dart run capture.dart

02Generate candidates

The small regex finds numbers with an optional decimal part. It explicitly does not replace the real candidate extractor, which also considers lines, positions and separated digit groups. The learning example may return multiple suggestions.

03Keep confirmation as a separate action

propose writes no reading. confirm requires a candidate and an explicit decision. A real interface displays photo and value together, permits correction and only then validates the domain rule.

04Handle resources and cancellation

The real ML Kit adapter closes TextRecognizer in finally. Cancellation must not secretly confirm a draft. The service needs to distinguish camera failure, missing plugins and invalid results. The interface retains a useful retry path.

05Distinguish scanning from OCR

Vehicle File’s document scanner creates a PDF with editable scanned pages. The app deliberately copies the result into a record. Meter OCR instead generates a value suggestion. Both need device adapters but produce different domain results.

Expected result

Before confirmation, saved is empty. Rejection leaves it empty. Confirmation adds exactly the selected value.

06Verify the real device workflow

On Android, test a clear photo, a blurred photo, missing permission, cancellation and multiple numbers. Check that no reading exists before confirmation. Browser tests do not replace this evidence. Google ML Kit scanner functions have the device and Play Services requirements documented for the product.

Data flow
  1. Photo / scan
  2. Device port
  3. OCR candidates
  4. User review
  5. Domain rule / save

Your exercise

Add an OcrPort that throws an error. Verify that saved stays unchanged.

Show the solution and explanation

The exception must occur before confirm. Catch it at the application boundary and offer capture retry or manual input. Do not store an invented zero. An additional test checks a rejected candidate and an explicitly corrected value.

Project context and sources

The example is simplified for learning. The linked source version shows its use in the project and the checks applied there.