Build a drawing and annotation editor using a Custom Widget with the image_painter package. ImagePainter.memory(imageBytes) creates an interactive canvas where users draw with finger or mouse, add text, and place shapes. The toolbar provides pen, text, shape, color picker, stroke width, undo, and clear actions via ImagePainterController. Export the annotated result as PNG bytes with controller.exportImage() and upload to Firebase Storage.
Building an Image Annotation Editor in FlutterFlow
Photo annotation is essential for document signing, marking up screenshots, and feedback workflows. This tutorial builds a full image editor Custom Widget with drawing tools, text placement, shapes, undo, and PNG export — all wired into FlutterFlow's visual builder.
Prerequisites
- A FlutterFlow project on the Pro plan (Custom Code required)
- Firebase Storage enabled for saving annotated images
- An image source — either a Firebase Storage URL or a local asset to annotate
Step-by-step guide
Add image_painter ^0.7.0 to Pubspec Dependencies
Add image_painter ^0.7.0 to Pubspec Dependencies
Open Settings → Custom Code → Pubspec Dependencies and add image_painter: ^0.7.0. This package provides an interactive canvas with built-in drawing, text, shapes, color picking, stroke control, undo/redo, and PNG export. It works on both web and mobile.
Expected result: The image_painter package appears in the dependency list and resolves without errors on next build.
Create the ImageEditorWidget Custom Widget with ImagePainter.memory
Create the ImageEditorWidget Custom Widget with ImagePainter.memory
Create a new Custom Widget named ImageEditorWidget. Add a Component Parameter imageBytes (Uint8List) for the source image. In the build method, return a Column containing an Expanded child with ImagePainter.memory(widget.imageBytes, controller: _controller, scalable: true). Store the ImagePainterController as a class field so toolbar buttons can call its methods. Set width and height to double.infinity so the canvas fills its parent.
Expected result: The Custom Widget renders the source image on an interactive canvas that responds to touch and mouse drawing.
Wire up toolbar Row with pen, text, rectangle, circle, and line IconButtons
Wire up toolbar Row with pen, text, rectangle, circle, and line IconButtons
Below the Expanded ImagePainter, add a Row with mainAxisAlignment: spaceEvenly. Add five IconButtons: edit (pen), text_fields (text), crop_square (rectangle), circle_outlined (circle), show_chart (line). On each button tap, call _controller.setMode(PaintMode.freeStyle), _controller.setMode(PaintMode.text), etc. Highlight the active tool by comparing the current mode to the button's mode and setting color to Theme primary.
Expected result: Tapping each toolbar icon switches the drawing mode — pen draws freehand, text places editable text, shapes draw the selected shape.
Add color picker IconButton and stroke width Slider to the toolbar
Add color picker IconButton and stroke width Slider to the toolbar
Add a Container with a colored circle (current color) as an IconButton. On tap, show a simple grid of 8 color swatches (red, blue, green, black, white, yellow, orange, purple) in a Wrap inside a Show Dialog. On swatch tap, call _controller.setColor(selectedColor) and close the dialog. Below the toolbar Row, add a Slider (min: 1, max: 20, divisions: 19) bound to a local _strokeWidth variable. On changed, call _controller.setStrokeWidth(value).
Expected result: Users can pick a drawing color from the swatch grid and adjust stroke thickness with the Slider.
Implement undo, clear, and export-to-Firebase-Storage buttons
Implement undo, clear, and export-to-Firebase-Storage buttons
Add three more IconButtons: undo (calls _controller.undo()), delete_outline (calls _controller.clearAll()), and save (calls _controller.exportImage()). The exportImage() method returns a Uint8List of the annotated PNG. On save tap: export → upload the bytes to Firebase Storage at path annotations/{userId}/{timestamp}.png using FirebaseStorage.instance.ref(path).putData(bytes) → get the download URL → pass the URL back to the parent page via an Action Parameter callback.
Expected result: Undo removes the last stroke, clear resets the canvas, and save uploads the annotated image to Firebase Storage and returns the download URL.
Complete working example
1// Custom Widget: ImageEditorWidget2// Pubspec: image_painter: ^0.7.034import 'dart:typed_data';5import 'package:flutter/material.dart';6import 'package:image_painter/image_painter.dart';7import 'package:firebase_storage/firebase_storage.dart';89class ImageEditorWidget extends StatefulWidget {10 final double width;11 final double height;12 final Uint8List imageBytes;13 final Future Function(String downloadUrl)? onSave;1415 const ImageEditorWidget({16 Key? key,17 required this.width,18 required this.height,19 required this.imageBytes,20 this.onSave,21 }) : super(key: key);2223 @override24 State<ImageEditorWidget> createState() => _ImageEditorWidgetState();25}2627class _ImageEditorWidgetState extends State<ImageEditorWidget> {28 final _controller = ImagePainterController(29 color: Colors.red,30 strokeWidth: 4.0,31 mode: PaintMode.freeStyle,32 );33 double _strokeWidth = 4.0;34 PaintMode _activeMode = PaintMode.freeStyle;3536 final _colors = [37 Colors.red, Colors.blue, Colors.green, Colors.black,38 Colors.white, Colors.yellow, Colors.orange, Colors.purple,39 ];4041 @override42 Widget build(BuildContext context) {43 return SizedBox(44 width: widget.width,45 height: widget.height,46 child: Column(children: [47 Expanded(48 child: ImagePainter.memory(49 widget.imageBytes,50 controller: _controller,51 scalable: true,52 ),53 ),54 // Toolbar row55 Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [56 _toolBtn(Icons.edit, PaintMode.freeStyle),57 _toolBtn(Icons.text_fields, PaintMode.text),58 _toolBtn(Icons.crop_square, PaintMode.rect),59 _toolBtn(Icons.circle_outlined, PaintMode.circle),60 _toolBtn(Icons.show_chart, PaintMode.line),61 // Color picker62 IconButton(63 icon: Icon(Icons.palette,64 color: _controller.color),65 onPressed: _showColorPicker,66 ),67 IconButton(icon: const Icon(Icons.undo), onPressed: _controller.undo),68 IconButton(icon: const Icon(Icons.delete_outline), onPressed: _controller.clearAll),69 IconButton(icon: const Icon(Icons.save), onPressed: _exportAndUpload),70 ]),71 // Stroke width slider72 Slider(73 value: _strokeWidth,74 min: 1, max: 20, divisions: 19,75 onChanged: (v) {76 setState(() => _strokeWidth = v);77 _controller.setStrokeWidth(v);78 },79 ),80 ]),81 );82 }8384 Widget _toolBtn(IconData icon, PaintMode mode) {85 return IconButton(86 icon: Icon(icon,87 color: _activeMode == mode88 ? Theme.of(context).primaryColor89 : Colors.grey),90 onPressed: () {91 setState(() => _activeMode = mode);92 _controller.setMode(mode);93 },94 );95 }9697 void _showColorPicker() {98 showDialog(99 context: context,100 builder: (_) => AlertDialog(101 content: Wrap(102 spacing: 8,103 children: _colors.map((c) => GestureDetector(104 onTap: () {105 _controller.setColor(c);106 Navigator.pop(context);107 },108 child: CircleAvatar(backgroundColor: c, radius: 18),109 )).toList(),110 ),111 ),112 );113 }114115 Future<void> _exportAndUpload() async {116 final bytes = await _controller.exportImage();117 if (bytes == null) return;118 final ref = FirebaseStorage.instance119 .ref('annotations/${DateTime.now().millisecondsSinceEpoch}.png');120 await ref.putData(bytes);121 final url = await ref.getDownloadURL();122 widget.onSave?.call(url);123 }124}Common mistakes when building a Custom Image Editor in FlutterFlow
Why it's a problem: Not providing an undo button on the annotation toolbar
How to avoid: Always expose _controller.undo() and _controller.clearAll() as IconButtons in the toolbar so users can step back or reset.
Why it's a problem: Forgetting to call controller.dispose() in the widget dispose method
How to avoid: Override dispose() in your Custom Widget State class and call _controller.dispose() before super.dispose().
Why it's a problem: Passing a network URL string instead of Uint8List bytes to ImagePainter.memory
How to avoid: Fetch the image bytes first using http.get(Uri.parse(url)) and pass response.bodyBytes to the widget, or use ImagePainter.network(url) instead.
Best practices
- Use ImagePainter.memory for images already in memory, ImagePainter.network for remote URLs
- Highlight the active tool icon with the Theme primary color so users know which mode is selected
- Set a sensible default stroke width (3-5 pixels) — too thin is invisible, too thick obscures content
- Show a loading spinner while exportImage() processes and uploads to prevent double-tap saves
- Limit the color palette to 8-12 high-contrast swatches rather than a full color wheel for faster picking
- Wrap the Custom Widget in an AspectRatio to prevent canvas distortion on different screen sizes
- Pass the exported download URL back to the parent via an Action Parameter callback for flexibility
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to build an image editor in FlutterFlow using the image_painter package. Show me the full Custom Widget code with drawing tools (pen, text, rectangle, circle, line), a color picker, stroke width slider, undo, clear, and export to Firebase Storage.
Add a Custom Widget that lets users draw on an image with pen, text, and shapes. Include a toolbar with undo and save buttons.
Frequently asked questions
Can I load an image from Firebase Storage into the editor?
Yes. Fetch the image bytes using http.get(Uri.parse(storageUrl)), then pass response.bodyBytes as the imageBytes parameter to the Custom Widget.
Does image_painter support pinch-to-zoom on the canvas?
Yes. Set scalable: true on ImagePainter and users can pinch to zoom in and out while annotating.
How do I add a text annotation with a custom font size?
Call _controller.setMode(PaintMode.text) and _controller.setStrokeWidth(fontSize). The stroke width parameter doubles as the font size for text mode.
Can I save the drawing as SVG instead of PNG?
No. image_painter exports only raster PNG via exportImage(). For SVG export, you would need a different package like flutter_drawing_board.
How do I pre-load annotations so users can resume editing?
image_painter does not support loading previous annotations. Save the final exported PNG each time and let users annotate on top of the previously saved image.
Can RapidDev help build a production document signing flow?
Yes. RapidDev can implement a full document annotation pipeline with signature capture, multi-page PDF markup, version history, and approval workflows.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation