Flutter: Making Beautiful Chat Bubbles (2 Approaches)

Updated: October 5, 2022 By: A Goodman Post a comment

This article walks you through a couple of different ways to create beautiful, professional chat bubbles in Flutter. In the first approach, we will write code from scratch and only use built-in stuff of Flutter like CustomPainter, Transform, etc. In the second approach, we will take advantage of a third-party library to get the job done quickly.

A Brief Overview

Nowadays, chat feature appears in many kinds of apps, from messaging apps to e-commerce platforms, from education apps to social networks. In general, speech bubbles often have the following basic characteristics:

  • Outgoing messages are on the right side
  • Incoming messages are on the left side
  • Usually have a small triangle (like an arrowhead)
  • The corners are usually rounded
  • The background color of incoming and outgoing messages will be different, and different from the background color of the app for easy identification

Creating chat bubbles from scratch

Example preview

We will create two reusable bubble widgets: one for sent messages and the other for received messages.

The code

1. Create a custom triangle:

// Create a custom triangle
class Triangle extends CustomPainter {
  final Color backgroundColor;
  Triangle(this.backgroundColor);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()..color = backgroundColor;

    var path = Path();
    path.lineTo(-5, 0);
    path.lineTo(0, 10);
    path.lineTo(5, 0);
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

2. Sent message bubble:

// Sent message bubble
class OutBubble extends StatelessWidget {
  final String message;
  const OutBubble({Key? key, required this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Flexible(
          child: Container(
            padding: const EdgeInsets.all(15),
            margin: const EdgeInsets.only(bottom: 5),
            decoration: BoxDecoration(
              color: Colors.indigo.shade600,
              borderRadius: const BorderRadius.only(
                topLeft: Radius.circular(19),
                bottomLeft: Radius.circular(19),
                bottomRight: Radius.circular(19),
              ),
            ),
            child: Text(
              message,
              style: const TextStyle(color: Colors.white, fontSize: 15),
            ),
          ),
        ),
        CustomPaint(painter: Triangle(Colors.indigo.shade600)),
      ],
    );
  }
}

3. Received message bubble:

//  Received message bubble
class InBubble extends StatelessWidget {
  final String message;
  const InBubble({Key? key, required this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Transform(
          alignment: Alignment.center,
          transform: Matrix4.rotationY(math.pi),
          child: CustomPaint(
            painter: Triangle(Colors.grey.shade300),
          ),
        ),
        Flexible(
          child: Container(
            padding: const EdgeInsets.all(15),
            margin: const EdgeInsets.only(bottom: 5),
            decoration: BoxDecoration(
              color: Colors.grey.shade300,
              borderRadius: const BorderRadius.only(
                topRight: Radius.circular(19),
                bottomLeft: Radius.circular(19),
                bottomRight: Radius.circular(19),
              ),
            ),
            child: Text(
              message,
              style: const TextStyle(color: Colors.black, fontSize: 15),
            ),
          ),
        ),
      ],
    );
  }
}

4. Display multiple message bubbles by using a list view:

ListView(
            children: const [
              InBubble(message: 'Hello'),
              OutBubble(message: 'Hi there'),
              OutBubble(message: 'How it going?'),
              InBubble(message: 'Everything is OK'),
              OutBubble(message: 'Goodbye'),
              InBubble(message: 'See you soon')
            ],
          ),

5. The complete code (I put everything in a single file but you should organize each class in a separate file):

// main.dart
import 'package:flutter/material.dart';
import 'dart:math' as math;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'KindaCode.com',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Chat with KindaCode.com'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(10),
          child: ListView(
            children: const [
              InBubble(message: 'Hello'),
              OutBubble(message: 'Hi there'),
              OutBubble(message: 'How it going?'),
              InBubble(message: 'Everything is OK'),
              OutBubble(message: 'Goodbye'),
              InBubble(message: 'See you soon')
            ],
          ),
        ));
  }
}

//  Received message bubble
class InBubble extends StatelessWidget {
  final String message;
  const InBubble({Key? key, required this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Transform(
          alignment: Alignment.center,
          transform: Matrix4.rotationY(math.pi),
          child: CustomPaint(
            painter: Triangle(Colors.grey.shade300),
          ),
        ),
        Flexible(
          child: Container(
            padding: const EdgeInsets.all(15),
            margin: const EdgeInsets.only(bottom: 5),
            decoration: BoxDecoration(
              color: Colors.grey.shade300,
              borderRadius: const BorderRadius.only(
                topRight: Radius.circular(19),
                bottomLeft: Radius.circular(19),
                bottomRight: Radius.circular(19),
              ),
            ),
            child: Text(
              message,
              style: const TextStyle(color: Colors.black, fontSize: 15),
            ),
          ),
        ),
      ],
    );
  }
}

// Sent message bubble
class OutBubble extends StatelessWidget {
  final String message;
  const OutBubble({Key? key, required this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Flexible(
          child: Container(
            padding: const EdgeInsets.all(15),
            margin: const EdgeInsets.only(bottom: 5),
            decoration: BoxDecoration(
              color: Colors.indigo.shade600,
              borderRadius: const BorderRadius.only(
                topLeft: Radius.circular(19),
                bottomLeft: Radius.circular(19),
                bottomRight: Radius.circular(19),
              ),
            ),
            child: Text(
              message,
              style: const TextStyle(color: Colors.white, fontSize: 15),
            ),
          ),
        ),
        CustomPaint(painter: Triangle(Colors.indigo.shade600)),
      ],
    );
  }
}

// Create a custom triangle
class Triangle extends CustomPainter {
  final Color backgroundColor;
  Triangle(this.backgroundColor);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()..color = backgroundColor;

    var path = Path();
    path.lineTo(-5, 0);
    path.lineTo(0, 10);
    path.lineTo(5, 0);
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

Using a 3rd plugin

There are many good open-source packages that provide premade speech bubble widgets: bubble, chat_bubble, flutter_chat_bubble, etc. The example below will use the bubble package which provides a lot of options so that you can implement almost every chat style you want.

Example preview

The code

1. Installing the plugin by executing this command:

flutter pub add bubble

Then run:

flutter pub get

2. Full source code in main.dart:

// main.dart
import 'package:flutter/material.dart';
import 'package:bubble/bubble.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'KindaCode.com',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('KindaCode.com'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ListView(
          children: [
            Bubble(
              margin: const BubbleEdges.only(top: 10),
              alignment: Alignment.topRight,
              nipWidth: 8,
              nipHeight: 24,
              nip: BubbleNip.rightTop,
              color: const Color.fromRGBO(225, 255, 199, 1.0),
              child: const Text('Hello, how are you?',
                  textAlign: TextAlign.right, style: TextStyle(fontSize: 17)),
            ),
            Bubble(
              margin: const BubbleEdges.only(top: 10),
              alignment: Alignment.topLeft,
              nipWidth: 8,
              nipHeight: 24,
              nip: BubbleNip.leftTop,
              child: const Text(
                'I am fine. And you?',
                style: TextStyle(fontSize: 17),
              ),
            ),
            Bubble(
              margin: const BubbleEdges.only(top: 10),
              alignment: Alignment.topRight,
              nipWidth: 30,
              nipHeight: 10,
              nip: BubbleNip.rightTop,
              color: const Color.fromRGBO(225, 255, 199, 1.0),
              child: const Text('I am sick',
                  textAlign: TextAlign.right, style: TextStyle(fontSize: 17)),
            ),
            Bubble(
              margin: const BubbleEdges.only(top: 10),
              alignment: Alignment.topLeft,
              nipWidth: 30,
              nipHeight: 10,
              nip: BubbleNip.leftTop,
              child: const Text('Do you need help?',
                  style: TextStyle(fontSize: 17)),
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion

This article showed you two distinct ways to implement chat bubbles. In the first example, we made things from scratch and only used the built-in features of Flutter (especially the CustomPainter class). In the second example, we got what we want quickly by using a third-party plugin.

Flutter is awesome, beautiful, and interesting. If you’d like to explore new stuff about this SDK, take a look at the following articles:

You can also take a tour around our Flutter topic page and Dart topic page to see the latest tutorials and examples.

Related Articles