Stream Flutter: Building a Social Network with Flutter – Group Channels

In the third part of our series, we're building group chat into our social application. This allows users to chat with multiple people at the same time. We leverage Stream Chat to do the heavy lifting. This post assumes you've gone through part 1 and part 2.

Using our code from part 2, we only focus on the Flutter application, since our backend gives us everything we need already. To recap, the backend generates a frontend token for Stream Chat, which allows the Flutter application to communicate directly with the Stream Chat API. Also, since we have direct messaging implemented, there are no additional libraries. The previously installed Stream Chat Android and Swift libraries are all we need.

The app goes through these steps to enable group chat:

  • User navigates to a list of chat channels they can join. To start, there will be none, so they must create the first one.
  • The user hits "Create Chat Channel" and generates a chat channel with an id.
  • The mobile app queries the channel for previous messages and indicates to Stream that we'd like to watch this channel for new messages. The mobile app listens for new messages.
  • The user creates a new message and sends it to the Stream Chat API. Stream broadcasts this message to all users watching that channel.
  • When the message is broadcast, including messages created by the user, the mobile application consumes the event and displays the message.

We rely on Stream's Android/Swift libraries to do most of the work communicating with the API. This is done by leveraging Flutter's (Swift/Kotlin) Platform Channels to communicate with native code (Kotlin/Swift). If you'd like to follow along, make sure you get both the backend and mobile app running part 2 before continuing.

Prerequisites

Basic knowledge of Node.js (JavaScript), Flutter (Dart), and Kotlin, is required to follow this tutorial. Knowledge of Swift is useful if you want to browse the iOS implementation. This code is intended to run locally on your machine.

If you'd like to follow along, you'll need an account with Stream. Please make sure you can run a Flutter app, at least on Android. If you haven't done so, make sure you have Flutter installed. If you're having issues building this project, please check if you can create run a simple application by following the instructions here.

Once you have an account with Stream, you need to set up a development app (see part 1:

You'll need to add the credentials from the Stream app to the source code for it to work. See both the mobile and backend READMEs.

First, we'll explore how a user creates a group channel.

Step 1: Navigation

To start, we add a new navigation item to the bottom bar:

To do this, in main.dart we simply add a new BottomNavigationItem:

https://gist.github.com/nparsons08/83c8edd64aaa02d090ce125761fd1131

and the corresponding widget to boot when the user selects that item:

https://gist.github.com/nparsons08/9eddae3f73d6fb0ae0d28f41131c30f4

This boots the Channels widget that shows a list of channels and allows the user to create a new one.

Step 2: Creating a group channel

When the user first arrives at this screen, it will be empty if no one else has created any channels. We'll add a new channel button to the widget. Since this will be a list of group channels, we'll use a ListView with a single item, our new button, in it for now. We'll talk about how the FutureBuilder and RefreshIndicator with the _channel state in a bit. Here is the structural code with the "New Channel" button:

https://gist.github.com/nparsons08/87ebffcf88ac72b700cd0d02c0551ca3

Our first list item is a button. When the user clicks the button, we navigate to a new widget called NewChannel. We check the return value of Navigator.push to check channel creation. If it were created, we'd refresh the channel list (we'll look at it in a bit).

Upon navigating, the user sees a form to create the channel. This is a simple widget where the user types in a channel id and creates the channel:

Let's look at the widget definition:

https://gist.github.com/nparsons08/8de7b480c692d2a6e804771154346a94

Here we see a simple Flutter form, backed with a TextEditingController. The first thing is to check is the text is a valid channel id. Stream Chat has rules around what a channel id can look like, and for simplicity, we'll just create and list channels by this id. You can refer to the docs if you'd like to add a separate channel name.

Once a user submits a channel id, we simply navigate to the LivestreamChannel widget. Notice we don't create a channel in Stream here. Stream lazily creates channels upon our first interaction with them. The LivestreamChannel will query and watch the channel, which will force its creation. Also, we use the name "Livestream" to mirror the type of channel we'll using in Stream. Livestream is the default channel type we want since, in part 4, we'll implement live video into our group channel. If none of the default types of work for your application, you can create your channel types.

Here is what the user sees when first joining a group channel:

This is the most complex widget, so we'll go through this in small chunks. Remember to refer to the source if you need to see the entire file. First, let's look at our build method to see how we're laying out our view:

https://gist.github.com/nparsons08/26b2c80e1672e70b41d84766c4be6d60

This is a simple scaffold that shows the id of the channel at the top and two pieces, the message list, and the new message input. When we initialize this widget, we listen to the channel, very similar to how we listened to direct message channels in part 2. We do this in the initState method:

https://gist.github.com/nparsons08/2a92f1a221273111251f8c15c01530fa

We call the method .listenToChannel on the ApiService. This sets queries and watches the corresponding Stream channel. This means that it will give the initial set of messages and subsequent messages to us. Every time we receive messages, we merge them into the previously displayed set. We'll see how to display these messages in a few steps.

We also store a cancelChannel function which allows the widget to stop listening once it's disposed of:

https://gist.github.com/nparsons08/5295b6d9b7e9396f7d2753b094a403ea

This is important. Otherwise, we'd have strange behavior due to orphaned listeners hanging around. Let's look at the implementation of .listenToChannel:

https://gist.github.com/nparsons08/2f15963b884524ab2889099c1ab06c1f

This is identical to how we set things up in part 3 except for the channel id. Since the user gives us an ID, we don't need to generate one. This method tells the native side to set up the channel with Stream and starts an EventChannel with that channel id. Once that's done, we subscribe to the EventChannel, which allows the native side to stream messages to us. We take that stream, listen to it, and parse any results that come across and pass them along to the widget.

Next, we go to our setupChannel implementation in Kotlin. This method coordinates with Stream, establishes a channel connection, and creates an event stream to send data back to the Flutter side:

https://gist.github.com/nparsons08/3b42664b26216e7ac5151b2bf9ff952e

This code is what communicates with Stream. First, we create a Channel object with the type livestream and our channel id. As described before, livestream is the appropriate default channel type for our group chat. It allows any user to join the channel and chat with others.

Next, we start a Flutter EventChannel in Kotlin. This allows us to stream data back to the Flutter side. In our .onListen method, which is called when the Flutter side subscribes to the EventChannel, we query the channel for the initial set of messages and tell Stream to watch for future messages. This initial query will create the channel in Stream if it doesn't exist. The initial set of messages will trigger our QueryChannelCallback, and they've sent over the EventChannel as a JSON string.

To receive future messages, we need to register an event handler with the channel. This is done by calling channel.addEventHandler. Since we indicated we'd like to watch the channel when we did our initial query, any future messages will be sent to our ChatChannelEventHandler callback. We send these over the EventChannel as a JSON string, just like above.

When the Flutter side indicates they'd like to cancel, the .onCancel is called. We simply stop watching and clean up our event handlers. Now the user is ready to send their first message.

Step 3: Sending a message

First, we'll need an input and submit button:

https://gist.github.com/nparsons08/361c0e9a0189a7ec1a93a70bc2268c34

This looks hairy, but all that's happening is a simple, flexible layout with two elements, an input box and a submit. The submit button is the size of the icon, and the text input is adjustable, which allows it to take up the remaining space. We also wrap it in a Container with some margin to avoid phone-specific features that take up bottom real estate, such as the home indicator on iOS.

When the user has typed a message and hit submit, we're ready to send the message to the Stream channel. Here's the implementation of _sendMessage:

https://gist.github.com/nparsons08/05c4f26ce37da8e9b3009b7f86dfdba4

This checks the length and passes it along to the API method .postChannelMessage:

https://gist.github.com/nparsons08/ae9207fbaac415b3b08585ee10e5fff9

Which then passes it to the Kotlin method .postChannelMessage:

https://gist.github.com/nparsons08/99a031b61f28c1e52741e528af8888c9

Here we create a Stream Message and send it to the channel. There's not a lot for our code to do since Stream's libraries take care of the work. We simply return true when Stream indicates success via our callback.

Step 4: Viewing messages

We're finally ready to view all our hard work. Since we're already listening to the channel, which was set up in part 2, all we need to do is display the messages. We create a list view that takes up the rest of the space not taken by our create message input:

https://gist.github.com/nparsons08/005e2ab52b63531fba26c5e594991cc9

This creates iOS message style bubbles with our messages on the right in blue and other user's messages on the left in grey with their name attached to the message. We reverse the list, so the most recent messages are on the bottom:

Step 5: Viewing other channels

Now a user can go back to the channel list and view all the channels, including those created by other users. Let's revisit the widget we started in Step 1 and update it, so it queries and displays the list of channels:

All that's left to get this widget fully functional is plugging in the calls to ApiService.channels and adding those to our list view. First, when we first boot the widget in initState, we need to load the channels:

https://gist.github.com/nparsons08/271a38b59194a85d12ee502314054fd8

The ApiService.channel call simple calls to do the native side and decodes the result:

https://gist.github.com/nparsons08/3369e54dc091f81ba80a58ccadbd285c

The native implementation to retrieve channels is a similar pattern to how we query for messages above:

https://gist.github.com/nparsons08/1dd20392cbf052273ee740f06494a8bc

We ask the Stream library to query all livestream channels. We only look for the livestream, so we don't list the private direct messaging channels. Once the library has returned this data to us, we pass it along as a JSON string to the Flutter side.

Since our ApiService is returning a Future that will contain channels, we plug this Future into a FutureBuilder to deal with the state changes:

https://gist.github.com/nparsons08/98fd4593316f0c7bf3f4c0c0b91b5d70

Once that data loads, we can use the snapshot.data from the FutureBuilder to add to our ListView inside our .build method:

https://gist.github.com/nparsons08/4b63430c513d2acad838c375f9cf8e53

When we click on a ListTile, we simply navigate to the appropriate LivestreamChat widget. Now a user will see all channels and be able to chat in any of them.

The last remaining piece is to plug in the ApiService.channels call in where we need to refresh the list. The RefreshIndicator calls the method _refreshChannels when a user pulls to refresh. We also call this when we create a channel successfully. Here's the implementation of _refreshChannels:

https://gist.github.com/nparsons08/513e5cb2e07fa18f06f04e6cd9503f03

This method simply sets the state to a new Future that will complete with the latest list of channels. Once that ends, we'll see the updated list.

That's it! We now have a fully functioning group chat via Stream Channels!

In addition to the tutorial above, Stream offers a comprehensive Flutter SDK Tutorial, so you can see how to get up and running quickly with Flutter.

TutorialsChat