Proof of delivery photo and signature
Capture a recipient signature on a canvas, export it as PNG bytes and pair it with a photo from the camera.
FlutterUploadsfluttersignaturecameradelivery
Code
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class SignatureController extends ChangeNotifier {
final List<List<Offset>> strokes = [];
bool get isEmpty => strokes.isEmpty;
void start(Offset p) { strokes.add([p]); notifyListeners(); }
void extend(Offset p) { strokes.last.add(p); notifyListeners(); }
void clear() { strokes.clear(); notifyListeners(); }
/// Renders the strokes to PNG bytes on a white background.
Future<Uint8List> toPng(Size size) async {
final rec = ui.PictureRecorder();
final canvas = Canvas(rec, Offset.zero & size);
canvas.drawRect(Offset.zero & size, Paint()..color = Colors.white);
_paint(canvas);
final img = await rec.endRecording().toImage(size.width.toInt(), size.height.toInt());
final data = await img.toByteData(format: ui.ImageByteFormat.png);
return data!.buffer.asUint8List();
}
void _paint(Canvas canvas) {
final pen = Paint()..color = Colors.black..strokeWidth = 3..strokeCap = StrokeCap.round..style = PaintingStyle.stroke;
for (final s in strokes) {
final path = Path()..moveTo(s.first.dx, s.first.dy);
for (final p in s.skip(1)) { path.lineTo(p.dx, p.dy); }
if (s.length == 1) canvas.drawPoints(ui.PointMode.points, s, pen);
canvas.drawPath(path, pen);
}
}
}
class _Painter extends CustomPainter {
_Painter(this.c) : super(repaint: c);
final SignatureController c;
@override
void paint(Canvas canvas, Size size) => c._paint(canvas);
@override
bool shouldRepaint(_Painter old) => false; // repaint comes from the listenable
}
class ProofCapture extends StatefulWidget {
const ProofCapture({super.key, required this.onSubmit});
final Future<void> Function(Uint8List signature, XFile photo) onSubmit;
@override
State<ProofCapture> createState() => _ProofCaptureState();
}
class _ProofCaptureState extends State<ProofCapture> {
final _sig = SignatureController();
final _padKey = GlobalKey();
XFile? _photo;
Future<void> _submit() async {
final size = (_padKey.currentContext!.findRenderObject() as RenderBox).size;
await widget.onSubmit(await _sig.toPng(size), _photo!);
}
@override
Widget build(BuildContext context) {
return Column(children: [
Container(
key: _padKey,
height: 180,
decoration: BoxDecoration(border: Border.all(), borderRadius: BorderRadius.circular(8)),
child: GestureDetector(
onPanStart: (d) => _sig.start(d.localPosition),
onPanUpdate: (d) => _sig.extend(d.localPosition),
child: CustomPaint(painter: _Painter(_sig), size: Size.infinite),
),
),
Row(children: [
TextButton(onPressed: _sig.clear, child: const Text('Clear')),
TextButton.icon(
icon: const Icon(Icons.photo_camera),
label: Text(_photo == null ? 'Take photo' : 'Retake photo'),
onPressed: () async {
final x = await ImagePicker().pickImage(source: ImageSource.camera, maxWidth: 1600, imageQuality: 80);
if (x != null) setState(() => _photo = x);
},
),
]),
ListenableBuilder(
listenable: _sig,
builder: (_, _) => FilledButton(
onPressed: _photo != null && !_sig.isEmpty ? _submit : null,
child: const Text('Confirm delivery'),
),
),
]);
}
}
Needs: image_picker@^1.1.0
How it works
What it solves
Couriers need evidence that a parcel reached the right person: a signature and a photo of the parcel at the door. Both must be captured offline-friendly on the device and sent as ordinary files.
How it works
SignatureController stores strokes as lists of points and notifies when they change. A CustomPainter that listens to it repaints only the pad. toPng replays the same drawing routine onto a PictureRecorder canvas with a white background and converts the image to PNG bytes, so what is saved matches what is on screen.
The photo comes from image_picker with a size cap and JPEG quality. The Confirm button listens to the signature controller and stays disabled until both a photo and at least one stroke exist. A single tap draws a dot via PointMode.points.
Adapting it
Upload the PNG bytes and photo as multipart fields and include the capture time and coordinates in the same request. Add a recipient name field, or offer an alternative one-time code entry for contactless drops. Increase the stroke width for tablets.
Pitfalls
- The export uses the pad's laid-out size, so measure it after layout as done through the key.
- Strokes are in logical pixels; export with a larger image if the server needs high resolution.
- Declare camera permissions on both platforms.
How to use it
Add image_picker and copy the file to lib/proof/. Add camera permission strings to the iOS and Android configuration.
ProofCapture(onSubmit: (png, photo) => api.uploadProof(png, photo))
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.