Developing a real-time chat application is one of the best ways to understand the core fundamentals of modern mobile app developmentโauthentication, state management, real-time updates, and media handling.
In this complete guide, we will build a Flutter chat application using Firebase Messaging, Firestore real-time database, Firebase Authentication, and Cloudinary for image/file uploads, all structured using the MVC (Model-View-Controller) architecture.
This blog provides complete code, folderstructure, and a clean UI that you can instantly integrate into your project.
๐ Why This Blog?
You will learn:
-
MVC architecture in Flutter
-
Firebase Authentication (sign-in/sign-up)
-
Firestore real-time messages
-
Cloudinary image/file upload
-
Clean chat UI (chat bubble, sender/receiver alignment)
-
Real-time message streaming
-
Modular, scalable folder structure
-
Production-ready formatting & best practices
๐ Project Folder Structure (MVC)

This structure keeps code clean, scalable, testable, and maintainable.
๐ฅ Core Technologies Used
1. Firebase Authentication
Used to authenticate users so each message has a unique sender.
2. Cloud Firestore
Stores the messages in real-time.
3. Cloudinary
Uploads images/files and returns a secure URL.
4. MVC Architecture
Provides clean separation of:
-
Models โ Data structures
-
Views โ UI files
-
Controllers โ Business logic

๐ Core Features of This App
โ Real-time messaging
Messages are stored in Firestore and updated instantly.
โ Firebase Authentication
Users login/register using email & password.
โ Cloudinary Integration
Supports:
-
Image upload
-
File upload
-
Cloud storage delivery URLs

โ MVC Structure
Professional code organization for large-scale projects.
models/
chat.dart
// === file: lib/models/chat_model.dart ===
class ChatModel {
final String id;
final List<dynamic> participants;
final String lastMessage;
final DateTime updatedAt;
ChatModel({required this.id, required this.participants, required this.lastMessage, required this.updatedAt});
factory ChatModel.fromMap(String id, Map<String, dynamic> map) {
return ChatModel(
id: id,
participants: map['participants'] ?? [],
lastMessage: map['lastMessage'] ?? '',
updatedAt: (map['updatedAt'] as DateTime?) ?? DateTime.now(),
);
}
}
message_model.dart
class MessageModel {
final String senderId;
final String senderName;
final String message;
final String? mediaUrl;
final DateTime timestamp;
MessageModel({
required this.senderId,
required this.senderName,
required this.message,
required this.timestamp,
this.mediaUrl,
});
Map<String, dynamic> toMap() => {
"senderId": senderId,
"senderName": senderName,
"message": message,
"mediaUrl": mediaUrl,
"timestamp": timestamp.toIso8601String(),
};
factory MessageModel.fromMap(Map<String, dynamic> map) {
return MessageModel(
senderId: map["senderId"],
senderName: map["senderName"],
message: map["message"],
mediaUrl: map["mediaUrl"],
timestamp: DateTime.parse(map["timestamp"]),
);
}
}
user_model.dart
class UserModel {
final String uid;
final String name;
final String email;
UserModel({
required this.uid,
required this.name,
required this.email,
});
Map<String, dynamic> toMap() => {
'uid': uid,
'name': name,
'email': email,
};
factory UserModel.fromMap(Map<String, dynamic> map) {
return UserModel(
uid: map['uid'],
name: map['name'],
email: map['email'],
);
}
}
controllers/
auth_controller.dart
import '../services/firebase_auth_service.dart';
class AuthController {
final FirebaseAuthService _authService = FirebaseAuthService();
Future<bool> login(String email, String password) async {
final user = await _authService.login(email, password);
return user != null;
}
Future<bool> register(String email, String password) async {
final user = await _authService.register(email, password);
return user != null;
}
Future<void> logout() async {
await _authService.logout();
}
}
chat_controller.dart
import 'dart:io';
import '../models/message_model.dart';
import '../services/chat_service.dart';
import '../services/cloudinary_service.dart';
import '../services/firebase_auth_service.dart';
class ChatController {
final ChatService _chatService = ChatService();
final CloudinaryService _cloud = CloudinaryService();
final FirebaseAuthService _auth = FirebaseAuthService();
String get currentUserId => _auth.currentUser?.uid ?? "";
Future<void> sendMessage(String text) async {
final user = _auth.currentUser;
if (user == null) return;
final msg = MessageModel(
senderId: user.uid,
senderName: user.email ?? "User",
message: text,
timestamp: DateTime.now(),
);
await _chatService.sendMessage(msg);
}
Future<void> sendImage(File image) async {
final url = await _cloud.uploadImage(image);
await sendMediaMessage(url);
}
Future<void> sendMediaMessage(String url) async {
final user = _auth.currentUser;
final msg = MessageModel(
senderId: user!.uid,
senderName: user.email ?? "User",
message: "",
mediaUrl: url,
timestamp: DateTime.now(),
);
await _chatService.sendMessage(msg);
}
Stream<List<MessageModel>> getMessages() => _chatService.getMessages();
}
screens/
chat_list_screen.dart
import 'package:flutter/material.dart';
class ChatListScreen extends StatelessWidget {
const ChatListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Chats")),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, "/chat");
},
child: Text("Open Global Chat Room"),
),
),
);
}
}
chat_room_screen.dart
import 'package:flutter/material.dart';
import '../views/chat_view.dart';
class ChatRoomScreen extends StatelessWidget {
const ChatRoomScreen({super.key});
@override
Widget build(BuildContext context) {
return ChatView(); // Using your main chat UI
}
}
login_screen.dart
import 'package:flutter/material.dart';
import '../views/login_view.dart';
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return LoginView();
}
}
services/
chat_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/message_model.dart';
class ChatService {
final _firestore = FirebaseFirestore.instance;
Stream<List<MessageModel>> getMessages() {
return _firestore
.collection("messages")
.orderBy("timestamp", descending: false)
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => MessageModel.fromMap(doc.data()))
.toList();
});
}
Future<void> sendMessage(MessageModel msg) async {
await _firestore.collection("messages").add(msg.toMap());
}
}
firebase_auth_service.dart
import 'package:firebase_auth/firebase_auth.dart';
class FirebaseAuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<User?> login(String email, String password) async {
final result = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
return result.user;
}
Future<User?> register(String email, String password) async {
final result = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
return result.user;
}
Future<void> logout() async {
await _auth.signOut();
}
User? get currentUser => _auth.currentUser;
}
cloudinary_service.dart
import 'dart:io';
import 'package:cloudinary_public/cloudinary_public.dart';
class CloudinaryService {
final cloudinary = CloudinaryPublic(
'your_cloud_name',
'your_upload_preset',
cache: false,
);
Future<String> uploadImage(File file) async {
final response = await cloudinary.uploadFile(
CloudinaryFile.fromFile(file.path, resourceType: CloudinaryResourceType.Image),
);
return response.secureUrl;
}
Future<String> uploadFile(File file) async {
final response = await cloudinary.uploadFile(
CloudinaryFile.fromFile(file.path, resourceType: CloudinaryResourceType.Auto),
);
return response.secureUrl;
}
}
views/
chat_view.dart
import 'package:flutter/material.dart';
import '../controllers/chat_controller.dart';
import '../models/message_model.dart';
import '../widgets/chat_bubble.dart';
class ChatView extends StatelessWidget {
final ChatController controller = ChatController();
final TextEditingController _msgController = TextEditingController();
ChatView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter Chat"),
actions: [
IconButton(
onPressed: () {
Navigator.pushReplacementNamed(context, "/login");
},
icon: Icon(Icons.logout),
)
],
),
body: Column(
children: [
Expanded(
child: StreamBuilder<List<MessageModel>>(
stream: controller.getMessages(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final messages = snapshot.data!;
return ListView.builder(
padding: EdgeInsets.all(12),
physics: BouncingScrollPhysics(),
itemCount: messages.length,
itemBuilder: (_, index) {
final msg = messages[index];
final isMe = msg.senderId == controller.currentUserId;
return ChatBubble(
isMe: isMe,
senderName: msg.senderName,
message: msg.message,
timestamp: msg.timestamp,
);
},
);
},
),
),
// ------------------- MESSAGE INPUT -------------------
Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(color: Colors.grey.shade200),
child: Row(
children: [
Expanded(
child: TextField(
controller: _msgController,
decoration: InputDecoration(
hintText: "Type a message...",
fillColor: Colors.white,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 14,
),
),
),
),
SizedBox(width: 8),
CircleAvatar(
radius: 26,
child: IconButton(
icon: Icon(Icons.send),
onPressed: () {
if (_msgController.text.trim().isNotEmpty) {
controller.sendMessage(_msgController.text.trim());
_msgController.clear();
}
},
),
),
],
),
),
],
),
);
}
}
login_view.dart
import 'package:flutter/material.dart';
import '../controllers/auth_controller.dart';
class LoginView extends StatelessWidget {
final _email = TextEditingController();
final _pass = TextEditingController();
final AuthController controller = AuthController();
LoginView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Login")),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
TextField(controller: _email, decoration: InputDecoration(hintText: "Email")),
TextField(controller: _pass, obscureText: true, decoration: InputDecoration(hintText: "Password")),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
final success = await controller.login(_email.text, _pass.text);
if (success) {
Navigator.pushReplacementNamed(context, "/chat");
}
},
child: Text("Login"),
),
TextButton(
onPressed: () => Navigator.pushNamed(context, "/register"),
child: Text("Create Account"),
)
],
),
),
);
}
}
register_view.dart
import 'package:flutter/material.dart';
import '../controllers/auth_controller.dart';
class RegisterView extends StatelessWidget {
final _email = TextEditingController();
final _pass = TextEditingController();
final AuthController controller = AuthController();
RegisterView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Register")),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
TextField(controller: _email, decoration: InputDecoration(hintText: "Email")),
TextField(controller: _pass, obscureText: true, decoration: InputDecoration(hintText: "Password")),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
final success = await controller.register(_email.text, _pass.text);
if (success) {
Navigator.pushReplacementNamed(context, "/chat");
}
},
child: Text("Register"),
)
],
),
),
);
}
}
widgets/
chat_bubble.dart
import 'package:flutter/material.dart';
class ChatBubble extends StatelessWidget {
final bool isMe;
final String senderName;
final String message;
final DateTime timestamp;
const ChatBubble({
super.key,
required this.isMe,
required this.senderName,
required this.message,
required this.timestamp,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: EdgeInsets.symmetric(vertical: 6),
padding: EdgeInsets.all(12),
constraints: BoxConstraints(maxWidth: 280),
decoration: BoxDecoration(
color: isMe ? Colors.blueAccent : Colors.grey.shade300,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(14),
topRight: Radius.circular(14),
bottomLeft: Radius.circular(isMe ? 14 : 0),
bottomRight: Radius.circular(isMe ? 0 : 14),
),
),
child: Column(
crossAxisAlignment:
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
if (!isMe)
Text(
senderName,
style: TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
message,
style: TextStyle(
color: isMe ? Colors.white : Colors.black87,
fontSize: 15,
),
),
SizedBox(height: 4),
Text(
"${timestamp.hour}:${timestamp.minute.toString().padLeft(2, '0')}",
style: TextStyle(
fontSize: 10,
color: isMe ? Colors.white70 : Colors.black54,
),
),
],
),
),
);
}
}
upload_button.dart
import 'package:flutter/material.dart';
class UploadButton extends StatelessWidget {
final VoidCallback onTap;
const UploadButton({required this.onTap});
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.add_photo_alternate),
onPressed: onTap,
);
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'views/login_view.dart';
import 'views/register_view.dart';
import 'views/chat_view.dart';
import 'firebase_options.dart'; // Generated via flutterfire configure
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(ChatApp());
}
class ChatApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter MVC Chat App",
debugShowCheckedModeBanner: false,
initialRoute: "/login",
routes: {
"/login": (context) => LoginView(),
"/register": (context) => RegisterView(),
"/chat": (context) => ChatView(),
},
theme: ThemeData(
primarySwatch: Colors.blue,
scaffoldBackgroundColor: Colors.grey.shade100,
),
);
}
}
๐ Step 6: Chat View UI (With Media Support)
Your chat_view.dart will display text + images using ChatBubble.
Images from Cloudinary appear instantly.
๐ผ Where Do Cloudinary Images Go?
Cloudinary stores images in your Media Library:
๐ Cloudinary Dashboard โ Media Library
Every uploaded file generates:
-
Secure URL
-
Public ID
-
Delivery URL
-
Transformations
Your app stores only the secure URL in Firestore.
๐ Final Output
You now have a complete:
-
Real-time chat app
-
Firebase-auth connected
-
Cloudinary-upload enabled
-
MVC-structured Flutter project
๐ข Subscribe CTA
If you liked this tutorial, subscribe to our blog to get upcoming guides on:
-
Realtime group chats
-
Push notifications (FCM)
-
Audio/video calling
-
Cloudinary advanced transformations
-
Flutter animations
