Flutter is a versatile framework that excels at building beautifully customized user interfaces. As your app scales, you’ll often find the need to go beyond standard widgets and design more advanced, dynamic components. In this article, we’ll walk through how to build complex UIs using custom widgets by creating an interactive chat application — complete with message bubbles, a typing indicator, and automatic replies.
Custom widgets give you the power to build flexible, reusable components tailored to your app’s unique requirements. Since everything in Flutter is a widget, you can easily compose complex UI structures that are scalable and maintainable.
We’ll create a simple chat app that allows users to send messages and receive simulated responses. Each message will be displayed inside a stylized bubble with a “tail,” and an animated “Your friend is typing…” indicator will simulate real-time messaging behavior.
📌 Step 1: Setting Up the Chat App Structure
Let’s begin by building the core layout, including message handling, input controls, and scroll behavior.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(ChatApp());
class ChatApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Custom Chat App",
home: ChatScreen(),
);
}
}
class ChatScreen extends StatefulWidget {
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
List<Map<String, dynamic>> _messages = [];
TextEditingController _controller = TextEditingController();
ScrollController _scrollController = ScrollController();
bool _isTyping = false;
The ChatApp
sets up our app’s root widget, while ChatScreen
manages the message list and chat interaction logic.
📌 Step 2: Sending and Receiving Messages
To simulate a real chat experience, we’ll show a typing indicator before automatically replying to messages after a brief delay.
void _sendMessage() {
String message = _controller.text.trim();
if (message.isEmpty) return;
setState(() {
_messages.add({"message": message, "isSentByMe": true});
_isTyping = true;
});
_controller.clear();
_scrollToBottom();
Timer(Duration(seconds: 2), () {
setState(() {
_isTyping = false;
_messages.add({
"message": "Your friend's auto-generated reply",
"isSentByMe": false,
});
_scrollToBottom();
});
});
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
This code ensures a smooth interaction by scrolling to the latest message after every send/receive.
📌 Step 3: Building the Chat UI
Now we’ll add the chat screen’s visual layout, including the message list, typing indicator, and input field.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Chat App")),
body: Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _messages.length,
itemBuilder: (context, index) {
return ChatBubble(
message: _messages[index]["message"],
isSentByMe: _messages[index]["isSentByMe"],
);
},
),
),
if (_isTyping)
Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"Your friend is typing...",
style: TextStyle(fontSize: 12, color: Colors.grey),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: "Enter a message...",
border: OutlineInputBorder(),
),
minLines: 1,
maxLines: 5,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: _sendMessage,
),
],
),
),
],
),
);
}
Here, the message list fills the available space, and the typing indicator sits just above the input area for a realistic experience.
📌 Step 4: Designing a Custom Chat Bubble
To complete the look, we’ll build a reusable chat bubble widget that adjusts its alignment, color, and tail based on who sent the message.
class ChatBubble extends StatelessWidget {
final String message;
final bool isSentByMe;
ChatBubble({required this.message, required this.isSentByMe});
@override
Widget build(BuildContext context) {
return Align(
alignment: isSentByMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
padding: EdgeInsets.all(10),
constraints: BoxConstraints(maxWidth: 250),
decoration: BoxDecoration(
color: isSentByMe ? Colors.blue : Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
bottomLeft: isSentByMe ? Radius.circular(15) : Radius.zero,
bottomRight: isSentByMe ? Radius.zero : Radius.circular(15),
),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 5,
),
],
),
child: Text(
message,
style: TextStyle(
color: isSentByMe ? Colors.white : Colors.black87,
),
),
),
);
}
}
The ChatBubble
widget mimics real chat apps by styling each message differently depending on the sender. A subtle shadow and rounded corners enhance the visual depth.
In this tutorial, we built a simple but functional chat app using Flutter’s powerful widget system. Along the way, we explored:
- Structuring complex UIs
- Creating reusable custom widgets
- Simulating typing indicators and auto-replies
Flutter’s flexibility makes it ideal for building highly interactive, polished apps. Try extending this project by adding avatars, timestamps, message status indicators, or even chat animations.
Got questions or ideas to expand this app?
We’re here to assist you with any questions https://synpass.pro/contactsynpass/ 🤝