Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Top 10 Use Cases of Vibe Coding in Large-Scale Node.js Applications

      September 3, 2025

      Cloudsmith launches ML Model Registry to provide a single source of truth for AI models and datasets

      September 3, 2025

      Kong Acquires OpenMeter to Unlock AI and API Monetization for the Agentic Era

      September 3, 2025

      Microsoft Graph CLI to be retired

      September 2, 2025

      ‘Cronos: The New Dawn’ was by far my favorite experience at Gamescom 2025 — Bloober might have cooked an Xbox / PC horror masterpiece

      September 4, 2025

      ASUS built a desktop gaming PC around a mobile CPU — it’s an interesting, if flawed, idea

      September 4, 2025

      Hollow Knight: Silksong arrives on Xbox Game Pass this week — and Xbox’s September 1–7 lineup also packs in the horror. Here’s every new game.

      September 4, 2025

      The Xbox remaster that brought Gears to PlayStation just passed a huge milestone — “ending the console war” and proving the series still has serious pulling power

      September 4, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      Magento (Adobe Commerce) or Optimizely Configured Commerce: Which One to Choose

      September 4, 2025
      Recent

      Magento (Adobe Commerce) or Optimizely Configured Commerce: Which One to Choose

      September 4, 2025

      Updates from N|Solid Runtime: The Best Open-Source Node.js RT Just Got Better

      September 3, 2025

      Scale Your Business with AI-Powered Solutions Built for Singapore’s Digital Economy

      September 3, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      ‘Cronos: The New Dawn’ was by far my favorite experience at Gamescom 2025 — Bloober might have cooked an Xbox / PC horror masterpiece

      September 4, 2025
      Recent

      ‘Cronos: The New Dawn’ was by far my favorite experience at Gamescom 2025 — Bloober might have cooked an Xbox / PC horror masterpiece

      September 4, 2025

      ASUS built a desktop gaming PC around a mobile CPU — it’s an interesting, if flawed, idea

      September 4, 2025

      Hollow Knight: Silksong arrives on Xbox Game Pass this week — and Xbox’s September 1–7 lineup also packs in the horror. Here’s every new game.

      September 4, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»How to Save and Share Flutter Widgets as Images – A Complete Production-Ready Guide

    How to Save and Share Flutter Widgets as Images – A Complete Production-Ready Guide

    September 3, 2025

    In many apps, you may want users to be able to save or share visual content generated in the UI. Flutter doesn’t ship with a “save widget to image” API, but with RepaintBoundary plus a few small packages, you can capture any widget, save it to the device’s gallery, and share it through the native share sheet.

    This article will go through the process of capturing and saving a widget step-by-step. We’ll be building a small Flutter app that renders a styled Quote Card and provides two actions:

    1. Save the quote card to the device’s gallery as a PNG.

    2. Share the image through the native share sheet (WhatsApp, Gmail, Messages, and so on).

    Table of Contents:

    1. Prerequisites

    2. Project Setup

    3. Dependencies

    4. Platform Configuration

      1. Android

      2. iOS

    5. App Architecture and Files Overview

    6. Code Sections With Explanations

      1. lib/main.dart

      2. lib/widgets/quote_card.dart

      3. lib/utils/capture.dart

      4. lib/services/permission_service.dart

      5. lib/services/gallery_saver_service.dart

      6. lib/services/share_service.dart

      7. lib/screens/quote_screen.dart

      8. State variables

      9. Capture function

      10. Save function

      11. Share function

      12. Summary

    7. Testing The Flow

    8. Troubleshooting And Common Pitfalls

    9. Enhancements And Alternatives

    10. Conclusion

    11. References

    Prerequisites

    1. Flutter 3.x or later installed and configured

    2. An Android device or emulator, and optionally an iOS device or simulator

    3. Basic familiarity with Flutter widgets and project structure

    Project Setup

    Create a new project and open it in your IDE:

    flutter create quote_share_app
    <span class="hljs-built_in">cd</span> quote_share_app
    

    Dependencies

    Add the following to pubspec.yaml under dependencies: and run flutter pub get:

    <span class="hljs-attr">dependencies:</span>
      <span class="hljs-attr">flutter:</span>
        <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
      <span class="hljs-attr">permission_handler:</span> <span class="hljs-string">^11.3.1</span>
      <span class="hljs-attr">image_gallery_saver:</span> <span class="hljs-string">^2.0.3</span>
      <span class="hljs-attr">path_provider:</span> <span class="hljs-string">^2.1.3</span>
      <span class="hljs-attr">share_plus:</span> <span class="hljs-string">^9.0.0</span>
    

    Notes about this code:

    1. permission_handler handles runtime permissions where required.

    2. image_gallery_saver writes raw bytes to the photo gallery (Android and iOS).

    3. path_provider creates a temporary file location before sharing.

    4. share_plus invokes the platform share sheet.

    Version numbers above are examples that work with Flutter 3.x at the time of writing. If you update, check each package’s README for any API changes.

    Platform Configuration

    Modern Android and iOS storage permissions are stricter than older blog posts often suggest. The snippets below are current best practices.

    Android

    Open android/app/src/main/AndroidManifest.xml.

    For Android 10 (API 29) and above, WRITE_EXTERNAL_STORAGE is deprecated. For Android 13 (API 33)+ you request media-scoped permissions like READ_MEDIA_IMAGES only if you are reading images. For saving your own image to the Pictures or DCIM collection, many devices don’t require the legacy external storage permissions when you write via MediaStore (plugins often handle this). image_gallery_saver typically works without WRITE_EXTERNAL_STORAGE on API 29+.

    Add the following only if you target older devices and the plugin still requires it. Otherwise, you can omit storage permissions for modern SDKs.

    <span class="hljs-comment"><!-- Optional for older devices pre-API 29 --></span>
    <span class="hljs-tag"><<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.WRITE_EXTERNAL_STORAGE"</span>
        <span class="hljs-attr">android:maxSdkVersion</span>=<span class="hljs-string">"28"</span> /></span>
    <span class="hljs-tag"><<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.READ_EXTERNAL_STORAGE"</span>
        <span class="hljs-attr">android:maxSdkVersion</span>=<span class="hljs-string">"32"</span> /></span>
    
    <span class="hljs-comment"><!-- For Android 13+ if you ever need to read user images; not required just to write your own image --></span>
    <span class="hljs-tag"><<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.READ_MEDIA_IMAGES"</span> /></span>
    

    Do not add android:requestLegacyExternalStorage="true". That flag was a temporary compatibility bridge for Android 10 and is not recommended anymore.

    Gradle configuration: ensure your compileSdkVersion and targetSdkVersion are reasonably up to date (33 or 34). You usually don’t need special Gradle changes beyond what Flutter templates provide.

    iOS

    Open ios/Runner/Info.plist and add the following keys to explain why you save to the user’s photo library:

    <span class="hljs-tag"><<span class="hljs-name">key</span>></span>NSPhotoLibraryAddUsageDescription<span class="hljs-tag"></<span class="hljs-name">key</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">string</span>></span>The app needs access to save your generated images.<span class="hljs-tag"></<span class="hljs-name">string</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">key</span>></span>NSPhotoLibraryUsageDescription<span class="hljs-tag"></<span class="hljs-name">key</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">string</span>></span>The app needs access to your photo library.<span class="hljs-tag"></<span class="hljs-name">string</span>></span>
    

    Some devices only require the Add usage description for writing, but supplying both keeps intent clear.

    App Architecture and Files Overview

    To keep the code maintainable, we will split it into small files:

    1. lib/main.dart

    2. lib/widgets/quote_card.dart

    3. lib/utils/capture.dart

    4. lib/services/permission_service.dart

    5. lib/services/gallery_saver_service.dart

    6. lib/services/share_service.dart

    7. lib/screens/quote_screen.dart

    This is the flow:

    1. QuoteCard renders the visual widget we want to capture.

    2. captureWidgetToPngBytes(GlobalKey) converts that widget into PNG bytes using RepaintBoundary.

    3. PermissionService requests storage or photo library permissions when needed.

    4. GallerySaverService saves bytes to the gallery.

    5. ShareService writes bytes to a temporary file and triggers the share sheet.

    6. QuoteScreen wires everything together with two buttons: Save and Share.

    Code Sections With Explanations

    1. lib/main.dart

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'screens/quote_screen.dart'</span>;
    
    <span class="hljs-keyword">void</span> main() {
      runApp(<span class="hljs-keyword">const</span> QuoteShareApp());
    }
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">QuoteShareApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
      <span class="hljs-keyword">const</span> QuoteShareApp({<span class="hljs-keyword">super</span>.key});
    
      <span class="hljs-meta">@override</span>
      Widget build(BuildContext context) {
        <span class="hljs-keyword">return</span> MaterialApp(
          title: <span class="hljs-string">'Quote Share App'</span>,
          debugShowCheckedModeBanner: <span class="hljs-keyword">false</span>,
          theme: ThemeData(
            colorSchemeSeed: Colors.teal,
            useMaterial3: <span class="hljs-keyword">true</span>,
          ),
          home: <span class="hljs-keyword">const</span> QuoteScreen(),
        );
      }
    }
    

    Code explanation:

    1. runApp bootstraps the app.

    2. MaterialApp provides theming and navigation.

    3. QuoteScreen is our only screen; it displays the card and buttons.

    2. lib/widgets/quote_card.dart

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">QuoteCard</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
      <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> quote;
      <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> author;
    
      <span class="hljs-keyword">const</span> QuoteCard({
        <span class="hljs-keyword">super</span>.key,
        <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.quote,
        <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.author,
      });
    
      <span class="hljs-meta">@override</span>
      Widget build(BuildContext context) {
        <span class="hljs-keyword">return</span> Container(
          width: <span class="hljs-built_in">double</span>.infinity,
          padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">24</span>),
          decoration: BoxDecoration(
            color: Colors.teal.shade50,
            borderRadius: BorderRadius.circular(<span class="hljs-number">20</span>),
            boxShadow: [
              BoxShadow(
                color: Colors.teal.shade200.withOpacity(<span class="hljs-number">0.4</span>),
                blurRadius: <span class="hljs-number">12</span>,
                offset: <span class="hljs-keyword">const</span> Offset(<span class="hljs-number">2</span>, <span class="hljs-number">6</span>),
              ),
            ],
            border: Border.all(color: Colors.teal.shade200, width: <span class="hljs-number">1</span>),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                <span class="hljs-string">'"<span class="hljs-subst">$quote</span>"'</span>,
                style: <span class="hljs-keyword">const</span> TextStyle(
                  fontSize: <span class="hljs-number">22</span>,
                  fontStyle: FontStyle.italic,
                  color: Colors.black87,
                  height: <span class="hljs-number">1.4</span>,
                ),
              ),
              <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">16</span>),
              Align(
                alignment: Alignment.bottomRight,
                child: Text(
                  <span class="hljs-string">'- <span class="hljs-subst">$author</span>'</span>,
                  style: <span class="hljs-keyword">const</span> TextStyle(
                    fontSize: <span class="hljs-number">16</span>,
                    fontWeight: FontWeight.w600,
                    color: Colors.black54,
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }
    

    Code explanation:

    1. Pure UI. This widget is what we will capture into an image.

    2. The stylings (padding, shadows, rounded corners) ensure that the result looks good when saved or shared.

    3. lib/utils/capture.dart

    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:typed_data'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:ui'</span> <span class="hljs-keyword">as</span> ui;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
    
    <span class="hljs-comment">/// <span class="markdown">Captures the widget referenced by [boundaryKey] into PNG bytes.</span></span>
    <span class="hljs-comment">/// <span class="markdown">Place a RepaintBoundary keyed with [boundaryKey] around the widget you want to capture.</span></span>
    Future<Uint8List?> captureWidgetToPngBytes(GlobalKey boundaryKey, {<span class="hljs-built_in">double</span> pixelRatio = <span class="hljs-number">3.0</span>}) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">final</span> context = boundaryKey.currentContext;
        <span class="hljs-keyword">if</span> (context == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    
        <span class="hljs-keyword">final</span> renderObject = context.findRenderObject();
        <span class="hljs-keyword">if</span> (renderObject <span class="hljs-keyword">is</span>! RenderRepaintBoundary) <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    
        <span class="hljs-comment">// If the boundary hasn't painted yet, wait a frame and try again.</span>
        <span class="hljs-keyword">if</span> (renderObject.debugNeedsPaint) {
          <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">20</span>));
          <span class="hljs-keyword">return</span> captureWidgetToPngBytes(boundaryKey, pixelRatio: pixelRatio);
        }
    
        <span class="hljs-comment">// Render to an Image with a higher pixelRatio for sharpness on high-dpi screens.</span>
        <span class="hljs-keyword">final</span> ui.Image image = <span class="hljs-keyword">await</span> renderObject.toImage(pixelRatio: pixelRatio);
    
        <span class="hljs-comment">// Encode the Image to PNG and return bytes.</span>
        <span class="hljs-keyword">final</span> byteData = <span class="hljs-keyword">await</span> image.toByteData(format: ui.ImageByteFormat.png);
        <span class="hljs-keyword">return</span> byteData?.buffer.asUint8List();
      } <span class="hljs-keyword">catch</span> (e) {
        debugPrint(<span class="hljs-string">'captureWidgetToPngBytes error: <span class="hljs-subst">$e</span>'</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
      }
    }
    

    Code explanation line by line:

    1. We accept a GlobalKey that must be attached to a RepaintBoundary wrapping the target widget.

    2. findRenderObject() retrieves the render tree node. RenderRepaintBoundary can snapshot itself to an image.

    3. debugNeedsPaint indicates whether the widget is fully laid out and painted. If not, we wait briefly and retry.

    4. toImage(pixelRatio: 3.0) renders at higher resolution for crisp output. Increase if you need even sharper images, but note memory tradeoffs.

    5. We encode the ui.Image to PNG via toByteData and return its bytes.

    4. lib/services/permission_service.dart

    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:permission_handler/permission_handler.dart'</span>;
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PermissionService</span> </span>{
      <span class="hljs-comment">/// <span class="markdown">Requests any storage/photo permissions needed for saving an image.</span></span>
      <span class="hljs-comment">/// <span class="markdown">On modern Android and iOS, saving to the Photos collection may not require</span></span>
      <span class="hljs-comment">/// <span class="markdown">the legacy WRITE permission, but some devices and OS versions still prompt.</span></span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-built_in">bool</span>> requestSavePermission() <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">if</span> (Platform.isAndroid) {
          <span class="hljs-comment">// For Android 13+ you typically do not need WRITE permission to save your own image.</span>
          <span class="hljs-comment">// Some OEMs still require storage permission for certain gallery operations.</span>
          <span class="hljs-keyword">final</span> status = <span class="hljs-keyword">await</span> Permission.storage.request();
          <span class="hljs-keyword">if</span> (status.isGranted) <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    
          <span class="hljs-keyword">if</span> (status.isPermanentlyDenied) {
            <span class="hljs-keyword">await</span> openAppSettings();
          }
          <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
        }
    
        <span class="hljs-keyword">if</span> (Platform.isIOS) {
          <span class="hljs-comment">// On iOS, request Photos permission for adding to library when needed.</span>
          <span class="hljs-keyword">final</span> status = <span class="hljs-keyword">await</span> Permission.photosAddOnly.request();
          <span class="hljs-comment">// Fallback if photosAddOnly is unavailable on older plugin versions:</span>
          <span class="hljs-keyword">if</span> (status.isGranted) <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    
          <span class="hljs-comment">// Some iOS versions may use `Permission.photos`.</span>
          <span class="hljs-keyword">final</span> photos = <span class="hljs-keyword">await</span> Permission.photos.request();
          <span class="hljs-keyword">if</span> (photos.isGranted) <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    
          <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
        }
    
        <span class="hljs-comment">// Other platforms</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
      }
    }
    

    In the above code, the Android storage permissions are fragmented by API level and OEM behavior. Requesting Permission.storage remains a pragmatic approach when using gallery saver plugins, though many modern devices will succeed even if the user denies it.

    On iOS, we request photo-library add permission, which allows writing to the library.

    5. lib/services/gallery_saver_service.dart

    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:typed_data'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_gallery_saver/image_gallery_saver.dart'</span>;
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GallerySaverService</span> </span>{
      <span class="hljs-comment">/// <span class="markdown">Saves [pngBytes] to the gallery and returns a descriptive result map from the plugin.</span></span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-built_in">Map?</span>> savePngBytesToGallery(Uint8List pngBytes, {<span class="hljs-built_in">String?</span> name}) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> ImageGallerySaver.saveImage(
          pngBytes,
          name: name, <span class="hljs-comment">// Optional file base name (plugin may append extension/time)</span>
          quality: <span class="hljs-number">100</span>,
        );
        <span class="hljs-comment">// Plugin returns a platform-dependent structure. We bubble it up unchanged.</span>
        <span class="hljs-keyword">return</span> result <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map?</span>;
      }
    }
    

    Code explanation:

    1. image_gallery_saver writes the provided bytes to the photo library.

    2. We pass quality: 100 for best PNG quality. The plugin may place the file in DCIM/Pictures on Android and Photos on iOS.

    This code defines a utility class that saves raw PNG image data (bytes) into the device’s photo gallery. Let me explain it step by step:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:typed_data'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_gallery_saver/image_gallery_saver.dart'</span>;
    
    1. dart:typed_data is imported because the image is represented as Uint8List (a list of unsigned 8-bit integers, basically raw binary data).

    2. image_gallery_saver is a Flutter plugin that lets you save images and videos to the device’s gallery.

    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GallerySaverService</span> </span>{
      <span class="hljs-comment">/// <span class="markdown">Saves [pngBytes] to the gallery and returns a descriptive result map from the plugin.</span></span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-built_in">Map?</span>> savePngBytesToGallery(Uint8List pngBytes, {<span class="hljs-built_in">String?</span> name}) <span class="hljs-keyword">async</span> {
    
    1. The class is called GallerySaverService.

    2. It has a static method savePngBytesToGallery that takes:

      • pngBytes: the raw PNG image data you want to save.

      • name: an optional file name to use for the saved image.

        <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> ImageGallerySaver.saveImage(
          pngBytes,
          name: name, <span class="hljs-comment">// Optional file base name (plugin may append extension/time)</span>
          quality: <span class="hljs-number">100</span>,
        );
    
    1. ImageGallerySaver.saveImage is called to save the image to the gallery.

    2. pngBytes is passed in directly.

    3. name is optional. The plugin may add an extension like .png and/or a timestamp to ensure uniqueness.

    4. quality: 100 ensures the best quality is saved (this parameter mostly applies to JPG, but still ensures maximum fidelity).

        <span class="hljs-comment">// Plugin returns a platform-dependent structure. We bubble it up unchanged.</span>
        <span class="hljs-keyword">return</span> result <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map?</span>;
      }
    }
    
    1. The plugin returns a result that may vary depending on platform (Android or iOS). Usually it’s a map containing information like file path and whether it was successful.

    2. This method just forwards that result without altering it.

    3. as Map? ensures the return type is a nullable Map.

    In short: This class takes PNG image bytes, saves them to the user’s gallery, and returns a result map containing info about the saved file.

    6. lib/services/share_service.dart

    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:typed_data'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:path_provider/path_provider.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:share_plus/share_plus.dart'</span>;
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShareService</span> </span>{
      <span class="hljs-comment">/// <span class="markdown">Writes [pngBytes] to a temporary file and invokes the platform share sheet.</span></span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-keyword">void</span>> sharePngBytes(Uint8List pngBytes, {<span class="hljs-built_in">String?</span> text}) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">final</span> tempDir = <span class="hljs-keyword">await</span> getTemporaryDirectory();
        <span class="hljs-keyword">final</span> filePath = <span class="hljs-string">'<span class="hljs-subst">${tempDir.path}</span>/quote_<span class="hljs-subst">${DateTime.now().millisecondsSinceEpoch}</span>.png'</span>;
    
        <span class="hljs-keyword">final</span> file = File(filePath);
        <span class="hljs-keyword">await</span> file.writeAsBytes(pngBytes, flush: <span class="hljs-keyword">true</span>);
    
        <span class="hljs-keyword">await</span> Share.shareXFiles(
          [XFile(file.path)],
          text: text ?? <span class="hljs-string">'Sharing a quote from my app.'</span>,
        );
      }
    }
    

    Sharing generally requires a file path, not raw bytes. We create a temporary file, write the bytes, and pass it to share_plus using shareXFiles.

    This code defines a ShareService class in Flutter/Dart that allows you to share an image (provided as raw PNG bytes) through the platform’s native share sheet (the system dialog that lets you share to WhatsApp, Gmail, Messenger, and so on).

    Here’s a breakdown of what’s happening:

    1. Imports

      • dart:io: Gives access to the File class for reading/writing files.

      • dart:typed_data: Provides Uint8List, the data type used for raw byte arrays (like image data).

      • path_provider: Used to get system directories (in this case, a temporary directory).

      • share_plus: Provides the API for invoking the share sheet with text, files, images, and so on.

    2. Class: ShareService

      • A utility class that contains one static method sharePngBytes.
    3. Method: sharePngBytes(Uint8List pngBytes, {String? text})

      1. Step 1: Get a temporary directory using getTemporaryDirectory(). This directory is suitable for writing temporary files that don’t need to persist.

      2. Step 2: Generate a unique file path inside that temp directory. The filename uses the current timestamp (DateTime.now().millisecondsSinceEpoch) so each shared image is unique, avoiding overwrites.

      3. Step 3: Create a File object at that path and write the pngBytes into it using file.writeAsBytes(). Setting flush: true ensures the data is written immediately.

      4. Step 4: Use Share.shareXFiles from the share_plus package to open the native share sheet, passing the newly created file as an XFile. An optional text message can also be attached. If no text is provided, it defaults to “Sharing a quote from my app.”

    Why this is useful:
    Flutter apps often generate images dynamically (like screenshots, charts, or quote cards). Since the share sheet requires an actual file (not just raw bytes), this service handles the conversion from memory (Uint8List) into a temporary file, then shares it seamlessly.

    7. lib/screens/quote_screen.dart

    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:typed_data'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/quote_card.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/capture.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'../services/permission_service.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'../services/gallery_saver_service.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'../services/share_service.dart'</span>;
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">QuoteScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
      <span class="hljs-keyword">const</span> QuoteScreen({<span class="hljs-keyword">super</span>.key});
    
      <span class="hljs-meta">@override</span>
      State<QuoteScreen> createState() => _QuoteScreenState();
    }
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_QuoteScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span><<span class="hljs-title">QuoteScreen</span>> </span>{
      <span class="hljs-comment">// This key will be attached to a RepaintBoundary that wraps the quote card.</span>
      <span class="hljs-keyword">final</span> GlobalKey _captureKey = GlobalKey();
    
      <span class="hljs-built_in">bool</span> _isSaving = <span class="hljs-keyword">false</span>;
      <span class="hljs-built_in">bool</span> _isSharing = <span class="hljs-keyword">false</span>;
    
      Future<Uint8List?> _capture() <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">return</span> captureWidgetToPngBytes(_captureKey, pixelRatio: <span class="hljs-number">3.0</span>);
      }
    
      Future<<span class="hljs-keyword">void</span>> _saveImage() <span class="hljs-keyword">async</span> {
        setState(() => _isSaving = <span class="hljs-keyword">true</span>);
        <span class="hljs-keyword">try</span> {
          <span class="hljs-comment">// Request permissions when relevant (see notes in PermissionService).</span>
          <span class="hljs-keyword">final</span> granted = <span class="hljs-keyword">await</span> PermissionService.requestSavePermission();
          <span class="hljs-keyword">if</span> (!granted) {
            <span class="hljs-keyword">if</span> (mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Permission required to save images.'</span>)),
              );
            }
            <span class="hljs-keyword">return</span>;
          }
    
          <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> _capture();
          <span class="hljs-keyword">if</span> (bytes == <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">if</span> (mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Failed to capture image.'</span>)),
              );
            }
            <span class="hljs-keyword">return</span>;
          }
    
          <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> GallerySaverService.savePngBytesToGallery(
            bytes,
            name: <span class="hljs-string">'quote_<span class="hljs-subst">${DateTime.now().millisecondsSinceEpoch}</span>'</span>,
          );
    
          <span class="hljs-keyword">if</span> (mounted) {
            <span class="hljs-keyword">final</span> ok = result != <span class="hljs-keyword">null</span>;
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(ok ? <span class="hljs-string">'Image saved to gallery.'</span> : <span class="hljs-string">'Save failed.'</span>)),
            );
          }
        } <span class="hljs-keyword">catch</span> (e) {
          <span class="hljs-keyword">if</span> (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(<span class="hljs-string">'Error: <span class="hljs-subst">$e</span>'</span>)),
            );
          }
        } <span class="hljs-keyword">finally</span> {
          <span class="hljs-keyword">if</span> (mounted) setState(() => _isSaving = <span class="hljs-keyword">false</span>);
        }
      }
    
      Future<<span class="hljs-keyword">void</span>> _shareImage() <span class="hljs-keyword">async</span> {
        setState(() => _isSharing = <span class="hljs-keyword">true</span>);
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> _capture();
          <span class="hljs-keyword">if</span> (bytes == <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">if</span> (mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Failed to capture image.'</span>)),
              );
            }
            <span class="hljs-keyword">return</span>;
          }
          <span class="hljs-keyword">await</span> ShareService.sharePngBytes(bytes, text: <span class="hljs-string">'Here is a quote I wanted to share.'</span>);
        } <span class="hljs-keyword">catch</span> (e) {
          <span class="hljs-keyword">if</span> (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(<span class="hljs-string">'Error: <span class="hljs-subst">$e</span>'</span>)),
            );
          }
        } <span class="hljs-keyword">finally</span> {
          <span class="hljs-keyword">if</span> (mounted) setState(() => _isSharing = <span class="hljs-keyword">false</span>);
        }
      }
    
      <span class="hljs-meta">@override</span>
      Widget build(BuildContext context) {
        <span class="hljs-keyword">const</span> quote = <span class="hljs-string">"Believe you can and you're halfway there."</span>;
        <span class="hljs-keyword">const</span> author = <span class="hljs-string">'Theodore Roosevelt'</span>;
    
        <span class="hljs-keyword">return</span> Scaffold(
          appBar: AppBar(
            title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Quote Share'</span>),
            centerTitle: <span class="hljs-keyword">true</span>,
          ),
          body: Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">20.0</span>),
            child: Column(
              children: [
                <span class="hljs-comment">// The RepaintBoundary must directly wrap the content you want to capture.</span>
                RepaintBoundary(
                  key: _captureKey,
                  child: <span class="hljs-keyword">const</span> QuoteCard(
                    quote: quote,
                    author: author,
                  ),
                ),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">24</span>),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    FilledButton.icon(
                      onPressed: _isSaving ? <span class="hljs-keyword">null</span> : _saveImage,
                      icon: _isSaving
                          ? <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">16</span>, height: <span class="hljs-number">16</span>, child: CircularProgressIndicator(strokeWidth: <span class="hljs-number">2</span>))
                          : <span class="hljs-keyword">const</span> Icon(Icons.download),
                      label: Text(_isSaving ? <span class="hljs-string">'Saving...'</span> : <span class="hljs-string">'Save'</span>),
                    ),
                    OutlinedButton.icon(
                      onPressed: _isSharing ? <span class="hljs-keyword">null</span> : _shareImage,
                      icon: _isSharing
                          ? <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">16</span>, height: <span class="hljs-number">16</span>, child: CircularProgressIndicator(strokeWidth: <span class="hljs-number">2</span>))
                          : <span class="hljs-keyword">const</span> Icon(Icons.share),
                      label: Text(_isSharing ? <span class="hljs-string">'Sharing...'</span> : <span class="hljs-string">'Share'</span>),
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Code explanation highlights:

    1. GlobalKey _captureKey identifies the RepaintBoundary.

    2. Buttons call _saveImage and _shareImage.

    3. We show progress indicators and disable buttons while busy.

    4. SnackBars provide user feedback for success or errors.

    Let’s break down what each part of this code is doing step by step.

    State variables

    <span class="hljs-built_in">bool</span> _isSaving = <span class="hljs-keyword">false</span>;
    <span class="hljs-built_in">bool</span> _isSharing = <span class="hljs-keyword">false</span>;
    
    1. _isSaving is used to track whether an image is currently being saved.

    2. _isSharing is used to track whether an image is currently being shared.

    3. These flags can be used to disable UI buttons, show a loading spinner, or prevent duplicate actions while the save/share process is in progress.

    Capture function

    Future<Uint8List?> _capture() <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">return</span> captureWidgetToPngBytes(_captureKey, pixelRatio: <span class="hljs-number">3.0</span>);
    }
    
    1. This function captures a Flutter widget (referenced by _captureKey) and converts it into PNG image bytes (Uint8List).

    2. pixelRatio: 3.0 ensures the captured image is high resolution (3x the screen density).

    3. It returns the raw PNG bytes that can later be saved or shared.

    Save function

    Future<<span class="hljs-keyword">void</span>> _saveImage() <span class="hljs-keyword">async</span> {
      setState(() => _isSaving = <span class="hljs-keyword">true</span>);
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">final</span> granted = <span class="hljs-keyword">await</span> PermissionService.requestSavePermission();
        <span class="hljs-keyword">if</span> (!granted) {
          <span class="hljs-keyword">if</span> (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Permission required to save images.'</span>)),
            );
          }
          <span class="hljs-keyword">return</span>;
        }
    
    1. Sets _isSaving to true to indicate saving has started.

    2. Requests storage/gallery permissions using PermissionService.

    3. If permission is not granted, shows a SnackBar and stops.

        <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> _capture();
        <span class="hljs-keyword">if</span> (bytes == <span class="hljs-keyword">null</span>) {
          <span class="hljs-keyword">if</span> (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Failed to capture image.'</span>)),
            );
          }
          <span class="hljs-keyword">return</span>;
        }
    
    1. Captures the widget as PNG bytes.

    2. If capture fails, shows an error and exits.

        <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> GallerySaverService.savePngBytesToGallery(
          bytes,
          name: <span class="hljs-string">'quote_<span class="hljs-subst">${DateTime.now().millisecondsSinceEpoch}</span>'</span>,
        );
    
    1. Saves the PNG bytes into the device’s photo gallery.

    2. A unique filename is generated using the current timestamp.

        <span class="hljs-keyword">if</span> (mounted) {
          <span class="hljs-keyword">final</span> ok = result != <span class="hljs-keyword">null</span>;
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(ok ? <span class="hljs-string">'Image saved to gallery.'</span> : <span class="hljs-string">'Save failed.'</span>)),
          );
        }
      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-keyword">if</span> (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(<span class="hljs-string">'Error: <span class="hljs-subst">$e</span>'</span>)),
          );
        }
      } <span class="hljs-keyword">finally</span> {
        <span class="hljs-keyword">if</span> (mounted) setState(() => _isSaving = <span class="hljs-keyword">false</span>);
      }
    }
    
    1. If saving succeeds, shows a success message; otherwise, shows failure.

    2. If an exception occurs, shows an error with details.

    3. Finally, resets _isSaving back to false.

    Share function

    Future<<span class="hljs-keyword">void</span>> _shareImage() <span class="hljs-keyword">async</span> {
      setState(() => _isSharing = <span class="hljs-keyword">true</span>);
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> _capture();
        <span class="hljs-keyword">if</span> (bytes == <span class="hljs-keyword">null</span>) {
          <span class="hljs-keyword">if</span> (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              <span class="hljs-keyword">const</span> SnackBar(content: Text(<span class="hljs-string">'Failed to capture image.'</span>)),
            );
          }
          <span class="hljs-keyword">return</span>;
        }
        <span class="hljs-keyword">await</span> ShareService.sharePngBytes(bytes, text: <span class="hljs-string">'Here is a quote I wanted to share.'</span>);
    
    1. Sets _isSharing to true at the start.

    2. Captures the widget as PNG bytes.

    3. If successful, calls ShareService.sharePngBytes to share the image with some text. This will typically open the system share sheet (WhatsApp, Email, and so on).

      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-keyword">if</span> (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(<span class="hljs-string">'Error: <span class="hljs-subst">$e</span>'</span>)),
          );
        }
      } <span class="hljs-keyword">finally</span> {
        <span class="hljs-keyword">if</span> (mounted) setState(() => _isSharing = <span class="hljs-keyword">false</span>);
      }
    }
    
    1. If an error occurs, shows a SnackBar.

    2. Resets _isSharing back to false after completion.

    Summary:

    1. _capture() converts a widget into an image (PNG bytes).

    2. _saveImage() captures the widget, checks permissions, and saves it to the gallery while handling errors and state.

    3. _shareImage() captures the widget and shares it using the system share options while handling errors and state.

    4. _isSaving and _isSharing are flags that help manage UI state during operations.

    Testing the Flow

    To test this setup, you’ll want to run it on a real device for the most accurate behavior.

    First, tap Share. The share sheet should appear and allow sending the image via installed apps. Then tap Save. On some devices you may be prompted for permission – accept it. Check your Photos or Gallery app for the saved image.

    If the image appears blurry, increase pixelRatio in captureWidgetToPngBytes to 3.0 or 4.0. Be mindful of memory.

    Troubleshooting and Common Pitfalls

    There are a number of common issues you might come across while saving and sharing your Flutter widgets as images. But don’t worry – we’ll address a lot of them quickly and efficiently here.

    First, let’s say the saved image is empty or black. To fix this, make sure that the widget is fully painted. We already wait briefly if debugNeedsPaint is true. Also, make sure the RepaintBoundary directly wraps the target content and not a parent that has zero size.

    What if permission is denied on Android even though you allowed it? Well, some OEMs have aggressive storage policies. Try again, and confirm the app has Photos or Files access in system settings. If your targetSdk is very new, just make sure that your plugins are updated.

    If the image isn’t visible in the Gallery, just give it a moment – some galleries index asynchronously. YOu can also try another gallery app to confirm the file exists.

    If sharing fails on iOS simulator, some share targets are unavailable in the simulator. Just try testing on a real device.

    Lastly, if you have blurry or jagged text, you can increase pixelRatio in toImage and add padding around the card so shadows and edges are captured cleanly.

    Enhancements and Alternatives

    Use a programmatic watermark or logo

    Instead of capturing a plain QuoteCard, you can overlay a small brand logo or watermark widget before taking the screenshot. This helps with branding (users know where the quote came from) and discourages unauthorized reuse. A simple way is to wrap the card in a Stack and place a Positioned logo in a corner.

    Use dynamic backgrounds

    Rather than using a flat color, you could make the captured quote more visually engaging by adding gradient fills or even image backgrounds. For example, a gradient background can subtly elevate the design, while thematic images can match the tone of the quote (e.g., nature shots for inspirational quotes). Flutter’s BoxDecoration with gradients or an Image.asset/Image.network background makes this straightforward.

    Have multiple capture targets

    If your app needs to capture more than just the quote card (for example, profile cards, stats, or receipts), you don’t want a single GlobalKey. A map of keys like Map<String, GlobalKey> lets you reference and capture the right widget dynamically. This adds flexibility and keeps your capture logic reusable across multiple UI components.

    Alternative packages

    There are some other packages you can consider using, like:

    • screenshot: Provides a higher-level API that can simplify screen capturing without manually juggling RepaintBoundary and keys. Particularly useful for capturing the entire screen or full widgets with less boilerplate.

    • widgets_to_image: Another option that focuses on turning specific widgets into images with a slightly different API style. Could be more ergonomic depending on your use case.

    • PDF generation (printing / pdf): If your use case involves creating shareable documents rather than images (e.g., a formatted quote booklet), these packages are a better fit since they work with vector-based content and are resolution-independent.

    Caching and performance

    Capturing widgets frequently can create memory churn and slow down the app if every capture is re-rendered from scratch. Adding caching strategies (for example, keeping the last rendered image in memory) can reduce overhead. If you write captures to disk (outside of the OS’s temporary directories), make sure you clean them up after sharing to avoid filling up user storage. Throttling rapid captures (for example, debounce a “Save Quote” button) is also a good practice to keep the UI responsive.

    Conclusion

    You now have a complete, production-ready approach for capturing a Flutter widget to an image, saving it to the gallery, and sharing it through the native share sheet. The key pieces are RepaintBoundary for pixel-perfect capture, careful handling of platform permissions, and small services that keep UI code clean. This pattern generalizes well to certificates, reports, memes, flashcards, and any other visual content your app creates.

    If you prefer a more batteries-included path, the screenshot package can achieve a similar result with slightly different tradeoffs.

    References

    1. share_plus Flutter Package Documentation

    2. image_gallery_saver Flutter Package Documentation

    3. path_provider Flutter Package Documentation

    4. Flutter Official Documentation: File Handling

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticlePerficient is Heading to Oracle AI World 2025 – Let’s Talk AI!
    Next Article Why You Should Test Your Page Without JavaScript

    Related Posts

    Development

    How to Make Bluetooth on Android More Reliable

    September 4, 2025
    Development

    Learn Mandarin Chinese for Beginners – Full HSK 1 Level

    September 4, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    CVE-2024-51983 – Cisco Web Services Crash DoS

    Common Vulnerabilities and Exposures (CVEs)

    CVE-2025-32819 – SonicWall SMA SSLVPN File Deletion Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    Tariffs could increase game console prices by up to 69% (not nice)

    News & Updates

    Nitrux: Passaggio da NX Desktop a Hyprland

    Linux

    Highlights

    RM Fiber Optic SC LC ST Connectors Price in India – Best Deals and Competitive Pricing

    May 9, 2025

    Post Content Source: Read More 

    CVE-2025-4988 – “3DEXPERIENCE Stored XSS”

    May 30, 2025

    CVE-2025-4646 – Centreon Web Privilege Escalation Vulnerability

    May 13, 2025

    Debian 13 “Trixie” è Arrivata: Guida ai Primi Errata e Aggiornamenti Necessari

    August 11, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.