Handling File Upload, Compression, and Encryption using AES Algorithm in Flutter Applications
File compression is the process of reducing the logical size of a file to save disk space. It also involves network optimization of files for easier transfer over the internet. <!--more--> On the other hand, file encryption is a way of encoding files, including the sensitive data they contain, to send them securely. This prevents unauthorized access and tampering.
Goal
This tutorial will guide the reader on how to create a simple Flutter mobile application that will be able to:
- Accept files such as images, videos, or documents from the phone's local storage.
- Compress the files without losing their quality.
- Encrypt the files using the AES encryption algorithm before uploading the file to Firebase storage.
Prerequisites
- A solid understanding of the Dart programming language.
- Knowledge of working with Flutter widgets.
- An IDE that supports Flutter such as Visual Studio Code. This tutorial uses Android Studio with the Flutter plugin installed.
We will be using Flutter version 2.10.4 in this tutorial. If you do not have Flutter SDK installed, visit Flutter Docs for the installation process.
Project overview
Open Android Studio and create a new Flutter application with the name flutter-app-demo
.
After the project has been initialized, we will create our project structure.
The project will have four routes. We will go to the lib
folder of our project and create a new folder called screens
and here, we will add our routes.
The routes are as follows:
- Home screen - This will be our startup page when the application loads. It will go by the name,
home.screen.dart
. - Image screen - This page will be called
image.screen.dart
. It will allow the user to pick and upload an image. - Video screen - This page will be called
video.screen.dart
. It will allow the user to pick a video and upload it to Firebase storage. - Document screen - This will be called
document.screen.dart
. It will allow the user to pick a document and manipulate it.
Next, we will create another folder under the lib
folder called services
. This folder will contain the APIs that we will use for the functionality of our application.
We will create the following APIs:
-
A Firebase API - It will contain the code that will enable us to upload files to Firebase storage.
-
File picker API - This API will enable us to pick files from the local storage.
-
File compression API - This APi will enable us to compress images and videos picked from the gallery.
-
File encryption API - This API will enable us to encrypt a file.
Setting up Firebase
To set up Firebase go to the Firebase Console and follow the step-by-step process to create your Firebase project.
After successfully creating your Firebase Android app. Navigate to the menu in the dashboard and select Storage
and then Get Started
as shown below:
Next, select to start in test mode and specify the location of your Firebase server and then click done.
After the process is complete, navigate to the Rules
tab and then change the rules, as shown below:
Note that this rule is not recommended when in production mode because it makes your data insecure.
Adding dependencies
Go to the pubspec.yaml
file and add the following dependencies:
dependencies:
encrypt: ^5.0.1
video_compress: ^3.1.0
firebase_storage: ^10.2.9
firebase_core: ^1.13.1
flutter_native_image: ^0.0.6+1
gallery_saver: ^2.3.2
file_picker: ^4.5.1
file_saver: ^0.1.0
firebase_storage
andfirebase_core
help us to access our Firebase storage where we will store our files.flutter_native_image
helps us to compress images that we pick from the local storage.video_compress
enables us to compress videos.gallery_saver
provides an easier way to save images or videos to the gallery.file_saver
provides us an easier way to save document files to the local storage.file_picker
enables us to pick from the local storage.encrypt
allows us to encrypt and decrypt files using the AES algorithm.
Creating APIs
We will create four APIs which we will use to build the functionality of our application.
These APIs are as follows:
1. File Picker API
In this API we create methods for the functions that the program will perform, i.e., picking images and videos from the local storage. These functions are as follows:
a. Picking an image
This function is shown below:
class FilePickerApi {
//Picking an image from local storage
static Future<File?> pickImage() async {
final pickedFile = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.image, // Only images will be picked in the file picker
);
if (pickedFile == null) {
return null;
} else {
final pickedImage = pickedFile.files.first;
return File(pickedImage.path!);
}
}
}
b. Picking a video
This will be done using the following code:
// Picking a video from the local storage
static Future<File?> pickVideo() async {
final pickedFile = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.video, //Only videos will be picked in the file picker
);
if (pickedFile == null) {
return null;
} else {
final pickedVideo = pickedFile.files.first;
return File(pickedVideo.path!);
}
}
This is the function for picking a video from the local storage.
c. Picking a document
It is as shown below:
// Picking a document from local storage
Future<File?> pickDocument() async {
final pickedFile = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.custom,
allowedExtensions: ["pdf"]); //Only PDF document files will be picked in the file picker
if (pickedFile == null) {
return null;
} else {
final pickedDocument = pickedFile.files.first;
return File(pickedDocument.path!);
}
}
The function above will enable us to pick documents from the gallery. We will only be able to select .pdf
document files from the local storage.
This is because we have set PDF files as the only allowed document format but more could be added.
2. File compression API
a. Compressing an image
It is as shown below:
class FileCompressionApi {
//Compressing the picked image
static Future<File?> compressImage(File file) async {
try {
final compressedFile = await FlutterNativeImage.compressImage(file.path,
quality: 100, percentage: 10);
return File(compressedFile.path);
} catch (e) {
return null; //If any error occurs during compression, the process is stopped.
}
}
}
From the function above, the picked image is passed as an argument. We have used the flutter-native-image
package to compress the image and return a new compressed file.
b. Compressing a video
It is as shown below:
//Compressing the picked video
static Future<MediaInfo?> compressVideo(File file) async {
try {
await VideoCompress.setLogLevel(0);
return VideoCompress.compressVideo(file.path,
quality: VideoQuality.LowQuality,
includeAudio: true,
deleteOrigin: true);
} catch (e) {
VideoCompress.cancelCompression(); //If any error occurs during compression, the process is stopped.
}
return null;
}
From the above function, we pass the picked video as an argument. We will then use the video_compress
package to compress the image.
The final output is a file with media information. We will use the media information to get the actual file which is our compressed video.
3. Firebase API
The function of the Firebase API is to enable the user to upload files to the Firebase storage.
We have created a class called firebase.api.dart
. Here is the code for the Firebase file uploading:
class FirebaseApi {
static UploadTask? uploadFile(String destination, File file) {
try {
final storageRef = FirebaseStorage.instance.ref(destination); //Here the destination of the file is passed.
return storageRef.putFile(file); // The file to be uploaded is passed.
} on FirebaseException catch (e) {
return null; // If any errors occur uploading is cancelled.
}
}
}
Here, we will pass the file we want to upload, and the path where will store the file in the Firebase storage bucket.
4. File Encryption API
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart';
class FileEcryptionApi {
static Future<Uint8List?> encryptFile(data) async {
final key = Key.fromSecureRandom(16);
final iv = IV.fromLength(16);
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
final encryptedFile = encrypter.encryptBytes(data, iv: iv);
return encryptedFile.bytes;
}
}
Here, we set the encryption algorithm to AES, and we have created a key variable. It contains an encryption key that is randomly generated containing 16 characters.
iv
represents an Initialization Vector
key which is used to initialize the encryption process.
Finally, we have passed the data which we want to encrypt which is the list of bytes that we want to encrypt.
User Interface
Home Screen
This page will be the main screen of our application. It is the root page that we will use to navigate to other pages in the application.
This is the code for our home screen:
import 'package:flutter/material.dart';
import 'package:flutter_app_demo/screens/document.screen.dart';
import 'package:flutter_app_demo/screens/image.screen.dart';
import 'package:flutter_app_demo/screens/video.screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter App Demo"),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const VideoScreen()));
},
icon: const Icon(Icons.video_file),
label: const Text("Select Video")),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ImageScreen()));
},
icon: const Icon(Icons.image_rounded),
label: const Text("Select Image")),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DocumentScreen()));
},
icon: const Icon(Icons.insert_drive_file_rounded),
label: const Text("Select Document"))
],
),
),
);
}
}
Image Screen
This is the page where we will be able to select an image, compress it, save it to the gallery or upload it to firebase.
This is the code for our Image screen:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_app_demo/services/file.compression.api.dart';
import 'package:flutter_app_demo/services/file.picker.api.dart';
import 'package:gallery_saver/gallery_saver.dart';
import '../services/firebase.api.dart';
class ImageScreen extends StatefulWidget {
const ImageScreen({Key? key}) : super(key: key);
@override
State<ImageScreen> createState() => _ImageScreenState();
}
class _ImageScreenState extends State<ImageScreen> {
File? image;
File? compressedImage;
bool isUploading = false;
bool isCompressing = false;
@override
Widget build(BuildContext context) {
final fileName =
image != null ? (image!.path.split('/').last) : "No Image Selected";
final fileSize = image != null
? (image!.lengthSync().roundToDouble() / 1048576).toStringAsFixed(2)
: "";
final compressedFileSize = compressedImage != null
? (compressedImage!.lengthSync().roundToDouble() / 1048576)
.toStringAsFixed(2)
: "";
return Scaffold(
appBar: AppBar(
title: const Text("Select Image"),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
fileName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
Text(
image != null ? "File Size: $fileSize MB" : "",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
Text(
compressedImage != null
? "Compressed File Size: $compressedFileSize MB"
: "",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 10,
),
if (image == null)
ElevatedButton.icon(
onPressed: () async {
final result = await FilePickerApi.pickImage();
if (result == null) {
return;
}
final filePath = result.path;
setState(() {
image = File(filePath);
});
},
icon: const Icon(Icons.image),
label: const Text("Pick Image"))
else
(compressedImage == null)
? ElevatedButton(
onPressed: () async {
setState(() {
isCompressing = true;
});
final result =
await FileCompressionApi.compressImage(
image!);
if (result == null) {
return;
}
final filePath = result.path;
setState(() {
compressedImage = File(filePath);
isCompressing = false;
});
},
child: isCompressing
? const Text("Compressing...")
: const Text("Compress Image"))
: Column(
children: [
ElevatedButton.icon(
onPressed: () async {
String url = compressedImage!.path;
GallerySaver.saveImage(url,
albumName: "Flutter App Demo")
.whenComplete(() =>
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text(
"Saved to Gallery!"))));
},
icon: const Icon(Icons.download_rounded),
label: const Text("Save to Gallery")),
const SizedBox(
height: 8,
),
ElevatedButton.icon(
onPressed: () async {
setState(() {
isUploading = true;
});
await uploadImage().whenComplete(() {
setState(() {
isUploading = false;
});
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text(
"Uploaded Successfully!")));
});
},
icon: const Icon(Icons.cloud_upload_rounded),
label: isUploading
? const Text("Uploading...")
: const Text("Upload to Firebase")),
],
),
],
)),
));
}
Future uploadImage() async {
if (compressedImage == null) return;
var filePath = compressedImage!.path;
var fileName = (filePath.split('/').last);
final destination = "files/images/$fileName";
await FirebaseApi.uploadFile(destination, compressedImage!);
}
}
We derive the name of the file that we pick and store it in the variable fileName
, as shown below:
final fileName = image != null ? (image!.path.split('/').last) : "No Image Selected";
We will get the size of the picked file and that of the compressed image file like this:
final fileSize = image != null
? (image!.lengthSync().roundToDouble() / 1048576).toStringAsFixed(2)
: "";
final compressedFileSize = compressedImage != null
? (compressedImage!.lengthSync().roundToDouble() / 1048576)
.toStringAsFixed(2)
: "";
Uploading an image to Firebase
We have created a function uploadImage()
which will enable us to upload the image to Firebase storage with the help of the Firebase API that we created, as demonstrated below:
Future uploadImage() async {
if (compressedImage == null) return;
var filePath = compressedImage!.path;
var fileName = (filePath.split('/').last);
final destination = "files/images/$fileName";
await FirebaseApi.uploadFile(destination, compressedImage!);
}
We first retrieve the name of the compressed image from its path. We then set the destination, which is a required argument by the Firebase API function, uploadFile()
.
The destination is the path in the Firebase storage bucket where the file will be stored. In our case, we will store images, videos, and documents in different folders.
Finally, we will call the function, uploadFile()
from the Firebase API and pass the required arguments, i.e., the destination of the file and the file itself as shown in the code above.
Saving an image to the gallery
We have created a function to save the compressed image to the local storage. We have used the gallery_saver
package to achieve that, as shown below:
String url = compressedImage!.path;
GallerySaver.saveImage(url, albumName: "Flutter App Demo")
A new album is created by the name provided and the image is saved there.
Video Screen
The video screen is almost similar to the image screen. We have only added a few extra functions that we will use for manipulating our video files such as video compressing.
This is the code for the video screen:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:video_compress/video_compress.dart';
import '../services/file.compression.api.dart';
import '../services/file.picker.api.dart';
import '../services/firebase.api.dart';
class VideoScreen extends StatefulWidget {
const VideoScreen({Key? key}) : super(key: key);
@override
State<VideoScreen> createState() => _VideoScreenState();
}
class _VideoScreenState extends State<VideoScreen> {
File? videoFile;
MediaInfo? compressedVideoInfo;
bool isUploading = false;
bool isCompressing = false;
@override
Widget build(BuildContext context) {
final fileName =
videoFile != null ? (videoFile!.path.split('/').last) : "No Video Selected";
final fileSize = videoFile != null
? (videoFile!.lengthSync().roundToDouble() / 1048576).toStringAsFixed(2)
: "";
return Scaffold(
appBar: AppBar(
title: const Text("Select Video"),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
fileName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
Text(
videoFile != null ? "File Size: $fileSize MB" : "",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 10,
),
if (videoFile == null)
ElevatedButton.icon(
onPressed: () async {
final result = await FilePickerApi.pickVideo();
if (result == null) {
return;
}
final filePath = result.path;
setState(() {
videoFile = File(filePath);
});
},
icon: const Icon(Icons.video_file),
label: const Text("Pick Video"))
else
(compressedVideoInfo == null)
? ElevatedButton(
onPressed: () async {
setState(() {
isCompressing = true;
});
final result =
await FileCompressionApi.compressVideo(
videoFile!);
if (result == null) {
return;
}
setState(() {
compressedVideoInfo = result;
videoFile = File(result.file!.path);
isCompressing = false;
});
},
child: isCompressing
? const Text("Compressing...")
: const Text("Compress Video"))
: Column(
children: [
ElevatedButton.icon(
onPressed: () async {
String url = compressedVideoInfo!.file!.path;
GallerySaver.saveVideo(url,
albumName: "Flutter App Demo")
.whenComplete(() =>
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text(
"Saved to Gallery!"))));
},
icon: const Icon(Icons.download_rounded),
label: const Text("Save to Gallery")),
const SizedBox(
height: 8,
),
ElevatedButton.icon(
onPressed: () async {
setState(() {
isUploading = true;
});
await uploadVideo().whenComplete(() {
setState(() {
isUploading = false;
});
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text(
"Uploaded Successfully!")));
});
},
icon: const Icon(Icons.cloud_upload_rounded),
label: isUploading
? const Text("Uploading...")
: const Text("Upload to Firebase")),
],
),
],
)),
));
}
Future uploadVideo() async {
if (compressedVideoInfo == null) return;
var filePath = compressedVideoInfo!.file!.path;
var fileName = (filePath.split('/').last);
final destination = "files/videos/$fileName";
await FirebaseApi.uploadFile(destination, compressedVideoInfo!.file!);
}
}
When we compress a video a MediaInfo
file containing the media information is returned. It contains the path to the file, the file size, title, author, duration e.t.c.
For our case, we will only use the file path. From here the picked video will be replaced with the new compressed video:
setState(() {
isCompressing = true;
});
final result =
await FileCompressionApi.compressVideo(videoFile!);
if (result == null) {
return;
}
setState(() {
compressedVideoInfo = result;
videoFile = File(result.file!.path);
isCompressing = false;
});
We store the media information which is returned when the compression is successful in the MediaInfo
variable that we created earlier:
MediaInfo? compressedVideoInfo;
From the media information, we derive the file path of the compressed video. The new video file will be the compressed video:
videoFile = File(result.file!.path);
Document Screen
We maintained a consistent user interface for all the screens. In the document screen, we will be able to pick a document from the phone storage, encrypt the document and then upload it to Firebase storage.
This is demonstrated below:
import 'dart:io';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_demo/services/file.encryption.api.dart';
import 'package:flutter_app_demo/services/file.picker.api.dart';
import 'package:flutter_app_demo/services/firebase.api.dart';
class DocumentScreen extends StatefulWidget {
const DocumentScreen({Key? key}) : super(key: key);
@override
State<DocumentScreen> createState() => _DocumentScreenState();
}
class _DocumentScreenState extends State<DocumentScreen> {
File? doc;
bool isUploading = false;
bool isEncrypting = false;
@override
Widget build(BuildContext context) {
final fileName =
doc != null ? (doc!.path.split('/').last) : "No Document Selected"; //Getting the name of the selected document
return Scaffold(
appBar: AppBar(
title: const Text("Select Document"),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
fileName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 10,
),
(doc == null)
? ElevatedButton.icon(
onPressed: () async {
final result = await FilePickerApi.pickDocument();
if (result == null) {
return;
}
final filePath = result.path;
setState(() {
doc = File(filePath);
});
},
icon: const Icon(Icons.attach_file),
label: const Text("Select Document"))
: Column(
children: [
ElevatedButton(
onPressed: () async {
setState(() {
isEncrypting = true;
});
final result =
await FileEcryptionApi.encryptFile(
doc!.readAsBytesSync()); //Changing the file into a list of bytes
await FileSaver.instance //Saving the encrypted document to local storage
.saveAs(fileName, result!, "aes",
MimeType.OTHER)
.whenComplete(() {
setState(() {
isEncrypting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Successfully encrypted!")));
});
},
child: isEncrypting
? const Text("Encrypting...")
: const Text("Encrypt Document")),
const SizedBox(
height: 10,
),
ElevatedButton.icon(
onPressed: () async {
setState(() {
isUploading = true;
});
await uploadDocument().whenComplete(() {
setState(() {
isUploading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Uploaded Successfully!")));
});
},
icon: const Icon(Icons.cloud_upload_rounded),
label: isUploading
? const Text("Uploading...")
: const Text("Upload to Firebase")),
],
),
],
)),
));
}
Future uploadDocument() async { // Function to upload the picked document to Firebase
if (doc == null) return;
var filePath = doc!.path;
var fileName = (filePath.split('/').last);
final destination = "files/documents/$fileName";
await FirebaseApi.uploadFile(destination, doc!);
}
}
Document File encryption
When encrypting the file, we first get the file we want to encrypt. For our case, we will use the document that we pick from the local storage.
The file is read and changed into a list of bytes which is the required format to be able to encrypt the file.
Once we have successfully changed the file into a list of bytes, the file is encrypted using the AES algorithm. This is done by calling the File Encryption API that we created earlier.
final result = await FileEcryptionApi.encryptFile(doc!.readAsBytesSync()); //Changing the file into a list of bytes
Once the encryption is complete, the encrypted file is saved to the local storage by passing the list of bytes, the file name, and the file extension as shown below:
await FileSaver.instance.saveAs(fileName, result!, "aes", MimeType.OTHER); //Saving the encrypted file to the local storage
main.dart
The main.dart
is the root of the application from where all other widgets form the widget tree.
For our main function we will initialize our Firebase app as follows:
import 'package:firebase_core/firebase_core.dart'; // Make sure this package is imported.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
Next, we will set the home page to be the home screen that we created earlier:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App Demo',
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: const HomeScreen(), // Here we set the first page that will appear when we start the application
);
}
}
Conclusion
In this article, we have gone through how to pick different types of files from the local storage and manipulate them.
We were able to pick videos and images from the gallery and compress them to achieve a smaller image and video size. This is a way of optimizing data usage when uploading such files to cloud storage.
We have also gone through the process of encrypting documents and uploading videos, images, and document files to Firebase storage. I do hope this article is useful.
Happy coding!
Peer Review Contributions by: Wanja Mike