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:

  1. The conversations screen showing a list of recent chats
  2. 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.

Conversations 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 and AppBar for the basic screen layout
  • ListView for the scrolling list of messages
  • Bubble from the bubble package for rendering message bubbles
  • TextField and IconButton 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:

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:

  1. Use a ListView.builder to efficiently render long lists of conversations and messages.
  2. Adopt a responsive layout that adapts to different screen sizes and orientations.
  3. Leverage Flutter‘s hot reload feature to quickly iterate on your UI design.
  4. Keep your widget tree shallow by extracting sub-components and moving them to separate files.
  5. Integrate with a real-time messaging backend like Firebase, PubNub, or Stream Chat.
  6. Write unit and widget tests to catch bugs and prevent regressions as your app grows in complexity.
  7. Internationalize your UI to support users in different locales.
  8. Follow platform-specific design guidelines and user expectations on Android and iOS.
  9. Continuously profile your app‘s performance to identify and fix any jank or lag.
  10. 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!

Similar Posts