
Flutter’s expressive and flexible UI toolkit makes it a fantastic choice for building beautiful, high-performance chat applications. But to create a great user experience, you need to think about more than just chat bubbles. Your UI must be fast, responsive to all screen sizes, and built on a solid foundation.
This guide covers everything you need to know, from the core widgets and key dependencies to performance optimization and making your UI look great on any device, from a phone to a web browser.
The Core Components: Building from Scratch
Before jumping into packages, it’s helpful to understand the basic Flutter widgets that make up a chat screen:
Scaffold: Provides the basic app structure (app bar, body).Column: The main widget that holds the chat history and the text input field.Expanded(inside theColumn): This is crucial. It holds the message list and tells it to take up all available space, pushing the input field to the bottom.ListView.builder: This is the key to performance. Never use a simpleColumnorListViewfor a chat history.ListView.builderonly builds the message bubbles that are visible on screen, allowing your app to handle thousands of messages without slowing down.Row(for the input area): Holds theTextFieldand theIconButton(send button) side-by-side.TextField: The actual text input field.Container/Card: Used to create the chat bubbles themselves, which you can style withBoxDecoration.
Key Dependencies: Packages to Supercharge Your UI
While building from scratch is a great learning exercise, several packages can save you hundreds of hours. They come with built-in features like message-typing indicators, avatars, date headers, and media handling.
Here are the most popular choices:
flutter_chat_ui: A highly customizable and complete chat UI solution. It’s backend-agnostic, meaning you can plug it into any backend service (like Firebase, Supabase, or your own).dash_chat_2: Another excellent, feature-rich, and easy-to-use chat UI package. It’s a spiritual successor to the originaldash_chatand is very popular in the community.
Other useful packages:
cached_network_image: Essential for loading and caching user avatars or images sent in the chat.intl: For formatting timestamps and dates (e.g., “Just now”, “Yesterday, 10:30 AM”).flutter_markdown: If you want to support markdown formatting in your chat messages.
UI Sample: A Simple “From Scratch” Chat Screen
Here’s a simplified example of a chat screen built from scratch to illustrate the core concepts.
import 'package:flutter/material.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
// Store messages in a list
final List<String> _messages = [];
final TextEditingController _controller = TextEditingController();
void _sendMessage() {
if (_controller.text.isNotEmpty) {
// Use setState to update the UI
setState(() {
_messages.insert(0, _controller.text); // Add new messages to the top
});
_controller.clear();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chat Room'),
),
body: Column(
children: [
// This Expanded widget holds the message list
Expanded(
child: ListView.builder(
reverse: true, // Makes the list start from the bottom
itemCount: _messages.length,
itemBuilder: (context, index) {
// This is our chat bubble
return Align(
alignment: Alignment.centerRight,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[600],
borderRadius: BorderRadius.circular(20),
),
child: Text(
_messages[index],
style: const TextStyle(color: Colors.white),
),
),
);
},
),
),
// This Row holds the input field and send button
_buildTextComposer(),
],
),
);
}
Widget _buildTextComposer() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
offset: const Offset(0, -1),
blurRadius: 2,
color: Colors.grey.withOpacity(0.1),
),
],
),
child: SafeArea(
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration.collapsed(
hintText: 'Send a message...',
),
onSubmitted: (value) => _sendMessage(),
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
),
],
),
),
);
}
}
Making Your Chat UI Responsive (for All Devices)

A chat app in 2025 must be responsive. It should look just as good on a large web browser or tablet as it does on a small phone. The key is to adapt your layout based on the available screen width.
The Strategy:
On a mobile device, you typically only show the chat conversation screen. On a tablet or web device, you have enough space to show a “master-detail” view: a list of conversations on the left and the selected chat on the right.
How to Implement It:
You can achieve this using MediaQuery or LayoutBuilder.
MediaQuery: Get the screen width and use a simpleifstatement.@override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; const breakpoint = 600; // Define your "tablet" breakpoint if (screenWidth < breakpoint) { // --- Mobile Layout --- // Just return the ChatScreen return ChatScreen(conversationId: _selectedConversationId); } else { // --- Tablet/Web Layout --- return Row( children: [ // Left Panel (Conversation List) SizedBox( width: 300, child: ConversationList( onConversationSelected: (id) { setState(() => _selectedConversationId = id); }, ), ), // Right Panel (Chat Screen) Expanded( child: ChatScreen(conversationId: _selectedConversationId), ), ], ); } }LayoutBuilder: This widget is great for making a part of your UI responsive. It gives you theconstraintsof its parent, not the whole screen.
Best Performance: The Golden Rule
As mentioned, performance is critical for a good chat experience.
The Golden Rule: Use ListView.builder (or SliverList).
- Why?
ListView.builderuses “lazy loading.” It only creates and renders the widgets that are currently visible on the screen. - What to avoid? Never, ever use
ColumnorListViewinside aSingleChildScrollViewfor a list of unknown length. This builds every single widget in the list at once, even if there are 10,000 messages. Your app will freeze and likely crash.
Quick Performance Tips:
- Use
constconstructors for widgets that don’t change. - Keep your widget tree as flat as possible.
- Use
CachedNetworkImagefor profile pictures to avoid re-downloading them.
Alternatives: Package vs. Custom
- Go with a Package (like
flutter_chat_ui):- Pros: Incredibly fast development. You get tons of features out of the box (replies, reactions, typing indicators, media uploads).
- Cons: You are limited by the package’s design. Customization can be complex if you need a truly unique look.
- Build from Scratch:
- Pros: 100% control over the UI, performance, and features. It’s a great way to learn Flutter’s layout system.
- Cons: It is a lot of work. You’ll have to manually implement features like bubble tails, date separators, read-receipts, and media handling.
My recommendation: Start with a package like flutter_chat_ui or dash_chat_2. If your app’s design is so unique that the package is holding you back, you can then consider building your own.
Happy coding!
