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
Why Legacy Systems Make Business Growth More Expensive: Navigate A Smarter Path to Legacy Modernization
Sep 23, 2026

Why Legacy Systems Make Business Growth More Expensive: Navigate A Smarter Path to Legacy Modernization

Learn how legacy systems make business growth more expensive and how edge-first modernization can remove constraints without replacing the existing system.

Insight
SSO, Audit Logs and RBAC: The Enterprise Features AI Prototyping Tools Do Not Cover | Sarika Gautam
Sep 23, 2026

SSO, Audit Logs and RBAC: The Enterprise Features AI Prototyping Tools Do Not Cover | Sarika Gautam

Why AI-generated prototypes fail enterprise review: the context behind SSO, the cost of skipping audit logs, and how role explosion makes RBAC a product of its own.

Insight
Feature Flags as Technical Debt: The Cleanup Nobody Schedules
Sep 21, 2026

Feature Flags as Technical Debt: The Cleanup Nobody Schedules

This blog explains how unmanaged feature flags create technical debt and how teams can detect, manage, and remove them safely.

Insight
Building AI-First Enterprises: Why System Design Matters More Than AI Adoption
Sep 21, 2026

Building AI-First Enterprises: Why System Design Matters More Than AI Adoption

This blog explores how system design, architecture, and validation shape AI-first enterprises, while examining AI’s impact on software engineering and human decision-making.

Insight
The Product Studio in the AI Era: What Actually Changes | Sarika Gautam
Sep 21, 2026

The Product Studio in the AI Era: What Actually Changes | Sarika Gautam

What changes in product development when AI writes the code: the shift to architecture, the token cost of unplanned builds, and why juniors still matter.

Insight
AI and the Future of Digital Customer Experience: Where Technology Meets Human Creativity
Sep 18, 2026

AI and the Future of Digital Customer Experience: Where Technology Meets Human Creativity

A discussion on how AI, human creativity, research, and cross-functional collaboration are shaping the future of digital customer experience.

Insight
Scaling Down Before Scaling Up: Why Bigger Servers Don’t Fix Bad Architecture
Sep 17, 2026

Scaling Down Before Scaling Up: Why Bigger Servers Don’t Fix Bad Architecture

This blog explores how inefficient backend architecture can cause performance issues even under low traffic, covering practical ways to reduce database load, API latency, and resource usage before scaling infrastructure.

The Right Conversation Can

Save You Six Months.

Book a call