— Dart, Flutter, Widgets, Flutter Tips — 2 min read
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.
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:
1Container(2 decoration: BoxDecoration(3 color: Colors.green,4 ),5 height: 200,6 width: 400,7 child: Center(8 child: Text(9 "Hello world!",10 style: TextStyle(11 color: Colors.white,12 fontSize: 24,13 ),14 )15 )16)
And here's what the above widget looks like visually(this was generated using DartPad):
To convert this widget to an image file(or capture/screenshot it) you need to wrap the widget in a RepaintBoundary
widget:
1RepaintBoundary(2 child: Container(3 decoration: BoxDecoration(4 color: Colors.green,5 ),6 height: 200,7 width: 400,8 child: Center(9 child: Text(10 "Hello world!",11 style: TextStyle(12 color: Colors.white,13 fontSize: 24,14 ),15 ),16 ),17 ),18)
Then you need to create a GlobalKey
and pass that to the RepaintBoundary
widget:
1final key = GlobalKey();2
3...4
5RepaintBoundary(6 key: key,7 child: Container(8 decoration: BoxDecoration(9 color: Colors.green,10 ),11 height: 200,12 width: 400,13 child: Center(14 child: Text(15 "Hello world!",16 style: TextStyle(17 color: Colors.white,18 fontSize: 24,19 ),20 ),21 ),22 ),23)
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:
1final boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;2final image = await boundary?.toImage();3final byteData = await image?.toByteData(format: ImageByteFormat.png);4final 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
):
1if (imageBytes != null) {2 final directory = await getApplicationDocumentsDirectory();3 final imagePath = await File('${directory.path}/container_image.png').create();4 await imagePath.writeAsBytes(imageBytes);5}
And here is the full code for this step:
1final boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;2final image = await boundary?.toImage();3final byteData = await image?.toByteData(format: ImageByteFormat.png);4final imageBytes = byteData?.buffer.asUint8List();5
6if (imageBytes != null) {7 final directory = await getApplicationDocumentsDirectory();8 final imagePath = await File('${directory.path}/container_image.png').create();9 await imagePath.writeAsBytes(imageBytes);10}
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:
By the way you can view the contents of a simulator's document directory using the following command on MacOS:
1open `xcrun simctl get_app_container booted [Bundle ID] data`
I'll close this post with a couple of caveats with the approach above:
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. (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...)