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

Author

Write A Comment