Stream Flutter: Building a Social Network with Flutter (3 Part Series)
Leveraging our code from part 1, we'll modify the backend to generate a Stream Chat frontend token so we can do the rest of the work in the mobile application. The Stream Chat frontend token securely authenticates a client application with the Stream Chat so it can directly talk with the API without going through our backend. This token is created and stored as a part of the login process, which is defined in section 1.
The app goes through these steps to allow a user to chat with another:
- User navigates to the user list, selects a user, and clicks "Chat". The mobile application joins a 1-on-1 chat channel between the two users.
- The 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 API.
- When the message is created, or a message from the other user is received, the mobile application consumes the event and displays the message.
Since we're relying on the Stream mobile libraries to do the heavy lifting, such as creating a WebSocket connection with the Stream API, this process is a dance between the Flutter code and native (Swift/Kotlin) via Platform Channels. If you'd like to follow along, make sure you get both the backend and mobile app running part 1 before continuing.
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
Let's get to building!
To communicate with the Stream Chat API from our mobile app, we need an authenticated frontend token. To get this, we'll add a step into our login process that requests this token, essentially replicating how we received our Stream Activity Feed API frontend token. First, in the mobile app, we add a step to the login that requests this token be generated by the
This is identical to part 1's login flow except for the
chatToken creation and
setupChat. Here we hit a separate
/v1/stream-chat-credentials to generate the token. Let's look at how the backend is implemented:
Here we use the same Stream credentials as the feed endpoint but use them to create a
StreamChat instance. Using this object, we can create a token and update that user in Stream. We return this data to the mobile app so it can store it for use.
Once we've received the token, we can set up our chat objects by telling the native code to
setupChat. Here is what's happening on the Android side:
Here we're initializing the
StreamChat singleton with our
API_KEY. We set the user on this instance, so Stream knows who's talking to the API and how to authenticate them. This sets up future calls to this library to share the configured instance.
Once we have our token, the user can navigate to the user list and indicate which user they'd like to start a chat with. To do this, we'll add a new button, "Chat", next to "Follow" that starts the conversation.
Let's look at how we add this button to this dialog:
Since we already have our dialog built, this is relatively straightforward. We simply create a
FlatButton that pushes a
Chat widget onto the navigation stack.
First, we need to create a basic layout that will display our chat messages and our input. The view looks like this:
And the Flutter code that produces this:
Here we use a simple
Scaffold layout to give us an
AppBar with a back button to exit the chat. If the
_messages variable is null, which it will be when we first navigate, we'll display a loading spinner. Once the initial messages are loaded, we show the chat widgets. For now, we're going to focus on sending a message. We'll explore how messages are loaded and displayed via the
buildMessages method later.
First, we'll need an input and a send button. Let's define
buildInput which creates the new message input widget:
This is a
Container that is a
Row containing the message input and send button. The message input is
Flexible meaning it will take up any remaining space in the
Row not taken by the other widgets, in this case, the send button. The input is bound to
_messageController which is a
TextEditingController. This stores whatever is typed in.
When a user is ready to send the message, they will press the send button which triggers
If there's text in the
_messageController, we take the text and send it to the native code with the account and user that is sending the message. The
ApiService will call the native Stream Chat library to send the message to the channel. Once that's done, we clear the text to be ready for the next message.
Let's take a look at what's happening in
We simply send this message, along with the
chatToken created during login, via the platform channel. The native side, implemented in Kotlin, is where we interact with Stream to send the message:
We start by grabbing the
StreamChat instance that we created during login via
setupChat. We connect to our channel, which is a
messaging channel type. The channel id is the two user's ids sorted and joined together with
-. So if we're
sara chatting with
jeff our channel id is
jeff-sara. Using a unique, consistent id is essential, so our users are joining the correct channels.
Once we've got the correct channel initialized, we create a Stream
Message add the text and send it to the channel. Stream is smart enough to lazily generate the channel if it does not exist when we send our first message. On success, we simply respond true. We could choose to send the message back to the Flutter side for display, but since we're listening to the Stream WebSocket for new messages, we'll see our new message come across there. We'll explore how this is done next.
If you're confused about how we call native code from Flutter, please refer to the Flutter documentation.
Chat widget first loads, we want to grab the initial messages from the channel. We'll trigger the message loading in
Here we call the
ApiService to start a connection with the channel. The
listenToChannel method will first send along historical messages to our callback then any subsequent messages that come across until we tell it to stop, whenever a new set of messages is generated. This happens whenever either user creates a message.
This widget responds to new messages by setting a new state by merging the previously displayed messages with any new messages that have been sent along.
This stream is open until we explicitly cancel the channel connection. The
listenToChannel method returns a cancel function. We store this and call it when we dispose of the widget:
Let's look at how we implement the
First, we call the native side to set up the channel. We'll look at this in a second, but it essentially starts the WebSocket connection with Stream's API and returns the channel id. Using this channel id, we bind to a Flutter EventChannel. This class allows us to create an event stream between the native code and our flutter code. We bind to the broadcast stream under that channel id and start it by calling
listen. As messages come in, the native code will request our callback with a JSON string. We decode the results and pass them along to the
listener callback given to this method.
For the caller code to tell us when to cancel this subscription, we return a small void function which wraps our
Now we go to the native Android implementation of
setupChannel to see how we start a connection with Stream and listen for new messages.
A lot is going on here, but we'll walk through it step by step. Essentially, this function does four things:
1) Set up a Flutter Java EventChannel.
2) Tells Stream that we're going to watch this channel (by starting a WebSocket connection) and sends the first page of messages to the event sink.
3) Binds to the channel's new messages event stream and sends them to the
4) Handles when the user cancels this stream and cleans everything up.
Walking through the code, the first thing the code does is sets up the channel with the correct
channelId using our
StreamChat singleton. We then build our Flutter
EventChannel using that
channelId so we can stream messages (events) back to the dart side. We store this so we can later unbind our event listeners.
To keep things all in one place, the code uses object expressions to instantiate objects of an anonymous class.
The call to
setStreamHandler takes an object that implements
EventChannel.StreamHandler. This requires two callback methods
onListen happens when the
EventChannel is listened to, which occurs on the dart side (shown above in the
onCancel is called when the listener tells us to stop (also shown above in
Looking at the
onListen method, we see this is where we tell the channel we'd like to watch it. This method does two things. First, it informs Stream that we're going to bind to the channel and listen for any new messages. It also queries the last set of messages and calls our
QueryWatchCallback object's method
onSuccess with those messages. We send these messages along to our
eventSink established by our
EventChannel, which streams them to the dart code.
After we've told Stream that we're listening, we then need to bind to the channel's event stream. We add an event handler that implements
ChatChannelEventHandler. We override the one event we care about, which is
onMessageNew. Whenever we get a new event, we pull the message off and send it to the
When the dart side has decided to cancel the stream, the
onCancel method will be called. We tell the channel we're done by calling
stopWatching, remove the event handler, and forget about the
Now that we're streaming messages to our Flutter code and storing them in
_messages, we can show them to the user. First, we build a
ListView that contains our messages:
We reverse the list to show the most recent message on the bottom. This also makes it so the
ListView scrolls to the bottom of the list by default, which is the messaging UX users expect. Each message is then it's own
Widget that shows our messages on the right in a blue bubble and the other user's messages in a grey bubble on the left.
The final result will look like this:
And that's it! You now have basic direct messaging between users.
Now our app has the ability for a user to post updates to their activity stream and direct message other users. Using both the Activity Feed and Chat products from Stream allowed us to build this with virtually no backend.
A big part of Flutter's power is the ability to leverage native iOS and Android libraries easily. You can get more for free by integrating Stream's out the box views. Utilizing Flutter's AndroidView or UiKitView we can embed Stream Chat Android's [https://github.com/GetStream/stream-chat-android/blob/master/docs/MessageList.md]. Keep in mind this level of integration is still considered in preview and is quite expensive, but it provides an even faster way to get up and running with Stream and Flutter.
Stream is also working hard, building a pure dart library. While it's not ready yet, be sure to check back and see when it's available for production!
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.
Make better choices about your code and your career.