Have you ever wanted to build a chat app and wondered how to create this messaging UI?
While this UI looks simple, there are a few things to consider:
- All text messages are shown inside a "chat bubble" with rounded corners and a fill color
- Chat bubbles are left-aligned or right-aligned depending on who sent the message
- The text in each bubble should wrap if it doesn't fit in one line
So let's see how to build this!
Along the way, we'll learn how to decorate, align and add custom styling to our widgets.
Creating a ChatBubble widget
As a first step, we can create a custom widget class to represent our chat bubble:
class ChatBubble extends StatelessWidget {
const ChatBubble({
Key? key,
required this.text,
required this.isCurrentUser,
}) : super(key: key);
final String text;
final bool isCurrentUser;
// TODO: Implement build method
}
This widget takes text
and isCurrentUser
as arguments, and needs to determine the alignment and style of the chat bubble.
For reference, we may use it like this in the parent widget:
ListView(
children: const [
ChatBubble(
text: 'How was the concert?',
isCurrentUser: false,
),
ChatBubble(
text: 'Awesome! Next time you gotta come as well!',
isCurrentUser: true,
),
ChatBubble(
text: 'Ok, when is the next date?',
isCurrentUser: false,
),
ChatBubble(
text: 'They\'re playing on the 20th of November',
isCurrentUser: true,
),
ChatBubble(
text: 'Let\'s do it!',
isCurrentUser: false,
),
],
)
In real-world apps, all the chat data would come from a backend and should be displayed with
ListView.builder
to ensure that only visible chat bubbles are rendered.
Next, let's work out what goes into the build
method.
1. Adding a DecoratedBox
Step one is to put a Text
widget inside a DecoratedBox
like so:
@override
Widget build(BuildContext context) {
return DecoratedBox(
// chat bubble decoration
decoration: BoxDecoration(
color: isCurrentUser ? Colors.blue : Colors.grey[300],
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text,
style: Theme.of(context).textTheme.bodyText1!.copyWith(
color: isCurrentUser ? Colors.white : Colors.black87),
),
),
);
}
Note: We could have used a
Container
as an alternative toDecoratedBox
. ButContainer
does a lot of things under the hood andDecoratedBox
is more lightweight, so we can use it for better performance.
If we run this code, the chat bubbles will appear like this:
Not a bad start, but we don't want the chat bubbles to stretch horizontally. And we could do with some padding.
2. Adding an Align widget
Let's fix this layout by adding Align
and Padding
widgets:
@override
Widget build(BuildContext context) {
return Padding(
// add some padding
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Align(
// align the child within the container
alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft,
child: DecoratedBox(
// chat bubble decoration
decoration: BoxDecoration(
color: isCurrentUser ? Colors.blue : Colors.grey[300],
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text,
style: Theme.of(context).textTheme.bodyText1!.copyWith(
color: isCurrentUser ? Colors.white : Colors.black87),
),
),
),
),
);
}
The most important line is this:
alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft,
This will ensure that each chat bubble is aligned correctly and doesn't fill all the available width:
This is much better and the text wraps over multiple lines if needed:
Let's do one final tweak.
3. Final tweak: asymmetric padding
The chat bubble should include a bit of padding on the left or right side to prevent it from using the full available width when the text wraps.
Here's how to do it:
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(
isCurrentUser ? 64.0 : 16.0,
4,
isCurrentUser ? 16.0 : 64.0,
4,
),
child: Align(...)
);
}
And here's how it looks when the text wraps:
That's it! We built a responsive chat bubble UI that we can reuse in our apps.
Here's the full code for the ChatBubble
class:
class ChatBubble extends StatelessWidget {
const ChatBubble({
Key? key,
required this.text,
required this.isCurrentUser,
}) : super(key: key);
final String text;
final bool isCurrentUser;
@override
Widget build(BuildContext context) {
return Padding(
// asymmetric padding
padding: EdgeInsets.fromLTRB(
isCurrentUser ? 64.0 : 16.0,
4,
isCurrentUser ? 16.0 : 64.0,
4,
),
child: Align(
// align the child within the container
alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft,
child: DecoratedBox(
// chat bubble decoration
decoration: BoxDecoration(
color: isCurrentUser ? Colors.blue : Colors.grey[300],
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text,
style: Theme.of(context).textTheme.bodyText1!.copyWith(
color: isCurrentUser ? Colors.white : Colors.black87),
),
),
),
),
);
}
}
Lessons learned
The completed implementation is simple and elegant.
But when I started researching this, I went down the wrong route and attempted to use a Row
to align things. And I ended up using LayoutBuilder
and ConstrainedBox
to fight some layout issues. If you're curious about this approach, here's a Twitter thread.
As it turns out, sometimes it's best to take a step back and look for a simpler solution.
Albert Einstein said it best:
"Everything should be made as simple as possible, but not simpler."
Turns out, this is great advice for Flutter app development. 😀
Happy coding!