TUTORIAL

Build a Flutter Chat App With The Stream SDK

Build a mobile chat application similar to Facebook Messenger or WhatsApp using Stream’s Flutter Chat SDK library. By the end of this tutorial, you will have a fully functioning mobile app with support for rich messages, reactions, threads, image uploads and videos.

We are also going to show how easy it is to make customizations to the built-in widgets that ship with this library and their styling.

Looking for more? We also recently published articles on Stream Chat Push Notifications, Comparing Chat API Pricing; and for feeds please check out our React Native Activity Feeds which details using our Activity Feed React Components.

Flutter Chat SDK Setup

First of all we need to setup our new project and have a working development environment for Flutter. If you do not have Flutter or an IDE configured to work with it, we highly recommend following the Install and Set up an editor steps from the official documentation.

Please make sure you are using the latest version of Flutter from the stable channel:

flutter channel stable
flutter upgrade

Now open your IDE and start a new Flutter application called awesome_flutter_chat. If you are using Android Studio (recommended) make sure to create the project as a Flutter application and keep all default settings.

Next step is to add stream_chat_flutter to your dependencies, to do that just open pubspec.yaml and add it inside the dependencies section.

dependencies:
  flutter:
    sdk: flutter

  stream_chat_flutter: ^0.2.0

After saving the file Android Studio will show a prompt, make sure to pick “Packages get” to have all dependencies installed.

Add Stream Chat to your Flutter application

Let’s start by adding the top-level Chat widget and initialize your application. There are three important things to notice that are common to all Flutter application using StreamChat:

  1. The Dart API client is initialized with your API Key
  2. The current user is set by calling setUser on the client
  3. The client is then passed to the top-level StreamChat widget

StreamChat is an inherited widget and must be the parent of all Chat related widgets, we will cover later how you can leverage the inherited widget APIs to build more advanced customizations to your application.

Let’s open the main.dart file and replace its content with this:

import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

void main() async {
  final client = Client(
    'b67pax5b2wdq',
    logLevel: Level.INFO,
  );

  await client.setUser(
    User(
      id: 'empty-smoke-2',
      extraData: {
        'image': 'https://getstream.io/random_png/?id=empty-smoke-2&name=Empty+smoke',
      },
    ),
    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZW1wdHktc21va2UtMiJ9.W8z5ag_kBMIDpRG5AGDoZ31x2-csQQ0erIFBer6sjMw',
  );

  final channel = client.channel('messaging', id: 'godevs');

  // ignore: unawaited_futures
  channel.watch();

  runApp(MyApp(client, channel));
}

class MyApp extends StatelessWidget {
  final Client client;
  final Channel channel;

  MyApp(this.client, this.channel);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        return StreamChat(
          child: widget,
          client: client,
        );
      },
      home: StreamChannel(
        channel: channel,
        child: ChannelPage(),
      ),
    );
  }
}

class ChannelPage extends StatelessWidget {
  const ChannelPage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ChannelHeader(),
      body: Column(
        children: <Widget>[
          Expanded(
            child: MessageListView(),
          ),
          MessageInput(),
        ],
      ),
    );
  }
}

Please note that while Flutter can be used to build both mobile and web applications; in this tutorial we are going to focus on mobile, make sure when running the app you use a mobile device.

Let's have a look at what we've built so far:

  • We set up the Chat Client with the API key
  • We set the the current user for Chat with Client.setUser and a pre-generated user token
  • We make StreamChat the root Widget of our application
  • We create a single ChannelPage widget under StreamChat with three widgets: ChannelHeader, MessageListView and MessageInput
If you now run the simulator you will see a single channel UI.

Rich Messaging

The widgets we have dropped on our code earlier provide several rich interactions out of the box.

  1. URL previews
  2. User mentions
  3. Chat commands
  4. Image uploads

Multiple Conversations

Most chat applications handle more than just one single conversation. Apps like Facebook Messenger, Whatsapp and Telegram allows you to have multiple one to one and group conversations.

Let’s find out how we can change our application chat screen to display the list of conversations and navigate between them.

Note: the SDK uses Flutter’s Navigator to move from one route to another, this allows us to avoid any boiler-plate code. Of course you can take total control of how navigation works by customizing widgets like Channel and ChannelList.

Let’s open again main.dart and make some small changes:

import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

void main() async {
  final client = Client(
    'b67pax5b2wdq',
    logLevel: Level.INFO,
  );

  await client.setUser(
    User(
      id: 'empty-smoke-2',
      extraData: {
        'image': 'https://getstream.io/random_png/?id=empty-smoke-2&amp;name=Empty+smoke',
      },
    ),
    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZW1wdHktc21va2UtMiJ9.W8z5ag_kBMIDpRG5AGDoZ31x2-csQQ0erIFBer6sjMw',
  );

- final channel = client.channel('messaging', id: 'godevs');

- // ignore: unawaited_futures
- channel.watch();

- runApp(MyApp(client, channel));
+ runApp(MyApp(client));
}

class MyApp extends StatelessWidget {
  final Client client;
- final Channel channel;

- MyApp(this.client, this.channel);
+ MyApp(this.client);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
-     home: StreamChat(
-       client: client,
-       child: StreamChannel(
-       channel: channel,
-         child: ChannelPage(),
-       ),
-     ),
+     builder: (context, child) => StreamChat(
+       client: client,
+       child: child,
+     ),
+     home: ChannelListPage(),
    );
  }
  
}

+ class ChannelListPage extends StatelessWidget {
+   @override
+   Widget build(BuildContext context) {
+     return Scaffold(
+       body: ChannelsBloc(
+         child: ChannelListView(
+           filter: {
+             'members': {
+               '\$in': [StreamChat.of(context).user.id],
+             }
+           },
+           sort: [SortOption('last_message_at')],
+           pagination: PaginationParams(
+             limit: 20,
+           ),
+           channelWidget: ChannelPage(),
+         ),
+       ),
+     );
+   }
+ }

class ChannelPage extends StatelessWidget {
  const ChannelPage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ChannelHeader(),
      body: Column(
        children: <Widget>[
          Expanded(
            child: MessageListView(),
          ),
          MessageInput(),
        ],
      ),
    );
  }
}

If you run your application now, you will see that the first screen shows a list of conversations, you can open each by tapping and go back to the list.

Every single widget involved in this UI can be customized or swapped with your own.

The ChannelListPage widget retrieves the list of channels based on a custom query and ordering using the ChannelsBloc inherited widget. In this case we are showing the list of channels the current user is a member and we order them based on the time they had a new message. ChannelListView handles pagination and updates automatically out of the box when new channels are created or when a new message is added to a channel.

Use our example app on Codepen to interact with your Flutter app and see how the ChannelListPage handles new messages:

Customize Channel Preview

So far you’ve learned how to use the default widgets. The library has been designed with composition in mind and to allow all common customizations to be very easy. This means that you can change any component in your application by swapping the default widgets with the ones you build yourself.

Let’s see how we can make some changes to the SDK’s UI components. We will start by changing how channel previews are shown in the channel list and include the number of unread messages for each.

We're passing a custom widget to channelPreviewBuilder in the ChannelListView, this will override the default ChannelPreview and allows you to create one yourself.

class ChannelListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ChannelsBloc(
        child: ChannelListView(
          filter: {
            'members': {
              '\$in': [StreamChat.of(context).user.id],
            }
          },
+         channelPreviewBuilder: _channelPreviewBuilder,
          sort: [SortOption('last_message_at')],
          pagination: PaginationParams(
            limit: 20,
          ),
          channelWidget: ChannelPage(),
        ),
      ),
    );
  }

+ Widget _channelPreviewBuilder(BuildContext context, Channel channel) {
+   final lastMessage = channel.state.messages.reversed
+       .firstWhere((message) => !message.isDeleted);
 
+   final subtitle = (lastMessage == null ? "nothing yet" : lastMessage.text);
+   final opacity = channel.state.unreadCount > .0 ? 1.0 : 0.5;
 
+   return ListTile(
+     leading: ChannelImage(
+       channel: channel,
+     ),
+     title: ChannelName(
+       channel: channel,
+       textStyle:
+           StreamChatTheme.of(context).channelPreviewTheme.title.copyWith(
+                 color: Colors.black.withOpacity(opacity),
+               ),
+     ),
+     subtitle: Text(subtitle),
+     trailing: channel.state.unreadCount > 0
+         ? CircleAvatar(
+             radius: 10,
+             child: Text(channel.state.unreadCount.toString()),
+           )
+         : SizedBox(),
+   );
+ }
}

There are a couple interesting things we do in this widget:

  • Instead of creating a whole new style for the channel name, we inherit the text style from the parent theme (StreamChatTheme.of) and only change the color attribute
  • We loop over the list of channel messages to search for the first not deleted message(channel.state.messages)
  • We retrieve the count of unread messages from channel.state

Use the button below to open the same chat on your browser and post a message, you will see how the new channel preview changes when messages are added or deleted.

Message Threads

Stream Chat supports message threads out of the box. Threads allows users to create sub-conversations inside the same channel.

Using threaded conversations is very simple and mostly a matter of plugging the MessageListView to another widget that renders the widget. To make this simple, such a widget only needs to build MessageListView with the parent attribute set to the thread’s root message.

Here’s how the builder and the change to the channel page looks like:

class ChannelPage extends StatelessWidget {
  const ChannelPage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ChannelHeader(),
      body: Column(
        children: <Widget>[
          Expanded(
-            child: MessageListView(),
+            child: MessageListView(
+             threadBuilder: (_, parentMessage) {
+               return ThreadPage(
+                 parent: parentMessage,
+               );
+             },
+           ),
          ),
          MessageInput(),
        ],
      ),
    );
  }
}

+ class ThreadPage extends StatelessWidget {
+   final Message parent;
 
+   ThreadPage({
+     Key key,
+     this.parent,
+   }) : super(key: key);
 
+   @override
+   Widget build(BuildContext context) {
+     return Scaffold(
+       appBar: ThreadHeader(
+         parent: parent,
+       ),
+       body: Column(
+         children: <Widget>[
+           Expanded(
+             child: MessageListView(
+               parentMessage: parent,
+             ),
+           ),
+           MessageInput(
+             parentMessage: parent,
+           ),
+         ],
+       ),
+     );
+   }
+ }

Now we can open threads and create new ones as well, if you long press a message you can tap on Reply and it will open the same ThreadPage.

Custom Message

Customizing how messages are rendered is another very common use-case that the SDK supports easily.

Replace the built-in message component with your own is done by passing it as a builder function to the MessageListView widget.

The message builder function will get the usual context argument as well as the message object and its position inside the list.

class ChannelPage extends StatelessWidget {
  const ChannelPage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ChannelHeader(),
      body: Column(
        children: <Widget>[
          Expanded(
            child: MessageListView(
              threadBuilder: (_, parentMessage) {
                return ThreadPage(
                  parent: parentMessage,
                );
              },
+             messageBuilder: _messageBuilder,
            ),
          ),
          MessageInput(),
        ],
      ),
    );
  }

+ Widget _messageBuilder(
+   BuildContext context,
+   MessageDetails details,
+   List<Message> messages,
+ ) {
+   final message = details.message;
+   final isCurrentUser = StreamChat.of(context).user.id == message.user.id;
+   final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
+   final color = isCurrentUser ? Colors.blueGrey : Colors.blue;

+   return Padding(
+     padding: EdgeInsets.all(5.0),
+     child: Container(
+       decoration: BoxDecoration(
+         border: Border.all(color: color, width: 1),
+         borderRadius: BorderRadius.all(
+           Radius.circular(5.0),
+         ),
+       ),
+       child: ListTile(
+         title: Text(
+           message.text,
+           textAlign: textAlign,
+         ),
+         subtitle: Text(
+           message.user.extraData['name'],
+           textAlign: textAlign,
+         ),
+       ),
+     ),
+   );
+ }
}

If you look at the code you can see that we use StreamChat.of to retrieve the current user so that we can style own messages in a different way.

Since custom widgets and builders are always children of StreamChat or part of a channel, you can use StreamChat.of, StreamChannel.of and StreamTheme.of to use the API client directly or to retrieve outer scope needed such as messages from the channel.state.

Custom Styles

The Flutter SDK comes with a fully designed set of widgets which you can customize to fit with your application style and typography. Changing the theme of Chat widgets works in a very similar way that MaterialApp and Theme do.

Out of the box all chat widgets use their own default styling, there are two ways to change the styling:

  1. Initialize the StreamChatTheme from your existing MaterialApp style
  2. Construct a custom theme ad provide all the customizations needed

Let’s first see how we can re-use an existing material design style to change the styling of your chat UI.

First we create a new Material Theme and pick green as swatch color. The theme is then passed to MaterialApp as usual.

class MyApp extends StatelessWidget {
  final Client client;

  MyApp(this.client);

  @override
  Widget build(BuildContext context) {
+   final theme = ThemeData(
+     primarySwatch: Colors.green,
+   );

    return MaterialApp(
+     theme: theme,
      builder: (context, child) => StreamChat(
        client: client,
        child: child,
      ),
      home: ChannelListPage(),
    );
  }
}

class ChannelPage extends StatelessWidget {
  const ChannelPage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ChannelHeader(),
      body: Column(
        children: <Widget>[
          Expanded(
            child: MessageListView(
-             messageBuilder: _messageBuilder,
            ),
          ),
          MessageInput(),
        ],
      ),
    );
  }
}

With the next step we create a new StreamChatTheme from the green theme we just created. After saving the app you will see the UI will update several widgets to match with the new color.

class MyApp extends StatelessWidget {
  final Client client;

  MyApp(this.client);

  @override
  Widget build(BuildContext context) {
    final theme = ThemeData(
      primarySwatch: Colors.green,
    );

    return MaterialApp(
      theme: theme,
      builder: (context, child) => StreamChat(
+       streamChatThemeData: StreamChatThemeData.fromTheme(theme),
        client: client,
        child: child,
      ),
      home: ChannelListPage(),
    );
  }
}

Let’s now do something more complex such as changing the message color posted by the current user. You can perform these more granular style changes using StreamChatTheme.copyWith.

class MyApp extends StatelessWidget {
  final Client client;

  MyApp(this.client);

  @override
  Widget build(BuildContext context) {
    final theme = ThemeData(
      primarySwatch: Colors.green,
    );
    return MaterialApp(
      theme: theme,
      builder: (context, child) => StreamChat(
-       streamChatThemeData: StreamChatThemeData.fromTheme(theme),
+       streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
+         ownMessageTheme: MessageTheme(
+           messageBackgroundColor: Colors.black,
+           messageText: TextStyle(
+             color: Colors.white,
+           ),
+           avatarTheme: AvatarTheme(
+             borderRadius: BorderRadius.circular(8),
+           ),
+         ),
+       ),        
        client: client,
        child: child,
      ),
      home: ChannelListPage(),
    );
  }
}

Flutter Chat Tutorial – Final thoughts

We hope that you enjoyed this tutorial. By using Stream’s Chat Components, you and your team will be able to get your Flutter application, with chat, up and running in minutes.

Now that you’ve completed the tutorial on Stream Chat, you can build anything chat related with our components. If you have a use-case that doesn’t quite seem to work, or simply have questions, please don’t hesitate to reach out here.

Next Steps