Contact Me
Blog

SVGs using Custom Painters in Flutter

In a previous post, I showed how you can embed images in Flutter components using Base64 data, rather then referencing a file in your application's assets. This can be particularly useful for Flutter Web, since it saves on an HTTP request and can help eliminate the delay you often get when displaying an image.

For SVG images, there are a couple of ways of doing something similar. One of those is to use a custom painter; that's what I'm going to look at here.

We're going to use the same image as last time:

It's a free, MIT-licenced icon from IconFinder; you can download it from there.

In practice, you probably wouldn't render an icon in this way; that's what icon fonts are designed for. It's just a small SVG that serves as an example of this approach.

Here it is in SVG format:

<?xml version="1.0" ?>
<svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg">
  <path d="M20 40V28h8v12h10V24h6L24 6 4 24h6v16z"/>
  <path d="M0 0h48v48H0z" fill="none"/>
</svg>

As you can see, it's such a small file that there's no real impact in terms of the size of the resulting binary.

Anatomy of an SVG

An SVG file contains what are essentially drawing instructions; move the "pen" to a specified set of co-ordinates on the canvas, draw a line to these co-ordinates, fill an area with a specified color. A <path> is a shape made up such instructions.

Flutter provides similar functionality in its rendering engine; custom painters. The approach is extremely similar, but programmatic rather than declarative.

By converting an SVG into Dart code, we can draw it onto a canvas and display it as a widget.

Converting SVGs to Painters

Fortunately, there's an immensely helpful tool to do the hard yards for us. Flutter Shape Maker enables you to draw vector images and generate Dart code from them; it also provides a tool to convert an SVG to code.

Follow the link and click SVG to Custom Paint in the menu bar, and you'll be presented with the popup pictured below:

The Flutter Shape Maker Tool being used to convert an SVG image into a Custom Painter.

You can either paste the SVG code in, or choose a file to upload. Either way, hit Generate Code to run the process once you've done so.

Here's the raw output from running it with the image referenced above:

import 'dart:ui' as ui;

//Add this CustomPaint widget to the Widget Tree
CustomPaint(
  size: Size(48, 48), 
  painter: RPSCustomPainter(),
)

//Copy this CustomPainter code to the Bottom of the File
class RPSCustomPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {

  Path path_0 = Path();
    path_0.moveTo(20,40);
    path_0.lineTo(20,28);
    path_0.lineTo(28,28);
    path_0.lineTo(28,40);
    path_0.lineTo(38,40);
    path_0.lineTo(38,24);
    path_0.lineTo(44,24);
    path_0.lineTo(24,6);
    path_0.lineTo(4,24);
    path_0.lineTo(10,24);
    path_0.lineTo(10,40);
    path_0.close();

  Paint paint_0_fill = Paint()..style=PaintingStyle.fill;
  paint_0_fill.color = Color(0xff000000).withOpacity(1.0);
  canvas.drawPath(path_0,paint_0_fill);

  Path path_1 = Path();
    path_1.moveTo(0,0);
    path_1.lineTo(48,0);
    path_1.lineTo(48,48);
    path_1.lineTo(0,48);
    path_1.close();
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

As you can see, it's a very similar approach to SVGs — draw an image by moving the "pen", drawing lines to specified coordinates and specifying a fill. If you look carefully at the co-ordinates, you'll see that they match the ones in the <path> tags in the underlying SVG.

Using a Custom Painter

In order to use this, we need to create a CustomPaint widget, give it a size and specify an instance of the CustomPainter class. The key method here is paint(), which takes a canvas that's injected by the widget, along with the size.

Let's use the code above to create a stateless widget:

import 'package:flutter/material.dart';

class HomeIcon extends StatelessWidget {

  const HomeIcon();

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(48, 48), 
      painter: _Painter(),
    );
  }
}

class _Painter extends CustomPainter {
    @override
    void paint(Canvas canvas, Size size) {

    Path path_0 = Path();
      path_0.moveTo(20,40);
      path_0.lineTo(20,28);
      path_0.lineTo(28,28);
      path_0.lineTo(28,40);
      path_0.lineTo(38,40);
      path_0.lineTo(38,24);
      path_0.lineTo(44,24);
      path_0.lineTo(24,6);
      path_0.lineTo(4,24);
      path_0.lineTo(10,24);
      path_0.lineTo(10,40);
      path_0.close();

    Paint paint_0_fill = Paint()..style=PaintingStyle.fill;
    paint_0_fill.color = Color(0xff000000).withOpacity(1.0);
    canvas.drawPath(path_0,paint_0_fill);

    Path path_1 = Path();
      path_1.moveTo(0,0);
      path_1.lineTo(48,0);
      path_1.lineTo(48,48);
      path_1.lineTo(0,48);
      path_1.close();

  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
      return true;
  }
}

Note that I've changed the painter's class name from RPSCustomPainter to _Painter so that it's not exported; it's meaningless outside of the context of this widget.

You can play around with this code on DartPad, or grab it from this Gist.