How to Capture, Save, and Share a Flutter Widget as an Image


In many applications particularly those in education you may want to let users save or share visual content generated within the app. Flutter doesn’t offer direct APIs to save a widget as an image out of the box, but with a few libraries and the right approach, this is entirely achievable.
This guide walks you through:
Capturing a Flutter widget as an image (using
RepaintBoundary
)Saving it to the gallery (using
image_gallery_saver
)Sharing it to platforms like WhatsApp, Gmail, etc. (using
share_plus
)
We will create a simple "Quote of the Day" widget to demonstrate this feature
What We Will Build
A simple app that displays a styled quote card with two buttons: one to save the quote as an image and another to share it using the native sharing options on the device.
Step 1: Create a New Flutter Project
If you haven't already, create a new Flutter project using the following command:
flutter create quote_share_app
cd quote_share_app
Open the project in your preferred IDE.
Step 2: Add Required Dependencies
Open your pubspec.yaml
file and add the following dependencies:
dependencies:
flutter:
sdk: flutter
permission_handler: ^11.0.0
image_gallery_saver: ^1.7.1
path_provider: ^2.1.1
share_plus: ^7.2.1
Then run:
flutter pub get
Step 3: Android Permissions
In your android/app/src/main/AndroidManifest.xml
, add the following permissions:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Also inside the <application>
tag, ensure this is present:
android:requestLegacyExternalStorage="true"
Step 4: Full Code (main.dart
)
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
void main() {
runApp(const QuoteShareApp());
}
class QuoteShareApp extends StatelessWidget {
const QuoteShareApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Quote Share App',
debugShowCheckedModeBanner: false,
home: const QuoteScreen(),
);
}
}
class QuoteScreen extends StatefulWidget {
const QuoteScreen({super.key});
@override
State<QuoteScreen> createState() => _QuoteScreenState();
}
class _QuoteScreenState extends State<QuoteScreen> {
final GlobalKey _quoteKey = GlobalKey();
Future<Uint8List?> _captureWidget() async {
try {
RenderRepaintBoundary boundary =
_quoteKey.currentContext?.findRenderObject() as RenderRepaintBoundary;
if (boundary.debugNeedsPaint) {
await Future.delayed(const Duration(milliseconds: 20));
return _captureWidget();
}
final image = await boundary.toImage(pixelRatio: 3.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
return byteData?.buffer.asUint8List();
} catch (e) {
debugPrint("Capture error: $e");
return null;
}
}
Future<void> _saveImage() async {
final permission = await Permission.storage.request();
if (!permission.isGranted) {
if (permission.isPermanentlyDenied) {
openAppSettings();
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Storage permission is required.")),
);
return;
}
final imageBytes = await _captureWidget();
if (imageBytes != null) {
final result = await ImageGallerySaver.saveImage(imageBytes);
debugPrint("Image saved: $result");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Image saved to gallery.")),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Failed to capture image.")),
);
}
}
Future<void> _shareImage() async {
try {
final imageBytes = await _captureWidget();
if (imageBytes == null) return;
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/quote_${DateTime.now().millisecondsSinceEpoch}.png';
final file = await File(filePath).create();
await file.writeAsBytes(imageBytes);
final params = ShareParams(
text: 'Here is a quote I wanted to share.',
files: [XFile(file.path)],
);
final result = await SharePlus.instance.share(params);
if (result.status == ShareResultStatus.success) {
debugPrint("Image shared successfully.");
}
} catch (e) {
debugPrint("Share error: $e");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Quote Share'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
RepaintBoundary(
key: _quoteKey,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.teal.shade100,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.teal.shade200,
blurRadius: 8,
offset: const Offset(2, 4),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'"Believe you can and you\'re halfway there."',
style: TextStyle(
fontSize: 22,
fontStyle: FontStyle.italic,
color: Colors.black87,
),
),
SizedBox(height: 10),
Align(
alignment: Alignment.bottomRight,
child: Text(
"- Theodore Roosevelt",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black54,
),
),
),
],
),
),
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _saveImage,
icon: const Icon(Icons.download),
label: const Text("Save"),
),
ElevatedButton.icon(
onPressed: _shareImage,
icon: const Icon(Icons.share),
label: const Text("Share"),
),
],
)
],
),
),
);
}
}
Code Explanation
GlobalKey and RepaintBoundary: These allow you to reference and isolate a portion of your UI that you want to capture.
_captureWidget(): Captures the widget and converts it to image bytes.
_saveImage(): Requests storage permission, captures the image, and saves it to the gallery.
_shareImage(): Captures the image, saves it temporarily, and shares it via native share dialogs.
User Interface: The quote card is designed inside a styled container that is wrapped in
RepaintBoundary
so that only it is captured.Buttons: Trigger the save or share functions respectively.
Reference Libraries
image_gallery_saver – Used to save image bytes to the gallery.
share_plus – Used to share files and text using the native sharing dialog.
path_provider – Provides access to the device's file system.
permission_handler – Manages runtime permissions, especially for Android storage access.
Conclusion
This article demonstrated how to capture a widget as an image and provide options to either save it locally or share it through external apps. This can be particularly useful for dynamic content like user achievements, personalized quotes, generated reports, or motivational cards.
This feature elevates user engagement and makes your Flutter app more versatile and social-friendly and another way to achieve this using the screenshot package
Subscribe to my newsletter
Read articles from Atuoha Anthony directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Atuoha Anthony
Atuoha Anthony
Google Developer Expert (Flutter/Dart) and Mobile Software Engineer specializing in Flutter/Dart, Kotlin (Jetpack Compose), and Swift (UIKit/SwiftUI), with a comprehensive background in web technologies. Skilled in designing, developing, and deploying software for Android, iOS, Web, and other platforms. Adept at collaborating effectively with cross-functional teams to create high-quality, scalable products.