Image Cropping And Optimised Transfer In Flutter

Jul 19, 2024

Image Cropping And Optimised Transfer In Flutter

Boost app performance with efficient image cropping and optimization. This guide details selecting, resizing, and compressing images for faster loading and a better user experience.

Image cropping and optimization are crucial in App Development to enhance user experience and performance. Though image optimization and cropping might not seem like necessary tasks, they are recommended if your application shows a lot of graphical images, transfers images on the network, provides camera functionalities, etc.

Cropping images allows developers to focus on essential parts of an image, improving visual appeal and clarity. This can be especially important for profile pictures, thumbnails, and feature images, where displaying the most relevant part is key.

Optimization, on the other hand, reduces the file size of images. This leads to faster loading times, reduced bandwidth usage, and a smoother user experience.

Efficient image handling is essential for high performance, particularly in mobile apps with more significant resource constraints. By incorporating image cropping and optimization, we can ensure that the apps are visually appealing, efficient, and user-friendly.

Let us discuss the above functionalities step by step:

  • Image Picker
  • Image Cropping And Optimization
  • Image Transfer Through Network

  1. Image Picker :

The image_picker package in Flutter offers a straightforward solution for selecting images from a device's gallery or camera. By integrating this package, developers can enable users to easily pick images directly within the app, enhancing functionality and user engagement. This package was picked because of its easy-to-use functions.

We have created a function _pickImage(), as below, which picks an image from the gallery and checks for its size. To avoid any memory issues, we only permit images below or equal to 2 MB to be picked. This is the first step towards optimising our image, as this will allow the app to deal only with a specific size of images.


  Future<void> _pickImage() async {
    final picker = ImagePicker();
    final pickedImage =
        await picker.pickImage(source: ImageSource.gallery);

    if (pickedImage != null) {
      File image = File(pickedImage.path);

      //getting size of image
      final Uint8List imageUint8List = image.readAsBytesSync();
      final bytes = imageUint8List.lengthInBytes;
      final kb = bytes / 1024;
      final mb = kb / 1024;
      //allows picking up images only if size <= 2 MB
      if (mb <= 2) {
        final result = await Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => CropImage(
                imageModel: CropImagePageModel(image: imageUint8List)),
          ),
        );
        if (result != null && (result is CropImagePageModel)) {
          imageModel = CropImagePageModel(image: result.image);
          setState(() {});
        }
      } else {
        // Show the Snackbar when the image size is greater then 2 mb
        const snackBar = SnackBar(
          backgroundColor: Colors.red,
          content: Text("File should be less than or equal to 2 MB"),
          duration: Duration(
              seconds:
                  1), // Optional: Set the duration the Snackbar will be displayed
        );
        ScaffoldMessenger.of(context).showSnackBar(snackBar);
      }
    }
  }
  1. Image Cropping And Optimization :

We use the crop_your_image package from pub.dev because of its wide popularity and great pub rating.

The package supports customizable cropping areas and aspect ratios, making it versatile for various use cases, such as profile picture adjustments or product image refinement.

Cropping an image allows the user to select only the necessary parts of an image. The cropping feature's usage depends on the application's nature; however, keeping the crop feature within your applications, especially for applications dealing with profile photos, feature photos, wallpapers, thumbnails, etc., will be a great way to optimize your images in the long run. This will allow the usage of only required details and remove the irrelevant parts. The Crop() widget provided by the package allows for a designated cropping area to be shown, which can be adjusted and zoomed as required. You can also specify the area, colors, backgrounds, onCrop methods, etc, based on your specific requirement.

Crop(
      image: widget.imageModel.image,
      // can be set as per required ratio
      aspectRatio: 16 / 16,
      interactive: true,
      //define the size of your cropping area below
      initialSize: 1,
      maskColor: Colors.black54,
      cornerDotBuilder: (size, edgeAlignment) {
       return const SizedBox();
       },
      radius: 40,
      fixCropRect: true,
      controller: _controller,
      onCropped: (image) {
       //RESIZING IMAGE TO A FIXED SIZE AND REDUCING QUALITY
      resizedImageBytes = Uint8List.fromList(
      img.encodeJpg(
      img.copyResize(
      img.decodeImage(image)!, // Decode the image
       width: 300, // Set the width to 200 pixels
       height: 300, // Set the height to 200 pixels
          ),
       //reducing quality to 75%
        quality: 75),
        );
         setState(() {});
         final CropImagePageModel result =
         CropImagePageModel(image: resizedImageBytes!);
         Navigator.pop(context, result);
          },  
       )               

The above function onCropped( ) also performs a series of steps to resize and compress an image, producing a smaller and more efficient version of the original. Here's what each part does:

Decoding the Image:

  • img.decodeImage(image)! converts the image from its original format into a format that can be manipulated programmatically. The exclamation mark ensures that the image is not null.

Resizing the Image:

  • img.copyResize(...) resizes the decoded image to a width and height of 300 pixels. This helps reduce the image's dimensions to the specified size.

Compressing the Image:

  • img.encodeJpg(..., quality: 75) converts the resized image back to JPEG format while reducing the quality to 75%. This further reduces the file size while maintaining reasonable image quality.

Creating a Uint8List:

  • Uint8List.fromList(...) takes the compressed image bytes and converts them into a Uint8List, a format suitable for further processing or storage.

    1. Image Transfer Through Network:
  • We can ensure that the image transferred through the network to our BE services in the Flutter app remains efficient, secure, and fast.
  • The first and foremost step is compressing your image using packages, as in the above steps. These packages significantly reduce the size of your images, ensuring faster transfer.
  • Secondly, convert the compressed image to a Base64 string if your backend expects this format. This is completely optional, as sending raw bytes is typically more efficient.
  • Thirdly, multipart/form-data requests, which are supported by most backend frameworks and designed for sending files, should be used for large images.
  • Lastly, always use HTTPS to encrypt the data during transmission to ensure the image is sent securely. Add an authentication token in the request headers to ensure only authorized users can upload images.

 // Create a multipart request
  var request = http.MultipartRequest('POST', Uri.parse('https://your-backend-url.com/upload'));
  request.files.add(http.MultipartFile.fromBytes('image', resizedImageBytes, filename: 'image.jpg'));
  // Add authentication token if required
  request.headers['Authorization'] = 'Bearer your_auth_token';
  // Send the request
  var response = await request.send();
  if (response.statusCode == 200) {
    print('Image uploaded successfully!');
  } else {
    print('Image upload failed with status: ${response.statusCode}');
  }

Full Example Snippet:

import 'dart:io';
import 'dart:typed_data';
import 'package:crop_your_image/crop_your_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_image_demo/image_model.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image/image.dart' as img;

void main() {
  runApp(const MyApp1());
}

class MyApp1 extends StatelessWidget {
  const MyApp1({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Image Demo',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        home: const MyApp());
  }
}

class CropImagePageModel {
  final Uint8List image;
  CropImagePageModel({
    required this.image,
  });
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  CropImagePageModel? imageModel;
  bool isLoading = true;

  Future<void> _pickImage() async {
    final picker = ImagePicker();
    final pickedImage =
        await picker.pickImage(source: ImageSource.gallery, imageQuality: 20);

    if (pickedImage != null) {
      File image = File(pickedImage.path);

      //getting size of image
      final Uint8List imageUint8List = image.readAsBytesSync();
      final bytes = imageUint8List.lengthInBytes;
      final kb = bytes / 1024;
      final mb = kb / 1024;
      //allows picking up images only if size <= 2 MB
      if (mb <= 2) {
        final result = await Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => CropImage(
                imageModel: CropImagePageModel(image: imageUint8List)),
          ),
        );
        if (result != null && (result is CropImagePageModel)) {
          imageModel = CropImagePageModel(image: result.image);
          setState(() {});
        }
      } else {
        // Show the Snackbar when the image size is greater then 2 mb
        const snackBar = SnackBar(
          backgroundColor: Colors.red,
          content: Text("file should be"),
          duration: Duration(
              seconds:
                  1), // Optional: Set the duration the Snackbar will be displayed
        );
        ScaffoldMessenger.of(context).showSnackBar(snackBar);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Image Picker Demo'),
      ),
      body: Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          (imageModel != null && imageModel?.image != null)
              ? Column(
                  children: [
                    Image.memory(imageModel!.image),
                    Text(
                        'Optimized size: ${imageModel!.image.lengthInBytes} bytes'),
                  ],
                )
              : const SizedBox(),
          const SizedBox(
            height: 20,
          ),
          MaterialButton(
            color: Colors.black,
            onPressed: () {
              _pickImage();
            },
            child: const Text(
              'Pick Image',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      )),
    );
  }
}

class CropImage extends StatefulWidget {
  final CropImagePageModel imageModel;
  const CropImage({super.key, required this.imageModel});

  @override
  State<CropImage> createState() => _CropImageState();
}

class _CropImageState extends State<CropImage> {
  final _controller = CropController();
  bool cropped = false;
  Uint8List? resizedImageBytes;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Container(
          color: Colors.white,
          child: Stack(
            children: <Widget>[
              Center(
                child: cropped == false
                    ? Padding(
                        padding: const EdgeInsets.all(0),
                        child: Crop(
                          image: widget.imageModel.image,
                          // can be set as per required ratio
                          aspectRatio: 16 / 16,
                          interactive: true,
                          //define the size of your cropping area below
                          initialSize: 1,
                          maskColor: Colors.black54,
                          cornerDotBuilder: (size, edgeAlignment) {
                            return const SizedBox();
                          },
                          radius: 40,
                          fixCropRect: true,
                          controller: _controller,
                          onCropped: (image) {
                            //RESIZING IMAGE TO A FIXED SIZE AND REDUCING QUALITY
                            resizedImageBytes = Uint8List.fromList(
                              img.encodeJpg(
                                  img.copyResize(
                                    img.decodeImage(image)!, // Decode the image
                                    width: 300, // Set the width to 200 pixels
                                    height: 300, // Set the height to 200 pixels
                                  ),
                                  //reducing quality to 75%
                                  quality: 75),
                            );
                            setState(() {});
                            final CropImagePageModel result =
                                CropImagePageModel(image: resizedImageBytes!);
                            Navigator.pop(context, result);
                          },
                        ),
                      )
                    : const SizedBox(
                        height: 30,
                        width: 30,
                        child: CircularProgressIndicator(
                          color: Colors.white,
                        )),
              ),
              Padding(
                padding: const EdgeInsets.only(
                    left: 30.0, right: 30.0, bottom: 8.0, top: 20.0),
                child: Row(
                  children: [
                    GestureDetector(
                      onTap: () {
                        Navigator.pop(context);
                      },
                      child: const Icon(
                        Icons.arrow_back,
                        color: Colors.white,
                        size: 30,
                      ),
                    ),
                    const SizedBox(
                      width: 20,
                    ),
                    Text(
                      'Move and zoom',
                      style: Theme.of(context)
                          .textTheme
                          .headlineMedium
                          ?.copyWith(fontSize: 20, color: Colors.white),
                    )
                  ],
                ),
              ),
              Positioned(
                bottom: 200,
                right: 100,
                child: Text(
                  'Original size: ${(widget.imageModel.image.lengthInBytes)} bytes',
                  style: const TextStyle(color: Colors.white),
                ),
              ),
              Positioned(
                bottom: 40,
                right: 30,
                child: GestureDetector(
                  onTap: () {
                    _controller.crop();
                    if (mounted) {
                      setState(() {
                        cropped = true;
                      });
                    }
                  },
                  child: Container(
                    decoration: BoxDecoration(
                        color: Colors.yellow,
                        borderRadius: BorderRadius.circular(100)),
                    child: const Padding(
                      padding: EdgeInsets.all(8.0),
                      child: Icon(
                        Icons.check,
                        color: Colors.black,
                        size: 40,
                      ),
                    ),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}


Key Takeaways

Integrating image cropping and optimization into app development is crucial for improving user experience and app performance. While these processes might appear optional, they are vital when handling multiple graphical images. They serve their own advantages with respect to each feature discussed above.

  • Image Cropping: Enhances visual appeal by focusing on essential image parts.
  • Optimization: Reduces file size for faster loading and reduced bandwidth usage.
  • Efficient Image Handling: Maintains high performance in resource-constrained mobile environments.

Implementing these practices ensures that your app remains efficient, visually appealing, and provides a better overall user experience.

Subscribe to Our Newsletter

More from the engineering frontline.

Dive deep into our research and insights on design, development, and the impact of various trends to businesses.
Insight
Building a Production-Ready Canva-like Editor with Konva.js, React 19 and Next.js 15
Sep 10, 2026

Building a Production-Ready Canva-like Editor with Konva.js, React 19 and Next.js 15

This blog explains how to build a production-ready canvas editor with Konva.js, React, and Next.js, covering architecture, performance, and key engineering decisions.

Insight
What a PHP-to-NestJS Banking Migration Taught Us About Architecture, Security, and Trust
Sep 8, 2026

What a PHP-to-NestJS Banking Migration Taught Us About Architecture, Security, and Trust

This blog explores the architecture, security, performance, and documentation lessons from migrating a legacy PHP/Laravel banking platform to NestJS.

Insight
Building Production-Grade Video Thumbnail Scrubbing in the Browser: HLS, Frame Extraction, Caching, and Performance Trade-offs
Sep 7, 2026

Building Production-Grade Video Thumbnail Scrubbing in the Browser: HLS, Frame Extraction, Caching, and Performance Trade-offs

This blog explains how to build responsive video thumbnail scrubbing in the browser for local files and HLS streams, covering frame extraction, caching, and performance trade-offs.

Insight
The Agent Can See Your App. How Often Can It Look?
Sep 4, 2026

The Agent Can See Your App. How Often Can It Look?

AI coding agents can now interact with mobile apps, but their effectiveness depends on iteration speed. This blog explores how React Native architecture influences feedback loops and AI-driven developer productivity.

Insight
Building Interactive Cards from Design JSON Without Killing Your Feed: Overlays, Video, Mute/Unmute, and Lag-Free Lists
Sep 1, 2026

Building Interactive Cards from Design JSON Without Killing Your Feed: Overlays, Video, Mute/Unmute, and Lag-Free Lists

Learn how to turn design JSON into interactive, video-enabled cards using overlays, smart media controls, caching, and virtualization without slowing down high-cardinality feeds.

Insight
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
Aug 19, 2026

The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari

A real-world look at how oversized images can trigger Safari reloads and iOS crashes in Flutter apps and how smarter image decoding prevents them.

Insight
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
Aug 19, 2026

From Prompting to Process: What Changed When Flutter Shipped Agent Skills

This blog explores how Flutter Agent Skills improve AI-assisted development by combining official framework workflows with project-specific guidance for more consistent development.

The Right Conversation Can

Save You Six Months.

Book a call
Image Cropping And Optimised Transfer In Flutter - GeekyAnts