Build a Flutter Chat Application
A Flutter Chat Application is a practical project that demonstrates how to build a real-time messaging application using Flutter and Dart. A chat application can include user registration, login, user profiles, one-to-one messaging, chat lists, message timestamps, online/offline status, typing indicators, message notifications, image sharing, and message history.
This project helps learners understand how Flutter widgets, navigation, asynchronous programming, state management, backend services, authentication, databases, and real-time data can work together to create a complete application.
1. Objectives of the Chat Application
- Create a complete chat application using Flutter.
- Create user registration and login screens.
- Display a list of users or conversations.
- Create one-to-one chat functionality.
- Send and receive text messages.
- Display message timestamps.
- Show sent and received messages differently.
- Manage chat state.
- Store messages in a backend database.
- Implement real-time message updates.
- Show online and offline status.
- Display typing indicators.
- Implement message notifications.
- Support image or file sharing.
- Implement message deletion where required.
- Display chat history.
- Handle loading, empty, and error states.
- Create a responsive chat interface.
2. Technologies Used
| Technology | Purpose |
|---|
| Flutter | Build the cross-platform user interface. |
| Dart | Programming language used for Flutter development. |
| Firebase Authentication | Handle user registration and authentication. |
| Cloud Firestore | Store and synchronize chat messages and user data. |
| Firebase Storage | Store images and other uploaded files. |
| Firebase Cloud Messaging | Send push notifications. |
| Provider | One possible approach for managing shared application state. |
3. Main Features
- Splash Screen
- Onboarding Screen
- User Registration
- User Login
- User Profile
- Chat List
- One-to-One Chat
- Real-Time Messages
- Message Timestamps
- Online Status
- Typing Indicator
- Unread Message Count
- Message Notifications
- Image Sharing
- File Sharing
- Message Deletion
- Chat Search
- User Search
- Block or Report User
- Logout
4. Chat Application Flow
Launch App
↓
Splash Screen
↓
Check Authentication
↓
Login / Register
↓
Home / Chat List
↓
Select User
↓
Chat Screen
↓
Type Message
↓
Send Message
↓
Store Message
↓
Real-Time Message Update
↓
Notification
↓
Chat History
5. Recommended Project Structure
lib/
├── main.dart
├── models/
│ ├── user_model.dart
│ ├── message_model.dart
│ └── chat_model.dart
├── screens/
│ ├── splash_screen.dart
│ ├── login_screen.dart
│ ├── register_screen.dart
│ ├── home_screen.dart
│ ├── chat_list_screen.dart
│ ├── chat_screen.dart
│ └── profile_screen.dart
├── services/
│ ├── auth_service.dart
│ ├── chat_service.dart
│ ├── notification_service.dart
│ └── storage_service.dart
├── providers/
│ ├── auth_provider.dart
│ └── chat_provider.dart
├── widgets/
│ ├── chat_tile.dart
│ ├── message_bubble.dart
│ ├── user_avatar.dart
│ └── typing_indicator.dart
└── utils/
└── constants.dart
Separating models, screens, services, providers, and reusable widgets keeps the application organized and easier to maintain.
6. Create the Flutter Project
Create a new Flutter project using the terminal:
flutter create chat_application
cd chat_application
flutter run
7. Add Required Packages
For a Firebase-based chat application, commonly required packages can include:
flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add cloud_firestore
flutter pub add firebase_storage
flutter pub add firebase_messaging
Additional packages can be added for features such as image selection, local notifications, or state management.
8. Configure Firebase
Firebase can be used as the backend for authentication, real-time database functionality, file storage, and notifications.
The general setup flow is:
- Create a Firebase project.
- Register the Android and/or iOS application.
- Configure Firebase for the Flutter project.
- Add the required Firebase packages.
- Initialize Firebase in the Flutter application.
- Enable Authentication.
- Create a Firestore database.
- Configure Firebase Storage if media sharing is required.
- Configure Firebase Cloud Messaging if push notifications are required.
9. Initialize Firebase
A typical Flutter application initializes Firebase before starting the application.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const ChatApplication());
}
10. Create the Main Application
class ChatApplication extends StatelessWidget {
const ChatApplication({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Chat',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
),
useMaterial3: true,
),
home: const LoginScreen(),
);
}
}
11. User Model
The user model represents information about a registered chat user.
class UserModel {
final String id;
final String name;
final String email;
final String? photoUrl;
final bool isOnline;
UserModel({
required this.id,
required this.name,
required this.email,
this.photoUrl,
this.isOnline = false,
});
factory UserModel.fromMap(
Map data,
String id,
) {
return UserModel(
id: id,
name: data['name'] ?? '',
email: data['email'] ?? '',
photoUrl: data['photoUrl'],
isOnline: data['isOnline'] ?? false,
);
}
Map toMap() {
return {
'name': name,
'email': email,
'photoUrl': photoUrl,
'isOnline': isOnline,
};
}
}
12. Message Model
Each chat message can contain a sender ID, receiver ID, message text, timestamp, and message type.
class MessageModel {
final String id;
final String senderId;
final String receiverId;
final String text;
final DateTime timestamp;
final String type;
MessageModel({
required this.id,
required this.senderId,
required this.receiverId,
required this.text,
required this.timestamp,
this.type = 'text',
});
factory MessageModel.fromMap(
Map data,
String id,
) {
return MessageModel(
id: id,
senderId: data['senderId'] ?? '',
receiverId: data['receiverId'] ?? '',
text: data['text'] ?? '',
timestamp: data['timestamp'].toDate(),
type: data['type'] ?? 'text',
);
}
Map toMap() {
return {
'senderId': senderId,
'receiverId': receiverId,
'text': text,
'timestamp': timestamp,
'type': type,
};
}
}
13. Chat Model
A chat model can represent a conversation between two users.
class ChatModel {
final String id;
final String otherUserId;
final String lastMessage;
final DateTime? lastMessageTime;
final int unreadCount;
ChatModel({
required this.id,
required this.otherUserId,
required this.lastMessage,
this.lastMessageTime,
this.unreadCount = 0,
});
}
14. Firebase Authentication
Firebase Authentication can be used to register and authenticate users.
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth =
FirebaseAuth.instance;
Future register(
String email,
String password,
) async {
return await _auth
.createUserWithEmailAndPassword(
email: email,
password: password,
);
}
Future login(
String email,
String password,
) async {
return await _auth
.signInWithEmailAndPassword(
email: email,
password: password,
);
}
Future logout() async {
await _auth.signOut();
}
}
15. Registration Screen
The registration screen allows a new user to create an account.
final emailController = TextEditingController();
final passwordController = TextEditingController();
final nameController = TextEditingController();
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
prefixIcon: Icon(Icons.person),
),
)
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
),
)
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
),
)
16. Login Screen
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() =>
_LoginScreenState();
}
class _LoginScreenState
extends State {
final emailController = TextEditingController();
final passwordController = TextEditingController();
bool isLoading = false;
Future login() async {
setState(() {
isLoading = true;
});
try {
await AuthService().login(
emailController.text.trim(),
passwordController.text.trim(),
);
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const HomeScreen(),
),
);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Login failed: $error',
),
),
);
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
),
),
const SizedBox(height: 12),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading ? null : login,
child: isLoading
? const CircularProgressIndicator()
: const Text('Login'),
),
),
],
),
),
);
}
}
17. Form Validation
Form validation prevents invalid or incomplete information from being submitted.
final formKey = GlobalKey();
Form(
key: formKey,
child: TextFormField(
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email';
}
return null;
},
),
)
18. Firestore Database Structure
A simple chat application can organize data using users, chats, and messages.
users/
userId/
name
email
photoUrl
isOnline
lastSeen
chats/
chatId/
participants
lastMessage
lastMessageTime
chats/
chatId/
messages/
messageId/
senderId
receiverId
text
timestamp
type
19. Users Collection
The users collection can store profile and presence information.
FirebaseFirestore.instance
.collection('users')
.doc(userId)
.set({
'name': name,
'email': email,
'photoUrl': photoUrl,
'isOnline': true,
'lastSeen': FieldValue.serverTimestamp(),
});
20. Send a Message
A message can be stored inside a chat's messages subcollection.
Future sendMessage({
required String chatId,
required String senderId,
required String receiverId,
required String text,
}) async {
if (text.trim().isEmpty) {
return;
}
await FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.collection('messages')
.add({
'senderId': senderId,
'receiverId': receiverId,
'text': text.trim(),
'timestamp': FieldValue.serverTimestamp(),
'type': 'text',
});
}
21. Chat Service
Moving database operations into a service class keeps UI widgets cleaner.
class ChatService {
final FirebaseFirestore _firestore =
FirebaseFirestore.instance;
Future sendMessage({
required String chatId,
required String senderId,
required String receiverId,
required String text,
}) async {
await _firestore
.collection('chats')
.doc(chatId)
.collection('messages')
.add({
'senderId': senderId,
'receiverId': receiverId,
'text': text,
'timestamp':
FieldValue.serverTimestamp(),
'type': 'text',
});
}
}
22. Listen to Messages in Real Time
A chat application needs to update the interface when a new message arrives. Firestore provides real-time listeners that can be consumed using a stream.
Stream getMessages(
String chatId,
) {
return FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timestamp')
.snapshots();
}
23. Display Messages Using StreamBuilder
StreamBuilder can rebuild the UI when the message stream changes.
StreamBuilder(
stream: ChatService().getMessages(chatId),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return const Center(
child: Text(
'Unable to load messages',
),
);
}
final messages =
snapshot.data?.docs ?? [];
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final data =
messages[index].data()
as Map;
return MessageBubble(
text: data['text'] ?? '',
isMe:
data['senderId'] == currentUserId,
);
},
);
},
)
24. Message Bubble
Sent and received messages can use different alignment and styling.
class MessageBubble extends StatelessWidget {
final String text;
final bool isMe;
const MessageBubble({
super.key,
required this.text,
required this.isMe,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: isMe
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
decoration: BoxDecoration(
color: isMe
? Colors.blue
: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16),
),
child: Text(
text,
style: TextStyle(
color: isMe
? Colors.white
: Colors.black87,
),
),
),
);
}
}
25. Chat Screen
class ChatScreen extends StatefulWidget {
final String chatId;
final String receiverId;
final String receiverName;
const ChatScreen({
super.key,
required this.chatId,
required this.receiverId,
required this.receiverName,
});
@override
State createState() =>
_ChatScreenState();
}
class _ChatScreenState
extends State {
final messageController =
TextEditingController();
void sendMessage() {
final text =
messageController.text.trim();
if (text.isEmpty) {
return;
}
ChatService().sendMessage(
chatId: widget.chatId,
senderId: currentUserId,
receiverId: widget.receiverId,
text: text,
);
messageController.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.receiverName),
),
body: Column(
children: [
Expanded(
child: StreamBuilder(
stream: ChatService()
.getMessages(widget.chatId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child:
CircularProgressIndicator(),
);
}
final messages =
snapshot.data!.docs;
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final data =
messages[index].data()
as Map;
return MessageBubble(
text: data['text'] ?? '',
isMe:
data['senderId'] ==
currentUserId,
);
},
);
},
),
),
MessageInput(
controller: messageController,
onSend: sendMessage,
),
],
),
);
}
}
26. Message Input
class MessageInput extends StatelessWidget {
final TextEditingController controller;
final VoidCallback onSend;
const MessageInput({
super.key,
required this.controller,
required this.onSend,
});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
textInputAction:
TextInputAction.send,
onSubmitted: (_) => onSend(),
decoration: InputDecoration(
hintText: 'Type a message...',
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(24),
),
),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: onSend,
icon: const Icon(
Icons.send,
),
),
],
),
),
);
}
}
27. Chat List Screen
The chat list displays the user's existing conversations.
class ChatListScreen extends StatelessWidget {
const ChatListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chats'),
),
body: ListView.builder(
itemCount: chats.length,
itemBuilder: (context, index) {
final chat = chats[index];
return ListTile(
leading: CircleAvatar(
child: Text(
chat.name[0].toUpperCase(),
),
),
title: Text(chat.name),
subtitle: Text(
chat.lastMessage,
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
trailing: Text(
chat.time,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ChatScreen(
chatId: chat.id,
receiverId:
chat.userId,
receiverName:
chat.name,
),
),
);
},
);
},
),
);
}
}
28. Display Last Message
The chat document can store the latest message so that the chat list can display a conversation preview without loading the entire message history.
await FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.update({
'lastMessage': text,
'lastMessageTime':
FieldValue.serverTimestamp(),
});
29. Create a Chat ID
For a one-to-one chat, both users need to reference the same conversation. A deterministic chat ID can be created from the two user IDs.
String createChatId(
String userId1,
String userId2,
) {
final ids = [
userId1,
userId2,
]..sort();
return ids.join('_');
}
This approach ensures that the same pair of users produces the same chat ID regardless of who starts the conversation.
30. Online and Offline Status
A chat application can store presence information such as whether a user is online and when the user was last active.
Future updateOnlineStatus(
String userId,
bool isOnline,
) async {
await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.update({
'isOnline': isOnline,
'lastSeen':
FieldValue.serverTimestamp(),
});
}
For production presence systems, the backend architecture should be designed carefully so that disconnects and network changes are handled reliably.
31. Display Online Status
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: user.isOnline
? Colors.green
: Colors.grey,
),
),
const SizedBox(width: 6),
Text(
user.isOnline
? 'Online'
: 'Offline',
),
],
)
32. Last Seen
When a user is offline, the application can display the last active time.
Text(
user.isOnline
? 'Online'
: 'Last seen recently',
)
33. Typing Indicator
A typing indicator lets the recipient know that the other user is currently typing a message.
class TypingIndicator extends StatelessWidget {
final bool isTyping;
const TypingIndicator({
super.key,
required this.isTyping,
});
@override
Widget build(BuildContext context) {
if (!isTyping) {
return const SizedBox.shrink();
}
return const Padding(
padding: EdgeInsets.all(12),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Typing...',
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
),
);
}
}
34. Detect Typing
A text controller can be used to detect whether the user has entered text.
messageController.addListener(() {
final isTyping =
messageController.text.isNotEmpty;
updateTypingStatus(
chatId,
currentUserId,
isTyping,
);
});
In a production application, typing updates should be throttled or debounced so that the backend is not updated on every keystroke.
35. Unread Messages
Unread message counts can be maintained for each conversation.
int unreadCount = 0;
void markMessageAsRead() {
unreadCount = 0;
}
A backend implementation can store read status or last-read timestamps to synchronize unread counts between devices.
36. Message Timestamp
Each message should contain a timestamp so the interface can display when it was sent.
Text(
formatTime(
message.timestamp,
),
)
A simple time-formatting function can be created according to the desired application format.
37. Date Separators
Long conversations can be easier to read when messages are grouped by date.
Today
10:30 AM Hello
10:31 AM How are you?
Yesterday
08:20 PM I am fine.
Monday
09:15 AM Good morning!
38. Scroll to Latest Message
When a new message is sent, the chat screen should normally scroll toward the latest message.
final ScrollController scrollController =
ScrollController();
void scrollToBottom() {
WidgetsBinding.instance
.addPostFrameCallback((_) {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration:
const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
39. Image Sharing
A chat application can allow users to select an image, upload it to cloud storage, and send the resulting URL as a message.
final imageMessage = {
'senderId': currentUserId,
'receiverId': receiverId,
'text': imageUrl,
'type': 'image',
'timestamp':
FieldValue.serverTimestamp(),
};
40. Firebase Storage
Firebase Storage can be used to store uploaded images and files.
final storageRef = FirebaseStorage.instance
.ref()
.child(
'chat_images/$fileName',
);
await storageRef.putFile(file);
final imageUrl =
await storageRef.getDownloadURL();
41. Image Message Bubble
if (message.type == 'image') {
return ClipRRect(
borderRadius:
BorderRadius.circular(12),
child: Image.network(
message.text,
width: 220,
height: 220,
fit: BoxFit.cover,
),
);
}
42. Message Types
| Message Type | Purpose |
|---|
| text | Normal text message. |
| image | Image message. |
| file | Document or file message. |
| audio | Audio message. |
| video | Video message. |
| location | Location-sharing message. |
43. Delete a Message
A message can be deleted when the application's business rules and security rules allow it.
Future deleteMessage(
String chatId,
String messageId,
) async {
await FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.collection('messages')
.doc(messageId)
.delete();
}
44. Edit a Message
Message editing can be implemented by updating the message document.
Future editMessage({
required String chatId,
required String messageId,
required String newText,
}) async {
await FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.collection('messages')
.doc(messageId)
.update({
'text': newText,
'edited': true,
});
}
45. Search Users
A search feature can help users find another registered user before starting a conversation.
Future searchUsers(
String email,
) {
return FirebaseFirestore.instance
.collection('users')
.where(
'email',
isEqualTo: email,
)
.get();
}
46. Search Conversations
The chat list can also provide a search field to filter conversations locally.
List searchChats(
List chats,
String query,
) {
if (query.trim().isEmpty) {
return chats;
}
return chats.where((chat) {
return chat.name
.toLowerCase()
.contains(
query.toLowerCase(),
);
}).toList();
}
47. Profile Screen
The profile screen can display the user's name, email address, profile photo, status, and account actions.
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const CircleAvatar(
radius: 50,
child: Icon(
Icons.person,
size: 50,
),
),
const SizedBox(height: 16),
const Text(
'John Doe',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'[email protected]',
),
const SizedBox(height: 30),
ListTile(
leading: const Icon(
Icons.edit,
),
title: const Text(
'Edit Profile',
),
onTap: () {},
),
ListTile(
leading: const Icon(
Icons.logout,
),
title: const Text(
'Logout',
),
onTap: () {},
),
],
),
),
);
}
}
48. Push Notifications
Push notifications can inform users when they receive a new message while they are not actively viewing the chat screen.
- New message notification
- New chat notification
- Call notification
- Group notification
- System notification
49. Firebase Cloud Messaging
Firebase Cloud Messaging can be used as part of a push notification system. A typical notification flow is:
Sender
↓
Send Message
↓
Backend / Cloud Function
↓
Firebase Cloud Messaging
↓
Receiver Device
↓
Notification
50. Chat State Management
Chat applications contain state that may be shared between multiple widgets and screens. Examples include the current user, selected conversation, message list, unread count, online status, and typing state.
class ChatProvider extends ChangeNotifier {
bool isLoading = false;
bool isTyping = false;
void setLoading(bool value) {
isLoading = value;
notifyListeners();
}
void setTyping(bool value) {
isTyping = value;
notifyListeners();
}
}
51. Provider Setup
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ChatProvider(),
child: const ChatApplication(),
),
);
}
52. Loading State
Loading indicators should be displayed while user information, chats, or messages are being retrieved.
if (isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
53. Empty Chat State
When a user has no conversations, the application can display a helpful empty state.
Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(
Icons.chat_bubble_outline,
size: 80,
),
const SizedBox(height: 16),
const Text(
'No conversations yet',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Start a conversation with someone.',
),
],
)
54. Error Handling
Chat applications should handle authentication errors, database errors, network problems, upload failures, and permission errors.
try {
await ChatService().sendMessage(
chatId: chatId,
senderId: senderId,
receiverId: receiverId,
text: text,
);
} catch (error) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'Message failed: $error',
),
),
);
}
55. Firestore Security Rules Concept
Chat data should not be publicly readable or writable. Security rules should restrict access so that users can access only the documents they are authorized to access.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write:
if request.auth != null;
}
match /chats/{chatId} {
allow read, write:
if request.auth != null;
}
}
}
The example above is only a simplified learning example. Production security rules should enforce participant-level access rather than granting every authenticated user access to every chat.
56. Chat Privacy
- Authenticate users before accessing private chat data.
- Restrict chat access to conversation participants.
- Protect profile information.
- Validate message ownership before editing or deleting.
- Use secure backend rules.
- Do not expose sensitive credentials in the Flutter application.
- Use HTTPS and secure backend services.
57. One-to-One Chat Architecture
User A
↓
Flutter App
↓
Authentication
↓
Chat Service
↓
Cloud Firestore
↓
Real-Time Listener
↓
Flutter Chat UI
↑
User B
↑
Flutter App
58. Real-Time Message Flow
User A Types Message
↓
User A Presses Send
↓
Message Written to Firestore
↓
Firestore Updates
↓
Stream Listener Receives Update
↓
User B Chat UI Rebuilds
↓
User B Sees Message
59. Chat Application Navigation
Splash
↓
Login / Register
↓
Home
├── Chats
│ ↓
│ Chat Screen
│ ↓
│ Messages
│
├── Search Users
│ ↓
│ Start Chat
│
└── Profile
↓
Settings
60. Responsive Chat Interface
The chat interface should work on different screen sizes. On a larger screen, the application can display the conversation list and selected chat side by side.
Row(
children: [
SizedBox(
width: 320,
child: ChatListScreen(),
),
const VerticalDivider(
width: 1,
),
const Expanded(
child: ChatScreen(
chatId: 'chat-id',
receiverId: 'user-id',
receiverName: 'User',
),
),
],
)
61. Mobile Chat Layout
Mobile
-------------------------
| Chat App |
-------------------------
| Message |
| Message |
| Message |
| Message |
| |
-------------------------
| Type a message... ➤ |
-------------------------
62. Tablet and Desktop Layout
--------------------------------------------
| Conversations | Chat Screen |
|---------------|---------------------------|
| User 1 | User Name |
| User 2 |---------------------------|
| User 3 | Hello |
| User 4 | Hi! |
| User 5 | How are you? |
| | |
| | Type a message... ➤ |
--------------------------------------------
63. Message Read Status
A chat application can track whether a message has been delivered or read.
| Status | Meaning |
|---|
| Sent | Message was created by the sender. |
| Delivered | Message reached the recipient system/device. |
| Read | Recipient opened or viewed the message. |
64. Read Receipt Example
await FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.collection('messages')
.doc(messageId)
.update({
'read': true,
'readAt':
FieldValue.serverTimestamp(),
});
65. Block User Feature
A production chat application can allow users to block another account.
Future blockUser(
String currentUserId,
String blockedUserId,
) async {
await FirebaseFirestore.instance
.collection('users')
.doc(currentUserId)
.collection('blockedUsers')
.doc(blockedUserId)
.set({
'blockedAt':
FieldValue.serverTimestamp(),
});
}
66. Report User Feature
A report feature can allow users to report inappropriate behavior or content. Reports should normally be processed securely on the backend.
Future reportUser({
required String reporterId,
required String reportedUserId,
required String reason,
}) async {
await FirebaseFirestore.instance
.collection('reports')
.add({
'reporterId': reporterId,
'reportedUserId': reportedUserId,
'reason': reason,
'createdAt':
FieldValue.serverTimestamp(),
});
}
67. Chat Search
Search can be implemented for users and conversations. For large datasets, search requirements should be considered when designing the backend data model.
TextField(
decoration: InputDecoration(
hintText: 'Search chats',
prefixIcon:
const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(12),
),
),
)
68. Local Data and Offline Experience
A chat application should consider what happens when the device temporarily loses connectivity. Cached data and backend synchronization can provide a better user experience.
- Display previously loaded conversations.
- Show connection status.
- Handle failed message sends.
- Retry failed operations.
- Synchronize messages when connectivity returns.
69. Performance Optimization
- Use
ListView.builder for large message lists.
- Paginate older messages.
- Avoid rebuilding the complete chat screen unnecessarily.
- Compress large images before upload when appropriate.
- Use efficient Firestore queries.
- Keep listeners scoped to the required data.
- Dispose controllers when they are no longer needed.
- Throttle typing-status updates.
- Load older messages only when required.
70. Pagination for Chat History
Long conversations can contain thousands of messages. Loading the complete history at once can be inefficient. Pagination can load recent messages first and older messages as the user scrolls.
final query = FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timestamp', descending: true)
.limit(30);
71. Notifications Flow
Sender
↓
Message Sent
↓
Backend
↓
Check Receiver Status
↓
Receiver Offline?
↓
Send Push Notification
↓
Receiver Device
↓
Tap Notification
↓
Open Chat Screen
72. Testing the Chat Application
| Test Type | Example |
|---|
| Unit Test | Test chat ID generation or message formatting. |
| Widget Test | Test message bubbles and input widgets. |
| Integration Test | Test login, chat selection, and message sending. |
| Authentication Test | Test login and registration validation. |
| Database Test | Test message creation and retrieval. |
| Notification Test | Test push notification behavior. |
73. Common Problems and Solutions
| Problem | Possible Solution |
|---|
| Firebase is not initialized | Initialize Firebase before running the application. |
| Login fails | Check Firebase Authentication configuration and user credentials. |
| Messages are not appearing | Check Firestore collection paths, queries, security rules, and stream listeners. |
| Messages are duplicated | Check listener and message-writing logic. |
| Chat does not update in real time | Verify that the UI is listening to the correct Firestore stream. |
| Images fail to upload | Check Firebase Storage configuration, permissions, and file handling. |
| Notifications are not received | Check FCM configuration, permissions, tokens, and notification handling. |
| Application becomes slow | Optimize listeners, message pagination, images, and widget rebuilding. |
| Users can access unauthorized chats | Review and strengthen Firestore security rules. |
74. Suggested Development Steps
- Create the Flutter project.
- Configure Firebase.
- Add Firebase dependencies.
- Initialize Firebase.
- Create authentication screens.
- Implement registration.
- Implement login.
- Create the user model.
- Create the message model.
- Create the chat model.
- Design the Firestore database structure.
- Implement the chat service.
- Create the chat list.
- Create the chat screen.
- Implement message sending.
- Implement real-time message updates.
- Add timestamps.
- Add online/offline status.
- Add typing indicators.
- Add unread message counts.
- Add read receipts.
- Add image sharing.
- Add notifications.
- Add profile functionality.
- Add search.
- Add blocking and reporting.
- Implement security rules.
- Test the application.
- Optimize performance.
- Prepare the application for release.
75. Best Practices
- Keep authentication logic separate from UI code.
- Keep Firestore operations inside service classes.
- Use models for users, messages, and chats.
- Use reusable widgets for message bubbles and chat tiles.
- Use real-time streams carefully.
- Paginate long conversations.
- Validate user input.
- Protect Firestore data with proper security rules.
- Never expose sensitive credentials in the Flutter client.
- Handle network and backend errors gracefully.
- Dispose
TextEditingController and ScrollController objects appropriately.
- Optimize uploaded media.
- Test authentication and message flows on multiple devices.
76. Advanced Features
- Group Chat
- Voice Messages
- Video Messages
- Voice Calling
- Video Calling
- Message Reactions
- Message Replies
- Message Forwarding
- Message Pinning
- Disappearing Messages
- Chat Themes
- Dark Mode
- Online Presence
- Read Receipts
- Typing Indicators
- End-to-End Encryption
77. Group Chat Architecture
Group
↓
Group Information
├── Group Name
├── Group Photo
├── Admin
└── Members
↓
Messages
├── User A
├── User B
├── User C
└── User D
78. Complete Chat Architecture
CHAT APPLICATION
|
+--------------+--------------+
| | |
Auth Chats Profile
| |
Login/Register Chat List
|
Select User
|
Chat Screen
|
+--------+--------+
| | |
Text Image File
| | |
+--------+--------+
|
Firestore
|
Real-Time Stream
|
Receiver Interface
|
Notification
79. Learning Outcomes
After completing this project, learners should understand how to build a practical Flutter chat application. The project provides experience with Flutter widgets, Dart programming, Firebase Authentication, Firestore, real-time streams, state management, navigation, user profiles, message models, chat services, notifications, media uploads, security rules, responsive layouts, and application testing.
80. Interview Questions
- How would you structure a Flutter chat application?
- How does Firebase Authentication work with Flutter?
- What is Cloud Firestore?
- How can Firestore be used for real-time chat?
- What is the difference between
FutureBuilder and StreamBuilder?
- Why is
StreamBuilder useful for chat applications?
- How would you structure chat messages in Firestore?
- How would you generate a unique chat ID?
- How would you implement a typing indicator?
- How would you implement online/offline status?
- How would you implement read receipts?
- How would you send push notifications?
- How would you upload images in a chat application?
- How would you implement message pagination?
- How would you protect private chat data?
- How would you implement message deletion?
- How would you optimize a chat application with thousands of messages?
- How would you implement group chat?
- What are Firestore security rules?
- How would you test a Flutter chat application?
81. Summary
Building a Flutter Chat Application is an excellent practical project for learning real-time application development. The project combines Flutter UI development, Dart programming, authentication, Firestore, real-time streams, state management, navigation, user profiles, message handling, online status, typing indicators, notifications, media uploads, and security.
A beginner version can start with local messages and simple screens. As the application grows, Firebase Authentication, Cloud Firestore, Firebase Storage, push notifications, real-time presence, read receipts, media sharing, group chat, and advanced security can be added.
Learn Flutter with JustAcademy
JustAcademy Flutter Training Course
Register for Flutter Course Demo