Tutorial: Build chat apps with PubNub using Flutter

This tutorial shows you how to build a scalable chat app with PubNub using Flutter.

Step 01

Build a Chat App with the Flutter Chat SDK

This tutorial will walk you through developing a basic chat application in Flutter using PubNub Dart SDK.

The source code for this application can be found on Github at https://github.com/PubNubDevelopers/GettingStarted-Flutter-SDK-Tutorial

Prerequisites

Before running this application, be sure you have the following installed:

  • Flutter 

  • Any Flutter dependencies.  For example, to target Android you will need the Android toolchain installed.  Run “flutter doctor” to make sure you have everything you want installed correctly.

Download Source Application

In a terminal, navigate to an empty folder.  Enter the following command to clone the repository:

git clone https://github.com/PubNubDevelopers/GettingStarted-Flutter-SDK-Tutorial.git

And navigate to the application folder:

cd GettingStarted-Flutter-SDK-Tutorial cd flutter_sdk_tutorial

Important: Install the flutter packages that this app needs

flutter packages get

The main git branch contains the completed version of this tutorial.  If you would like to follow along, please checkout the following branch:

git checkout tutorial_step_0

Step 02

Create keys

Demo keys

You're ready to go!

We’ve assigned demo keys which are set up to support the features of this tutorial app. These will be active for 72 hours but you can sign up for a free account at the end of the tutorial to create your own permanent keys.

The demo keys have the following functionality activated ready for your application:

  • Presence

  • Message Persistence You will be able to see user’s message history and meta data.

  • Stream Controller You will be able to receive messages against a group of channels, or post one message to multiple channels.

  • App Context This allows you to manage the metadata of your users and channels.

My own keys

You'll need to create an account to use your own keys

  1. Sign in to your PubNub Dashboard.

  2. Go to Keysets using the left hand side navigation.

  3. Click the “Create New Keyset” button on the upper right hand-side of the portal.

  4. A pop-up appears. Give the keyset a name and click the “Create” button.

  5. Click on the newly created keyset.

  6. Enable four features for the keyset to be able to connect to the PubNub Network: Presence, Message Persistence, Stream Controller and App Context.

  7. Enable Presence by clicking on the toggle to turn it on. A pop-up will require that you enter in “ENABLE”. Enter in “ENABLE” in all caps and then press the “Enable” button. You can leave the default settings.

  8. Enable Message Persistence by clicking on the toggle to turn it on. You can leave the default settings.

  9. Enable Stream Controller by clicking on the toggle to turn it on if it is already not enabled. You can leave the default settings.

  10. Enable App Context by clicking on the toggle to turn it on if it is not already enabled. Select the region closest to you to store your context.

  11. Click on the “Save Changes” button in the bottom right corner of the portal. A confirmation message will appear in the upper-left corner of the portal confirming that these changes have been saved. 

  12. Scroll back to the top of the portal. Copy the Publish key by clicking on the copy icon and paste it below

  13. Copy the Subscribe Key by clicking on the copy icon and paste it below

Step 03

Build and run

Watch the video or follow the steps below to build and run your application.

JavaScript
//This code box will update soon as you
//progress through the video
//Use the copy icon in the top right to quickly copy
//code as you're following along

Step 1. Add PubNub Keys

Open the keys.dart file (under \lib\utils\) and enter the publish and subscribe keys at the relevant location

java
static const pubnubPublishKey =
      "REPLACE WITH YOUR PUBNUB PUBLISH KEY"; //    <--  PubNub Keys go here
static const pubnubSubscribeKey =
      "REPLACE WITH YOUR PUBNUB SUBSCRIBE KEY"; //  <--  PubNub Keys go here

Step 2. Adding chat to your application with PubNub

The application on the main branch is fully functional and contains all the changes described in the following steps.

If you want to follow along with the tutorial and build the functionality into a templated version of the app, please checkout the following branch:

shell
git checkout tutorial_step_0

To run your application at any point use the following:

shell
flutter run

Or you can specify a device to run on, for example to run in Chrome you can specify:

shell
flutter run -d chrome

Step 2a. Creating the PubNub object

Before interacting with PubNub, you first need to create a PubNub configuration instance.  This requires 3 things, the publish and subscribe keys that were created in the previous step and a device identifier.

The device identifier you pass in should represent a unique instance of the user or device that connects to PubNub.  If you maintain your own user registration workflow then you could use whichever unique identifier you assigned the user during registration.  If your users do not already have an assigned ID, as in the case of this app, you should assign a device-specific identifier. 

Obtaining a device-specific identifier requires platform-specific code but there is a useful Flutter plugin that does this for us, platform_device_id.  Since PubNub device Ids should be alphanumeric, it is also best practice to strip out any non alphanumeric characters such as hyphens from this value.  On browser-based apps platform_device_id will return the user agent which does not suit our purposes since it is not unique to a user; in this case the tutorial app will store a random Id in local storage.

java
deviceId = await PlatformDeviceId.getDeviceId;

The PubNub object is instantiated in the pubnub_instance.dart file

java
_pubnub = PubNub(
      defaultKeyset: Keyset(
          subscribeKey: AppState.pubnubSubscribeKey,
          publishKey: AppState.pubnubPublishKey,
          userId: UserId(AppState.deviceId!)));

Step 2b. Subscribing to a channel & considerations on mobile

The concepts of publish and subscribe are fundamental to PubNub where messages are sent from a publishing source and received by subscribers over a channel.  You do not need to create the channel separately, it will be created automatically for you by PubNub whenever the first user publishes or subscribes to it.

Despite the apparent simplicity, the pub/sub model is able to accommodate the most complex of communication use cases.  

To keep this application simple we will only have a single channel to represent the application’s single "chat room".  The application will subscribe to receive messages from the channel that have been published by other users.

On mobile applications, you cannot rely on your application being active in the background so most chat developers will take a two stage approach:

  1. Subscribe to receive messages when the app comes to the foreground

  2. Unsubscribe when the app goes into the background

  3. Receive a mobile push notification whilst in the background to notify the user that a message is available.  This is outside of the scope of this tutorial but please see our Push Notifications tutorial for more information.

Since we want to receive presence events when users join or leave the channel, specify ‘withPresence’ to be true.

Create the Subscription object also in pubnub_instance.dart:

java
_subscription =
    _pubnub.subscribe(channels: {AppState.channelName}, withPresence: true);

If you are familiar with other PubNub SDKs such as our JavaScript SDK you may be used to the SDK having built-in presence management.  Because Dart is cross platform & supports multiple targets it does not have presence management built-in but can be easily made presence-aware using the following API calls which are part of the Dart SDK:

  • announceLeave(channels), to notify PubNub that the user is leaving the channel.

  • announceHeartbeat(channels), to notify PubNub that the user is present.

The tutorial maintains application lifecycle using a WidgetsBindingObserver.  In the conversation.dart file, add the following to update the user’s presence when the application comes into or goes out of the foreground:

java
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
   if (state == AppLifecycleState.paused) {
     //  Mobile only: App has gone to the background
     messageProvider.pubnub.announceLeave(channels: {AppState.channelName});
     messageProvider.subscription.pause();
   } else if (state == AppLifecycleState.resumed) {
     //  Mobile only: App has returned to the foreground
     messageProvider.subscription.resume();
     messageProvider.pubnub
         .announceHeartbeat(channels: {AppState.channelName});
   }
 }

Step 2c. Publishing messages to a channel

Messages published to a channel will be received by all subscribers on that channel. 

The message payload is a String but you can also optionally add custom ‘meta’ information to the message as a JSON object. 

Add this code to the send button click handler in messages.dart (a provider for message state):

java
await pubnub.publish(channel, message);

Step 2d. Listening for PubNub events

PubNub event listeners  are used to receive:

  • Messages: A message has been received.

  • Presence information: A user has joined or left a channel.

  • Signals: A signal has been received.

  • App Context: PubNub's serverless storage has been updated

  • Message Reactions: Used to modify messages after they are published, for example adding an emoji reaction.

App Context requires the 'App Context' feature to be enabled on the keyset, which was done during the ‘Create Keys’ section of this tutorial.  

This tutorial only uses a subset of the possible listeners which will be populated in upcoming steps.

The Dart SDK separates listeners into two categories, ‘presence’ to handle presence events and ‘messages’ to handle other types of events. 

The presence listener is defined as part of the ‘presence provider’, in presence.dart:

java
chatSubscription.presence.listen((presenceEvent) {
  switch (presenceEvent.action) {
      //  todo  
      break;
    default:
      break;
  }
});

The message listener is defined as part of the ‘message provider’, in messages.dart:

java
subscription.messages.listen((m) {
  if (m.messageType == MessageType.normal) {
    //  A message is received from PubNub.
    //  todo
  } else if (m.messageType == MessageType.objects) {
    //  Whenever Object meta data is changed, an Object event is received.
    //  todo
  }
});

Step 2e. Listening for new messages

Once an application subscribes to messages on a channel, the messages will be received by the message listener, created at step 2d.  

Update the (m.messageType == MessageType.normal) condition as follows to update the List of chat messages which this provider is responsible for:

java
_addMessage(ChatMessage(
    timetoken: '${m.publishedAt}',
    channel: m.channel,
    uuid: m.uuid.value,
    message: m.content));

Step 2f. Listening for presence events

By subscribing to a channel’s presence events, as was done in step 2b, we will also receive events whenever a user joins or leaves the channel.

Update the presenceEvent.action switch statement with the following cases:

java
case PresenceAction.join:
  _addOnlineUser(presenceEvent.uuid!.value);
  break;
case PresenceAction.leave:
case PresenceAction.timeout:
  _removeOnlineUser(presenceEvent.uuid!.value);
  break;
case PresenceAction.stateChange:
  break;
case PresenceAction.interval:
  if (presenceEvent.join.length > 0) {
    _onlineUsers.addAll(presenceEvent.join.map((uuid) => uuid.value));
  }
  if (presenceEvent.join.length > 0) {
    _onlineUsers.removeAll(presenceEvent.leave
        .where((id) => id.value != AppState.deviceId)
        .map((uuid) => uuid.value));
  }
  notifyListeners();
  break;

When the application first starts, it needs to populate the list of users currently in the channel.  Since presence events only apply to users newly joining or leaving, the application calls the ‘hereNow’ function on launch to retrieve a list of device IDs already in the group chat.  This can be done whenever the application launches, so add it to the initialization code for the PresenceProvider, in presence.dart:

java
var result = await pubnub.hereNow(channels: {AppState.channelName});
   _onlineUsers.addAll(result.channels.values
       .expand((c) => c.uuids.values)
       .map((occupantInfo) => occupantInfo.uuid));

Note that _onlineUsers is the List of user Ids taking part in the chat. 

Step 2g. Retrieving past messages

When a user first joins the chat, it is often useful to provide some recently sent messages, to give some context of any conversation in progress.

Alternatively, if a user leaves and then rejoins the chat they will need to get up to speed on any messages they missed whilst they were away.

Both of these use cases are handled by the PubNub Message Persistence API.

During the initialisation of the message provider, in messages.dart, add the following code:

java
var result = pubnub.batch.fetchMessages({AppState.channelName}, count: 8);
   result.then((batchHistoryResult) {
     if (batchHistoryResult.channels[AppState.channelName] != null) {
       List<BatchHistoryResultEntry> historyResults = batchHistoryResult
           .channels[AppState.channelName] as List<BatchHistoryResultEntry>;
       historyResults.forEach((element) async => {
             _addMessage(ChatMessage(
                 timetoken: '${element.timetoken}',
                 channel: AppState.channelName,
                 uuid: element.uuid.toString(),
                 message: element.message)),

             //  Look up the friendly names for any uuids found in the history
             await AppState.friendlyNames
                 .lookupMemberName(element.uuid.toString())
           });
     }
   });

Step 2h. Assign a friendly name to the device and persist it with PubNub Object storage

App Context is a very powerful serverless storage mechanism provided by PubNub and designed to meet many use cases of a standard chat app.  

For example, you can choose to use App Context to store:

  • User meta data such as name, avatar, email

  • Channel metadata such as name and description as well as who the members of that channel are

This application does not take full advantage of App Context but does store a single piece of information, the ‘friendly name’ that the user can assign themselves. Assigning a friendly name avoids having to display the long user ID in the member list.

Setting the ‘friendly name’ is handled by a textfield towards the top of the application, modify the ‘save’ button click handler, _saveFriendlyName(), in friendly_name.dart to write the name to the PubNub user meta data.

java
var uuidMetadataInput = UuidMetadataInput(name: _controller.text);
       var result = await AppState.pubnub.instance.objects
           .setUUIDMetadata(uuidMetadataInput, uuid: AppState.deviceId);

Step 2i. Lookup someone else's friendly name using PubNub Object storage

The PubNub App Context store is considered the master copy of ‘friendly names’ set by anyone involved in the chat, as detailed in step 2h.

The presence listener, described in step 2f, only tells you the device IDs of users joining or leaving the chat.  To update the list of member friendly names in the chat, add the following code to the lookupMemberName() function, in friendly_names.dart

java
GetUuidMetadataResult lookupResult;
try {
  lookupResult = await pubnub.objects.getUUIDMetadata(uuid: deviceId);
} catch (e) {
  //  Caught exception when looking up name
  return;
}
GetUuidMetadataResult result = lookupResult;
groupMemberDeviceIds[deviceId] = result.metadata?.name;

What about if another user changes their friendly name?  To be notified when the friendly name changes, populate the (m.messageType == MessageType.objects) condition in the listener callback you defined back in step 2d.  This listener will fire whenever any member user meta data is updated.

java
AppState.friendlyNames.replaceMemberName(
           m.payload['data']['id'].toString(),
           m.payload['data']['name'].toString());

One additional point about the ‘objects’ listener (App Context listener): You must have “User Metadata Events” applied to your keyset, as below.

Notes and other caveats

For efficiency, PubNub handles presence slightly differently depending on how many occupants you have in the channel.  If using this app with a number of users above the ‘Announce Max’ setting, which defaults to 20, be sure to enable the ‘Presence Deltas’ option in your admin dashboard.  This causes PubNub to report the device IDs who have joined or left as part of the interval event, which this app uses to maintain who is in the channel.

Also under the presence setting, if you want the user closing the browser tab to trigger a ‘leave’ event, also tick the option for ‘Generate Leave on TCP FIN or RST’

If you would like to see this application running cross-platform, I recorded an additional video available separate to this tutorial on YouTube

Step 04

Congratulations - what's next?

Chat SDK Documentation

Chat OverviewDart SDK DocsChat Developer Path

Live Tour

Start the live coding tour

Explore other Sample apps

Flutter reference app written by our engineering team

Developing to a tight deadline? Explore Chat Components

Chat Components Introduction

Ready to get beyond Proof of Concept?

Schedule a 15 minute chat with one of our experts to get your project moving

Talk to an Expert
Keep building

Take it to production on your own keys

Create a free PubNub account to get permanent keys for this app.