Build a real-time chat using a Firestore messages subcollection under each conversation document. Display messages in a reversed ListView ordered by timestamp descending so new messages appear at the bottom. Style message bubbles as Containers with different colors and alignment based on whether senderId matches the current user. Pin a TextField with a send IconButton at the bottom. Add a typing indicator by updating a typingUserId field on the conversation document.
Building a Real-Time Chat Interface in FlutterFlow
Chat is a core feature for social apps, customer support, marketplaces, and collaboration tools. This tutorial builds a complete chat screen with real-time Firestore messaging, styled bubbles, and typing indicators using only the FlutterFlow visual builder.
Prerequisites
- A FlutterFlow project with Firestore and authentication configured
- A conversations collection in Firestore
- Two test user accounts for sending and receiving messages
Step-by-step guide
Set up the Firestore data model for conversations and messages
Set up the Firestore data model for conversations and messages
Create a conversations collection with fields: participantIds (String Array), lastMessage (String), lastMessageTimestamp (Timestamp), typingUserId (String, nullable). Create a messages subcollection under each conversation with fields: text (String), senderId (String), senderName (String), timestamp (Timestamp), type (String: 'text'). Add indexes for messages ordered by timestamp descending. This subcollection pattern scales well — each conversation loads only its own messages.
Expected result: Firestore has conversations with a messages subcollection ready for real-time chat.
Build the chat page layout with reversed ListView and input bar
Build the chat page layout with reversed ListView and input bar
Create a ChatPage with Route Parameter conversationId. Use a Column layout. Add an Expanded widget containing a ListView set to reverse: true. Below the Expanded, add a Container with a Row: a TextField (flex: 1, hint: 'Type a message...', maxLines: 4) and an IconButton (send icon, Primary color). The reversed ListView shows newest messages at the bottom, matching how every chat app works. The input bar stays pinned at the bottom.
Expected result: A chat page with messages area on top and input bar pinned at the bottom.
Bind the ListView to a real-time Firestore query on messages
Bind the ListView to a real-time Firestore query on messages
On the ListView, add a Backend Query: Query Collection on conversations/{conversationId}/messages, ordered by timestamp descending. Critically, disable Single Time Query so the query is real-time — new messages appear instantly without refreshing. Enable Generate Dynamic Children. Each child is a message bubble Component that receives text, senderId, senderName, and timestamp as Component Parameters.
Expected result: Messages from Firestore display in the ListView and update in real-time as new messages are sent.
Style message bubbles with conditional alignment and color
Style message bubbles with conditional alignment and color
Create a MessageBubble Component with parameters: text, senderId, senderName, timestamp. The root widget is an Align widget. Set Conditional alignment: if senderId == currentUser.uid → Alignment.centerRight (sent), else → Alignment.centerLeft (received). Inside, a Container with borderRadius (rounded corners, flat on the side closest to the edge), background color (Primary for sent, grey100 for received), max width 75% of screen. Inside: Text message in white (sent) or black (received), and timestamp Text in small grey.
Expected result: Sent messages align right with primary color, received messages align left with grey background.
Implement the send action and typing indicator
Implement the send action and typing indicator
On the send IconButton tap: check TextField is not empty → Create Document in messages subcollection (text, senderId: currentUser.uid, senderName: currentUser.displayName, timestamp: now, type: 'text') → Update conversation document (lastMessage: text, lastMessageTimestamp: now) → Clear TextField. For typing indicator: on TextField focus, update conversation typingUserId to currentUser.uid. On blur or send, set typingUserId to null. Show a typing indicator Component (three dots animation) above the input bar with Conditional Visibility: typingUserId != null AND typingUserId != currentUser.uid.
Expected result: Sending creates a message in Firestore visible to both users. The typing indicator shows when the other person is typing.
Complete working example
1FIRESTORE DATA MODEL:2 conversations/{conversationId}3 participantIds: ["uid1", "uid2"]4 lastMessage: String5 lastMessageTimestamp: Timestamp6 typingUserId: String (nullable)7 └── messages/{messageId}8 text: String9 senderId: String10 senderName: String11 timestamp: Timestamp12 type: "text"1314PAGE: ChatPage15 Route Parameter: conversationId (String)1617WIDGET TREE:18 Column19 ├── Expanded20 │ └── ListView (reverse: true)21 │ Backend Query: messages, order by timestamp desc, real-time22 │ Generate Dynamic Children → MessageBubble Component23 ├── TypingIndicator Component24 │ Conditional Visibility: typingUserId != null && != currentUser.uid25 │ Layout: Row of 3 animated dots26 └── Container (input bar, padding 8)27 └── Row28 ├── Expanded TextField (hint: 'Type a message...', maxLines: 4)29 └── IconButton (send, Primary)30 On Tap:31 1. Create Document: messages/ (text, senderId, timestamp)32 2. Update Document: conversation (lastMessage, timestamp)33 3. Clear TextField34 4. Set typingUserId: null3536MESSAGE BUBBLE COMPONENT:37 Align (Conditional: right if sent, left if received)38 Container (maxWidth: 75%, borderRadius, bg: Primary/grey)39 Column40 Text (message text)41 Text (timestamp, small, grey)Common mistakes when creating a Custom Chat Widget for Your FlutterFlow App
Why it's a problem: Not using a reversed ListView for the message list
How to avoid: Set reverse: true on the ListView and order the Firestore query by timestamp descending. Newest messages appear at the bottom automatically.
Why it's a problem: Using a Single Time Query instead of real-time for messages
How to avoid: Disable Single Time Query on the Backend Query so it uses real-time listeners. New messages appear instantly.
Why it's a problem: Incrementing a like count or message count without Firestore transactions
How to avoid: Use FieldValue.serverTimestamp() for timestamps and FieldValue.increment() for counts in Custom Actions, or use Firestore transactions.
Best practices
- Use reversed ListView with timestamp descending order for natural chat layout
- Enable real-time queries (disable Single Time Query) for instant message updates
- Style message bubbles differently for sent vs received with conditional alignment and color
- Pin the input bar at the bottom outside the scrollable area
- Update the conversation document's lastMessage for display in conversation list
- Add a typing indicator using a Firestore field that updates on TextField focus/blur
- Limit the message query to the last 50 messages and load more on scroll for performance
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to build a real-time chat screen in FlutterFlow using Firestore subcollections. Show the data model for conversations/messages, reversed ListView query setup, conditional message bubble styling for sent/received, send action flow, and typing indicator implementation.
Create a chat page with a scrollable list taking up most of the screen and an input row at the bottom with a text field and send button.
Frequently asked questions
Can I add image and file sharing to the chat?
Yes. Add a type field to messages (text/image/file). For images, upload via FlutterFlowUploadButton to Firebase Storage, save the download URL in the message, and render an Image widget conditionally when type is 'image'.
How do I implement read receipts?
Add a readBy array field to each message. When a user views a message, add their UID to readBy. Display a read indicator (double checkmark) when readBy contains the other participant's UID.
Can I build group chat with more than 2 users?
Yes. The participantIds array already supports multiple users. Display senderName in group bubbles and allow any participant to send. Filter conversations where participantIds array-contains the current user.
How do I handle message pagination for very long conversations?
Set a page size (50 messages) in the Backend Query. Enable infinite scroll to load older messages as the user scrolls up. Since the ListView is reversed, scrolling up loads older messages.
Does real-time chat use a lot of Firestore reads?
Each real-time listener counts as one read per document initially, then one read per change. For cost optimization, limit the initial query to 50 messages and paginate.
Can RapidDev help build a full chat system?
Yes. RapidDev can implement chat with image/file sharing, read receipts, push notifications, message reactions, message search, and end-to-end encryption.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation