Uploading images, documents, and media files is one of the most essential features in modern mobile applications. Whether youβre building a social media platform, an e-commerce app, or a document storage system, you need a fast, reliable, and secure way to upload files.
This tutorial walks you through building a complete Flutter app that uploads images, files, and documents to Cloudinary using the MVC (ModelβViewβController) architecture.
We will also implement a beautiful UI, real-time upload progress, and a grid view gallery of uploaded files.
π Why Cloudinary for Flutter?
Cloudinary is a cloud-based image and video management service that offers:
-
β‘ Fast file uploads
-
π‘ Secure storage
-
π Automatic file optimization
-
πΌ Smart transformations
-
π Global CDN delivery
-
π Support for images, videos, documents, PDFs, and more
It is perfect for Flutter apps because Cloudinary handles heavy lifting like compression, storage, and CDN caching β allowing your app to stay fast and lightweight.
π§± Why Use MVC Architecture in Flutter?
MVC (Model-View-Controller) divides your app into three clean layers:
β Model
Handles data only (structure, types, properties).
β View
UI screens, widgets, layout β no business logic.
β Controller
Business logic, state, interactions, upload process.
This makes your app:
-
Clean
-
Testable
-
Maintainable
-
Scalable
-
Developer-friendly
β¨ What We Are Building Today
A complete Flutter app that allows users to:
β Upload image from Gallery
β Capture photo from Camera
β Upload any file (PDF, DOCX, ZIP, etc.)
β View real-time upload progress
β Display uploads on screen
β Beautiful & simple UI
β Cloudinary-backed media storage
π App Preview (UI Flow)
π§© Project Folder Structure (MVC)
π Step-by-Step Implementation (Full Code)
Below is the complete code used in this app.
Each part is clearly explained.
Recommended package choices (used in example)
-
cloudinary_publicβ easy client uploads to Cloudinary (unsigned preset support, progress callbacks, chunking). -
image_pickerβ pick images from gallery/camera. -
file_pickerβ pick other file types. -
provider(optional) orsetStatefor state management. -
cached_network_image(optional) for efficient image display.
Why cloudinary_public? It gives CloudinaryPublic('CLOUD_NAME', 'UPLOAD_PRESET') and an uploadFile() method with a progress callback. It supports multi-upload, chunked upload, and transformations client-side.

1οΈβ£ Model β upload_item.dart
The Model defines the structure of a single uploaded item.
// lib/models/upload_item.dart
class UploadItem {
final String url;
final String publicId;
final DateTime uploadedAt;
UploadItem({
required this.url,
required this.publicId,
required this.uploadedAt,
});
}
2οΈβ£ Service β cloudinary_service.dart
This layer handles Cloudinary API communication.
// lib/services/cloudinary_service.dart
import 'package:cloudinary_public/cloudinary_public.dart';
class CloudinaryService {
final CloudinaryPublic cloudinary = CloudinaryPublic(
'YOUR_CLOUD_NAME',
'YOUR_UPLOAD_PRESET',
cache: false,
);
Future<CloudinaryResponse> uploadFile(
String filePath,
CloudinaryResourceType type,
Function(double progress)? onProgress,
) async {
return await cloudinary.uploadFile(
CloudinaryFile.fromFile(filePath, resourceType: type),
onProgress: (count, total) {
if (onProgress != null && total != 0) {
onProgress(count / total);
}
},
);
}
}
3οΈβ£ Controller β upload_controller.dart
The Controller connects user actions with Cloudinary.
It manages:
β Image Picker
β File Picker
β Upload logic
β Progress updates
β Uploaded items list
// lib/controllers/upload_controller.dart
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
import '../models/upload_item.dart';
import '../services/cloudinary_service.dart';
import 'package:cloudinary_public/cloudinary_public.dart';
import 'package:flutter/material.dart';
class UploadController with ChangeNotifier {
final CloudinaryService cloudinaryService = CloudinaryService();
final ImagePicker picker = ImagePicker();
bool isUploading = false;
double progress = 0.0;
List<UploadItem> uploads = [];
// ---------------------- PICK IMAGE ----------------------
Future<void> pickImage(ImageSource source) async {
final XFile? picked = await picker.pickImage(
source: source,
imageQuality: 85,
maxWidth: 2000,
);
if (picked == null) return;
await upload(picked.path, CloudinaryResourceType.Image);
}
// ---------------------- PICK FILE ----------------------
Future<void> pickFile() async {
final result = await FilePicker.platform.pickFiles();
if (result == null || result.files.isEmpty) return;
await upload(result.files.first.path!, CloudinaryResourceType.Auto);
}
// ---------------------- UPLOAD FILE ----------------------
Future<void> upload(String path, CloudinaryResourceType type) async {
try {
isUploading = true;
progress = 0;
notifyListeners();
final response = await cloudinaryService.uploadFile(
path,
type,
(p) {
progress = p;
notifyListeners();
},
);
uploads.insert(
0,
UploadItem(
url: response.secureUrl,
publicId: response.publicId,
uploadedAt: DateTime.now(),
),
);
} catch (e) {
debugPrint("Upload error: $e");
} finally {
isUploading = false;
notifyListeners();
}
}
}
4οΈβ£ Views & Widgets (UI Layer)
This is the View part of MVC.
π Home Screen β home_view.dart
// lib/views/home_view.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../controllers/upload_controller.dart';
import 'widgets/upload_buttons.dart';
import 'widgets/upload_progress_bar.dart';
import 'widgets/uploaded_grid.dart';
class HomeView extends StatelessWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context) {
final controller = Provider.of<UploadController>(context);
return Scaffold(
appBar: AppBar(
title: const Text("Cloudinary MVC Upload"),
backgroundColor: Colors.indigo,
),
body: Column(
children: [
const SizedBox(height: 12),
/// Upload Buttons
UploadButtons(),
/// Progress Bar
if (controller.isUploading)
UploadProgressBar(progress: controller.progress),
const Divider(),
/// Gallery
const Expanded(child: UploadedGrid()),
],
),
);
}
}
π Upload Buttons Widget
// lib/views/widgets/upload_buttons.dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import '../../controllers/upload_controller.dart';
class UploadButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Provider.of<UploadController>(context, listen: false);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton.icon(
onPressed: () => controller.pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: const Text("Gallery"),
),
ElevatedButton.icon(
onPressed: () => controller.pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: const Text("Camera"),
),
ElevatedButton.icon(
onPressed: controller.pickFile,
icon: const Icon(Icons.attach_file),
label: const Text("File"),
),
],
);
}
}
π Upload Progress Indicator Widget
// lib/views/widgets/upload_progress_bar.dart
import 'package:flutter/material.dart';
class UploadProgressBar extends StatelessWidget {
final double progress;
const UploadProgressBar({required this.progress, super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
LinearProgressIndicator(value: progress),
const SizedBox(height: 6),
Text("${(progress * 100).toStringAsFixed(0)}%"),
],
);
}
}
πΌ Uploaded Grid Widget
// lib/views/widgets/uploaded_grid.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:provider/provider.dart';
import '../../controllers/upload_controller.dart';
class UploadedGrid extends StatelessWidget {
const UploadedGrid({super.key});
@override
Widget build(BuildContext context) {
final controller = Provider.of<UploadController>(context);
if (controller.uploads.isEmpty) {
return const Center(child: Text("No uploads yet."));
}
return GridView.builder(
padding: const EdgeInsets.all(8),
itemCount: controller.uploads.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, i) {
final item = controller.uploads[i];
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: item.url,
placeholder: (c, s) => Container(
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (c, s, e) => const Icon(Icons.error),
fit: BoxFit.cover,
),
);
},
);
}
}
5οΈβ£ Main File β main.dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'controllers/upload_controller.dart';
import 'views/home_view.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UploadController()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cloudinary MVC Flutter',
debugShowCheckedModeBanner: false,
home: const HomeView(),
);
}
}
π₯ Important Tips for Deployment
β Use your real Cloudinary cloud_name
β Use an unsigned upload_preset
β Enable CORS (if using APIs)
β Use caching for faster previews
β Serve images via Cloudinary CDN
π― Final Thoughts
In this complete blog, you learned how to build a full Flutter upload solution powered by Cloudinary, using a clean MVC architecture.
You now have:
β File & Image Uploads
β Real-time Progress
β Beautiful UI
β Organized Code Structure
β Fully scalable architecture


