blog

Building Complex UIs in Flutter: Custom Widgets and a Feature-Rich Chat App

Share:

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 🤝

Related articles

Circle icon
Circle icon
Circle icon
Circle icon
Circle icon
Circle icon
Circle icon
Circle icon
Circle icon
Circle icon
Circle icon
Circle icon

get in touch

EVEN IF YOU DON'T YET KNOW WHERE TO START WITH YOUR PROJECT - THIS IS THE PLACE

Drop us a few lines and we'll get back to you within one business day.

Thank you for your inquiry! Someone from our team will contact you shortly.
Where from have you heard about us?
Clutch
GoodFirms
Crunchbase
Googlesearch
LinkedIn
Facebook
Your option
I have read and accepted the Terms & Conditions and Privacy Policy
bracket icon
bracket icon
bracket icon
bracket icon
bracket icon
bracket icon
slash icon
slash icon
slash icon
slash icon
slash icon
slash icon
bracket icon
bracket icon
bracket icon
bracket icon
bracket icon
bracket icon