Building a Drawing App in Flutter
Learn how to create a drawing app in Flutter and explore Flutter’s capability to render and control a custom UI with the help of CustomPaint widgets. By Samarth Agarwal.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Building a Drawing App in Flutter
35 mins
- Getting Started
- Introducing Flutter Canvas and CustomPaint
- Using the CustomPaint Widget
- Understanding Canvas Basics
- Drawing Paths
- Changing the Stroke, Color and Width
- Diving Into Code
- Using GestureDetector
- Drawing a Single Path
- Drawing Multiple Paths
- Adding Stroke Color and Width
- Changing Stroke Color
- Changing Stroke Width
- Optimizing Your App
- Drawing Multiple Lines
- Using StreamBuilders and Two CustomPaint widgets
- Saving the Drawing
- Creating New and Save Buttons
- Using the Plugin
- Where to Go From Here
Diving Into Code
Now you know the basics of drawing simple shapes on the canvas, and so it’s time to start working on your drawing app. Exit this sample app and run the drawing app by using the command flutter run.
Then, you’ll start work on your app by drawing a simple Path using Sketcher — a CustomPainter class for this project located in lib/sketcher.dart — in combination with CustomPaint. You’ll use all of this together with GestureDetector to find the coordinates of the points that the user touches.
Using GestureDetector
Start by implementing buildCurrentPath() in lib/drawing_page.dart:
GestureDetector buildCurrentPath(BuildContext context) {
return GestureDetector(
onPanStart: onPanStart,
onPanUpdate: onPanUpdate,
onPanEnd: onPanEnd,
child: RepaintBoundary(
child: Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
// CustomPaint widget will go here
),
),
);
}
In the code above, you return GestureDetector from buildCurrentPath(). You use GestureDetector‘s onPanStart(), onPanUpdate() and onPanEnd() events to detect the touches — and specifically the dragging — on the screen. You also use RepaintBoundary to optimize the redrawing.
The next step is to implement the three missing methods. Create the methods as shown below:
void onPanStart(DragStartDetails details) {
print('User started drawing');
final box = context.findRenderObject() as RenderBox;
final point = box.globalToLocal(details.globalPosition);
print(point);
}
void onPanUpdate(DragUpdateDetails details) {
final box = context.findRenderObject() as RenderBox;
final point = box.globalToLocal(details.globalPosition);
print(point);
}
void onPanEnd(DragEndDetails details) {
print('User ended drawing');
}
In the code snippet above:
-
onPanStart()is executed when the user touches the screen and starts dragging their finger around it. - When the user is dragging their finger on the screen without lifting it off the screen, the app executes
onPanUpdate(). -
onPanEnd()is executed when the user lifts their finger off the screen.
To find RenderBox for GestureDetector, you used findRenderObject(). You also used globalToLocal() to convert the global coordinates to the local coordinates you’ll use to draw the path.
For now, you are printing the points the user touches on the screen to the console, to ensure that the detection works as expected.
Finally, add buildCurrentPath() to Stack in the main build().
...
Stack(
children: [
// Add this
buildCurrentPath(context),
],
),
Save everything and hot restart. Touch the screen and you’ll see some logs on the console similar to the ones shown below.
I/flutter (21819): User started drawing
I/flutter (21819): Offset(157.5, 305.5)
I/flutter (21819): Offset(157.5, 305.5)
...
I/flutter (21819): Offset(158.9, 362.2)
I/flutter (21819): User ended drawing
Drawing a Single Path
Combining the three methods above will give us coordinates of all the points the user touches with their screen in one go, without lifting their finger. Next, you need to create a DrawnLine using these three methods.
Modify the three methods as shown below:
void onPanStart(DragStartDetails details) {
...
setState((){
line = DrawnLine([point], selectedColor, selectedWidth);
});
}
void onPanUpdate(DragUpdateDetails details) {
...
final path = List.from(line.path)..add(point);
setState((){
line = DrawnLine(path, selectedColor, selectedWidth);
});
}
void onPanEnd(DragEndDetails details) {
setState((){
print('User ended drawing');
});
}
Here’s what the code above does:
- Inside
onPanStart(), you create a newDrawnLineand use the only point you have to createDrawnLine. Additionally, you useselectedColorfor the color, andselectedWidthfor the stroke width. Both of these have default values. Then, you callsetState()to update the UI. - Inside
onPanUpdate(), you create a path that’s a type ofList<Offset>, add new points to the list and update theline. Finally, you callsetState()to update the UI.
To see the DrawnLine line, you need a CustomPaint inside buildCurrentPath(). Add the following code as the Container‘s child and import the missing file – sketcher.dart.
GestureDetector buildCurrentPath(BuildContext context) {
return GestureDetector(
...
child: RepaintBoundary(
child: Container(
...
child: CustomPaint(
painter: Sketcher(lines: [line]),
),
),
),
);
}
In the code snippet above, you add a CustomPaint widget. For the painter parameter, you pass in a Sketcher instance. It takes in the lines property to which you pass in a List containing the DrawnLine that you created using gesture events.
Save all the files and hot restart, then try drawing with your finger on the screen.

Awesome, right? :]
Drawing Multiple Paths
Right now, when you draw a new path the older path just disappears. This happens because you reinitialize the line inside onPanStart(). As soon as you touch the screen, you lose the old path.
To draw multiple paths on the screen, you’ll need to store all the path points. You won’t always reinitialize the line inside onPanStart(). You only have to initialize it if the user is drawing the very first path.
When a path ends you’ll insert a null value instead of an Offset as the point, to know you need to initialize a new starting point.
Here are the changes you need to apply to GestureDetector events:
void onPanStart(DragStartDetails details) {
print('User started drawing');
...
if (line == null) {
line = DrawnLine([point], selectedColor, selectedWidth);
}
...
}
void onPanUpdate(DragUpdateDetails details) {
...
}
void onPanEnd(DragEndDetails details) {
final path = List.from(line.path)..add(null);
setState(() {
line = DrawnLine(path, selectedColor, selectedWidth);
});
}
Here’s what’s happening in the code above:
- Inside
onPanStart(), you initializelineonly if the line object isnull. This means that it is only initialized when the user touches the screen for the first time to draw the very first path. This prevents thelinefrom being cleared on eachonPanStart()event. - There are no changes in
onPanUpdate(). - Inside
onPanEnd, you insert anOffsetwith anullvalue to mark the end of path. TheSketchertakes thesenullvalues into account and draws paths accordingly.
Save everything and hot restart. Try drawing on the screen.

If you keep drawing, you’ll see that you’re able to draw multiple paths. It is actually a single path that is broken in various places with the help of null values.
Adding Stroke Color and Width
If you scroll to the top in lib/drawing_page.dart, you’ll find two variables defined: selectedColor and selectedWidth:
final selectedColor = Colors.black;
final selectedWidth = 5.0;
These are the values you pass to DrawnLine. This is the reason behind the default color and width of the paths you have been drawing so far.