Effective Image Compression in Flutter: A Step-by-Step Guide
Optimizing image size can considerably improve efficiency when transmitting photos or displaying them in galleries. This post explains how to add image compression to a Flutter project.
dependencies:
flutter:
sdk: flutter
image: ^4.2.0
image_picker: ^1.1.2
path_provider: ^2.1.4
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart';
- Isolate for Image Processing
To keep our app snappy, we use Dart’s Isolate to process images on a separate thread. This prevents the user interface from freezing during compression.
2. Compressing and Resizing Images
When a user picks an image, the following steps are executed:
- Picking the Image: We utilize the image_picker package to allow users to select an image from their device.
- Resizing and Compressing: The selected image is passed to a function that spawns an isolate. Inside the isolate, we decode the image, resize it (keeping the aspect ratio), and compress it to a JPEG format with a specified quality.
- Here’s how the compression function is structured:
Future<void> pickAndProcessImage() async {
setState(() {
_isLoading = true;
});
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
File imageFile = File(pickedFile.path);
String compressedFilePath = await callReceiver(imageFile);
setState(() {
_compressedImagePaths.add(compressedFilePath);
});
if (kDebugMode) {
print('Compressed image saved at: $compressedFilePath');
}
} else {
if (kDebugMode) {
print('No image selected.');
}
}
setState(() {
_isLoading = false;
});
}
Future<dynamic> sendReceive(SendPort sendPort, String filePath) {
final response = ReceivePort();
sendPort.send([filePath, response.sendPort]);
return response.first;
}
Future<String> callReceiver(File file) async {
final receivePort = ReceivePort();
// Spawn an isolate
await Isolate.spawn(_processImage, receivePort.sendPort);
final sendPort = await receivePort.first as SendPort;
final result = await sendReceive(sendPort, file.path);
return result; // Return the path of the compressed file
}
void _processImage(SendPort sendPort) async {
final port = ReceivePort();
sendPort.send(port.sendPort);
await for (var message in port) {
final filePath = message[0] as String;
final send = message[1] as SendPort;
// Process the image
File file = File(filePath);
img.Image? images = img.decodeImage(await file.readAsBytes());
if (images != null) {
int width, height;
if (images.width > images.height) {
width = 800;
height = (images.height / images.width * 800).round();
} else {
height = 800;
width = (images.width / images.height * 800).round();
}
img.Image resizeImage = img.copyResize(images, width: width, height: height);
// you can adjust image quality as per your needs
List<int> compressBytes = img.encodeJpg(resizeImage, quality: 85);
// Save the compressed image
File compressFile = File(file.path.replaceFirst('.jpg', 'compress.jpg'));
compressFile.writeAsBytesSync(compressBytes);
// Send the path back
send.send(compressFile.path);
} else {
send.send('Error: Image could not be decoded.');
}
}
}
3. Saving the Compressed Image
The compressed image is kept in the device’s application for easy access later.
File compressFile = File(file.path.replaceFirst('.jpg', 'compress.jpg'));
compressFile.writeAsBytesSync(compressBytes);
4. User Interface
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Image Compression',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
bool _isLoading = false;
final List<String> _compressedImagePaths = [];
Future<void> pickAndProcessImage() async {
setState(() {
_isLoading = true;
});
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
File imageFile = File(pickedFile.path);
String compressedFilePath = await callReceiver(imageFile);
setState(() {
_compressedImagePaths.add(compressedFilePath);
});
if (kDebugMode) {
print('Compressed image saved at: $compressedFilePath');
}
} else {
if (kDebugMode) {
print('No image selected.');
}
}
setState(() {
_isLoading = false;
});
}
Future<String> callReceiver(File file) async {
final receivePort = ReceivePort();
// Spawn an isolate
await Isolate.spawn(_processImage, receivePort.sendPort);
final sendPort = await receivePort.first as SendPort;
final result = await sendReceive(sendPort, file.path);
return result; // Return the path of the compressed file
}
Future<dynamic> sendReceive(SendPort sendPort, String filePath) {
final response = ReceivePort();
sendPort.send([filePath, response.sendPort]);
return response.first;
}
static void _processImage(SendPort sendPort) async {
final port = ReceivePort();
sendPort.send(port.sendPort);
await for (var message in port) {
final filePath = message[0] as String;
final send = message[1] as SendPort;
// Process the image
File file = File(filePath);
img.Image? images = img.decodeImage(await file.readAsBytes());
if (images != null) {
int width, height;
if (images.width > images.height) {
width = 800;
height = (images.height / images.width * 800).round();
} else {
height = 800;
width = (images.width / images.height * 800).round();
}
img.Image resizeImage = img.copyResize(images, width: width, height: height);
// you can adjust image quality as per your needs
List<int> compressBytes = img.encodeJpg(resizeImage, quality: 85);
// Save the compressed image
File compressFile = File(file.path.replaceFirst('.jpg', 'compress.jpg'));
compressFile.writeAsBytesSync(compressBytes);
// Send the path back
send.send(compressFile.path);
} else {
send.send('Error: Image could not be decoded.');
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Image Picker Example')),
body: Center(
child: _isLoading
? const CircularProgressIndicator()
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: pickAndProcessImage,
child: const Text('Pick and Compress Image'),
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: _compressedImagePaths.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Image ${index + 1}'),
subtitle: Text(_compressedImagePaths[index]),
);
},
),
),
],
),
),
);
}
}
Conclusion:
By utilizing Dart isolates for image compression, we not only assure a seamless user experience but also properly control image sizes, which can lead to improved app performance and lower storage requirements.