Capturing a Flutter widget as an image using RepaintBoundary
Tags: flutter, dart, tutorial
Published: 2021-08-04 23:15:13

The other day I decided I wanted to add a feature in one of my projects to share a graph and I was looking for ways to convert the graph into an image. In the end the approach that I landed on was using the RepaintBoundary widget and I'm going to explain how to use it in this post.

## The widget

First of all let's just start with a widget, a green Container with some text inside. Here's what the code for it looks like:

```dart
Container(
  decoration: BoxDecoration(
    color: Colors.green,
  ),
  height: 200,
  width: 400,
  child: Center(
    child: Text(
      "Hello world!",
      style: TextStyle(
        color: Colors.white,
        fontSize: 24,
      ),
    )
  )
)
```

And here's what the above widget looks like visually(this was generated using [DartPad](https://dartpad.dev)):

![Container Widget](https://developermemos.com/static/0499a9dbad344119c721b8584b951e20/293b3/example_container.png)

## Wrap it with RepaintBoundary

To convert this widget to an image file(or capture/screenshot it) you need to wrap the widget in a `RepaintBoundary` widget:

```dart
RepaintBoundary(
  child: Container(
    decoration: BoxDecoration(
      color: Colors.green,
    ),
    height: 200,
    width: 400,
    child: Center(
      child: Text(
        "Hello world!",
        style: TextStyle(
          color: Colors.white,
          fontSize: 24,
        ),
      ),
    ),
  ),
)
```

Then you need to create a `GlobalKey` and pass that to the `RepaintBoundary` widget:

```dart

final key = GlobalKey();

...

RepaintBoundary(
  key: key,
  child: Container(
    decoration: BoxDecoration(
      color: Colors.green,
    ),
    height: 200,
    width: 400,
    child: Center(
      child: Text(
        "Hello world!",
        style: TextStyle(
          color: Colors.white,
          fontSize: 24,
        ),
      ),
    ),
  ),
)
```

## Create the image

Now the last step is to use the key above kind of like a controller and create an image representation of the widget wrapped inside `RepaintBoundary`. I probably should also add that the widget you wrap doesn't have to be a `Container` widget. Okay, onto the code for getting the bytes for an image. You could trigger this code with a button or something along those lines:

```dart
final boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
final image = await boundary?.toImage();
final byteData = await image?.toByteData(format: ImageByteFormat.png);
final imageBytes = byteData?.buffer.asUint8List();
```

Then the next step is to write those bytes to a file somewhere(you will need the `path_provider` package to use `getApplicationDocumentsDirectory`):

```dart
if (imageBytes != null) {
  final directory = await getApplicationDocumentsDirectory();
  final imagePath = await File('${directory.path}/container_image.png').create();
  await imagePath.writeAsBytes(imageBytes);
}
```

And here is the full code for this step:

```dart
final boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
final image = await boundary?.toImage();
final byteData = await image?.toByteData(format: ImageByteFormat.png);
final imageBytes = byteData?.buffer.asUint8List();

if (imageBytes != null) {
  final directory = await getApplicationDocumentsDirectory();
  final imagePath = await File('${directory.path}/container_image.png').create();
  await imagePath.writeAsBytes(imageBytes);
}
```

Once you execute this code an image representation of the widget will be created in the applications document directory of your app. Here's the output I got after I executed the code on an iOS simulator:

![PNG Image Representation](https://developermemos.com/static/7b171fdd526ed2a7c44472d96e5a1fee/6a8a8/container_image.png)

By the way you can view the contents of a simulator's document directory using the following command on MacOS:

```bash
open `xcrun simctl get_app_container booted [Bundle ID] data`
```

## Some caveats

I'll close this post with a couple of caveats with the approach above:

- The resulting image might end up pixelated in some cases. You can rectify this by passing the `pixelRatio` parameter when using `await boundary?.toImage()`. You might want to combine this with `MediaQuery.of(context).devicePixelRatio` for the best effect. I learnt about this from the README of an excellent package called [screenshot](https://pub.dev/packages/screenshot). (The reason I wrote my own implementation is because I already use a lot of packages in the project I was working on and didn't want to add any more...)
- The generated image will be in PNG format. I'm not sure if you can generate a JPEG image or how you would do it. I might end up investigating this in the future.