How to Build a Beautiful, Full-Featured Chat App UI With Flutter and Dart
Flutter has taken the mobile app development world by storm in recent years, allowing developers to quickly build high-quality, cross-platform apps from a single codebase. With its rich set of customizable widgets, expressive UI framework, and reactive programming model, Flutter is particularly well-suited for implementing chat app user interfaces.
In this in-depth tutorial, we‘ll walk through the process of building a complete chat application UI using Flutter and Dart. Whether you‘re a beginner looking to learn the fundamentals of Flutter UI development or an experienced dev seeking best practices for architecting chat app interfaces, this guide has you covered. Let‘s get started!
Setting Up a New Flutter Project
First, make sure you have the Flutter SDK installed by following the official installation instructions. Then, create a new project by running this command in your terminal:
flutter create flutter_chat_app
Next, open the project in your favorite IDE and navigate to the pubspec.yaml
file. Here we‘ll add the dependencies for our chat app:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.3
bubble: ^1.2.1
jiffy: ^5.0.0
We‘ll be using the bubble
package for rendering message bubbles and the jiffy
package for handling dates/times. Run flutter pub get
in your terminal to install these packages.
Designing the Chat App Layout
Let‘s start by sketching out the high-level structure of our chat app UI. We‘ll have two main screens:
- The conversations screen showing a list of recent chats
- The chat screen where the user can view and send messages with a specific contact
We can represent this layout using Flutter‘s MaterialApp
and Scaffold
widgets:
class ChatApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ConversationsScreen(),
routes: {
‘/chat‘: (context) => ChatScreen(),
},
);
}
}
class ConversationsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(‘Chats‘)),
body: ConversationsList(),
);
}
}
class ChatScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(‘Chat‘)),
body: ChatMessages(),
);
}
}
This gives us a basic framework to start fleshing out the UI of each screen. Note how we‘ve defined a /chat
route to navigate from the conversations screen to an individual chat.
Building the Conversations Screen
Now let‘s implement the UI for the conversations screen, which will consist of a scrollable ListView of recent chats. Each list item will display the contact‘s name, profile picture, latest message, and timestamp.
First, we‘ll define a Conversation
model class to represent each chat:
class Conversation {
final String contactName;
final String contactImageUrl;
final String latestMessage;
final String latestMessageTime;
Conversation({
required this.contactName,
required this.contactImageUrl,
required this.latestMessage,
required this.latestMessageTime,
});
}
Next, we‘ll hard-code some sample conversation data:
final List<Conversation> conversations = [
Conversation(
contactName: ‘John Smith‘,
contactImageUrl: ‘https://i.pravatar.cc/150?img=1‘,
latestMessage: ‘Hey, how are you?‘,
latestMessageTime: ‘3:30 PM‘,
),
Conversation(
contactName: ‘Sam Clarke‘,
contactImageUrl: ‘https://i.pravatar.cc/150?img=2‘,
latestMessage: ‘Did you finish the FlutterChat tutorial?‘,
latestMessageTime: ‘Yesterday‘,
),
// Add more sample conversations...
];
Finally, we‘ll build a ConversationsList
widget to render the list of chats:
class ConversationsList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: conversations.length,
itemBuilder: (context, index) {
final conversation = conversations[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(conversation.contactImageUrl),
),
title: Text(conversation.contactName),
subtitle: Text(conversation.latestMessage),
trailing: Text(conversation.latestMessageTime),
onTap: () {
Navigator.pushNamed(context, ‘/chat‘);
},
);
},
);
}
}
This uses Flutter‘s built-in ListTile
widget to render each conversation with an image avatar, name, message preview, and timestamp. When a conversation is tapped, it navigates to the /chat
route to open the chat screen.
Implementing the Chat Screen
Next up is building the screen for an individual chat with a specific contact. This will consist of an app bar showing the contact‘s name and picture, a scrolling list of message bubbles, and an input field for composing new messages.
The key Flutter widgets we‘ll use to build this UI include:
Scaffold
andAppBar
for the basic screen layoutListView
for the scrolling list of messagesBubble
from the bubble package for rendering message bubblesTextField
andIconButton
for the message input field
Here‘s how we can structure the ChatScreen
widget:
class ChatScreen extends StatelessWidget {
final Conversation conversation;
ChatScreen({required this.conversation});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(conversation.contactName),
actions: [
IconButton(
icon: Icon(Icons.info),
onPressed: () {},
),
],
),
body: Column(
children: [
Expanded(
child: MessageList(conversation: conversation),
),
MessageInput(),
],
),
);
}
}
The MessageList
widget will render the list of chat bubbles, fetching messages from some mock message data:
class MessageList extends StatelessWidget {
final Conversation conversation;
MessageList({required this.conversation});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
return Row(
mainAxisAlignment: message.isSentByMe
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
if (!message.isSentByMe) ...[
CircleAvatar(
backgroundImage: NetworkImage(conversation.contactImageUrl),
),
SizedBox(width: 8.0),
],
Bubble(
nip: message.isSentByMe ? BubbleNip.rightTop : BubbleNip.leftTop,
margin: BubbleEdges.only(
top: 8.0,
left: message.isSentByMe ? 0.0 : 55.0,
right: message.isSentByMe ? 55.0 : 0.0,
),
color: message.isSentByMe ? Colors.blueGrey : Colors.grey[300],
child: Text(message.content),
),
if(message.isSentByMe) ...[
SizedBox(width: 8.0),
CircleAvatar(
backgroundImage: NetworkImage(‘https://i.pravatar.cc/150?img=56‘),
),
],
],
);
},
);
}
}
This aligns messages sent by the user on the right side and received messages on the left. It also conditionally shows the user‘s avatar next to their sent messages.
To handle user input, we‘ll create a simple MessageInput
widget with a text field and send button:
class MessageInput extends StatefulWidget {
@override
_MessageInputState createState() => _MessageInputState();
}
class _MessageInputState extends State<MessageInput> {
late TextEditingController _textController;
@override
void initState() {
super.initState();
_textController = TextEditingController();
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _textController,
decoration: InputDecoration(
hintText: ‘Type a message‘,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: () {
final String content = _textController.text;
if (content.trim().isNotEmpty) {
// Send the message...
_textController.clear();
}
},
),
],
),
);
}
@override
void dispose() {
_textController.dispose();
super.dispose();
}
}
With these pieces in place, we now have a functional UI for the chat screen:
Of course, this is just a starting point. You could enhance the UI with additional features like:
- Displaying message timestamps
- Handling longer messages that span multiple lines
- Adding support for images, videos, links, etc.
- Showing typing indicators or read receipts
- Allowing users to react to messages
Animating Message Bubbles
To make the chat UI feel more dynamic and responsive, let‘s add a subtle animation when new messages appear. We can use Flutter‘s built-in AnimationController
and SlideTransition
to animate each message bubble sliding in from the bottom.
First, update the MessageList
to create animation controllers for each message:
class MessageList extends StatefulWidget {
final Conversation conversation;
MessageList({required this.conversation});
@override
_MessageListState createState() => _MessageListState();
}
class _MessageListState extends State<MessageList> with TickerProviderStateMixin {
final List<AnimationController> _animationControllers = [];
@override
void dispose() {
for (final controller in _animationControllers) {
controller.dispose();
}
super.dispose();
}
// Build method...
Widget _buildMessageBubble(Message message, Animation<double> animation) {
return SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
),
child: Row(
mainAxisAlignment: message.isSentByMe
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
// Message bubble content...
],
),
);
}
}
Then, wire up the animation in the itemBuilder
:
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
AnimationController animationController;
if (index >= _animationControllers.length) {
animationController = AnimationController(
duration: Duration(milliseconds: 200),
vsync: this,
)..forward();
_animationControllers.add(animationController);
} else {
animationController = _animationControllers[index];
}
return _buildMessageBubble(message, animationController);
},
);
}
Now when the MessageList
is first built, each message will animate in from the bottom. The animation is kept subtle with a 200ms duration.
Flutter Chat UI Best Practices
As you continue building out a fully-featured chat UI, keep these best practices in mind:
- Use a ListView.builder to efficiently render long lists of conversations and messages.
- Adopt a responsive layout that adapts to different screen sizes and orientations.
- Leverage Flutter‘s hot reload feature to quickly iterate on your UI design.
- Keep your widget tree shallow by extracting sub-components and moving them to separate files.
- Integrate with a real-time messaging backend like Firebase, PubNub, or Stream Chat.
- Write unit and widget tests to catch bugs and prevent regressions as your app grows in complexity.
- Internationalize your UI to support users in different locales.
- Follow platform-specific design guidelines and user expectations on Android and iOS.
- Continuously profile your app‘s performance to identify and fix any jank or lag.
- Gather user feedback early and often to validate and improve your chat UI/UX.
Conclusion
In this tutorial, we walked through the process of building a chat app UI from scratch using Flutter and Dart. We started by laying out the screen-by-screen structure with routes. Then we broke down each screen into its constituent UI components and implemented them one-by-one. Along the way, we touched on various aspects of Flutter UI development, including:
- Working with ListView and ListTile widgets
- Designing reusable model classes for conversations and messages
- Customizing the visual style of message bubbles
- Animating new messages as they appear
- Capturing and responding to user input events
By understanding these fundamentals, you‘ll be well on your way to crafting engaging, interactive chat experiences with Flutter. But don‘t stop here! Continue exploring more advanced chat app UI/UX concepts like real-time messaging, offline support, encryption, accessibility, and more.
The complete source code for this tutorial is available on GitHub. Feel free to use it as a starting point for your own chat app projects. If you have any questions or feedback, let me know in the comments below. Happy coding!