# Getting started: Pub/Sub with Flutter This guide will get you started with Ably Pub/Sub in a new Flutter application. You'll establish a realtime connection to Ably and learn to publish and subscribe to messages. You'll also implement presence to track other online clients, and learn how to retrieve message history. ![Screenshot of the completed Flutter Pub/Sub application showing a web interface with connection status, a message input field, realtime message display, and a presence indicator showing online users. The interface demonstrates the key features you'll build including publishing messages, subscribing to receive messages in realtime, and tracking which clients are currently present in the channel.](https://raw.githubusercontent.com/ably/docs/main/src/images/content/screenshots/getting-started/pub-sub-flutter-getting-started-guide.png) ## Prerequisites 1. [Sign up](https://ably.com/sign-up) for a free Ably account. 2. Create a [new app](https://ably.com/accounts/any/apps/new), and create your first API key in the **API Keys** tab of the dashboard. 3. Your API key will need the `publish`, `subscribe`, `presence` and `history` [capabilities](https://ably.com/docs/auth/capabilities.md). ### Create a Flutter project Create a new Flutter project and navigate to the project folder: ```shell flutter create ably_pubsub_flutter cd ably_pubsub_flutter ``` ### Install Ably Pub/Sub Flutter SDK Add the Ably Flutter SDK to your `pubspec.yaml` file: ```flutter dependencies: flutter: sdk: flutter ably_flutter: ^1.2.35 ``` Then run: ```shell flutter pub get ``` ### Set up Ably Realtime Client Create a new file `lib/ably_service.dart` to manage your Ably connection: ```flutter // lib/ably_service.dart import 'package:ably_flutter/ably_flutter.dart' as ably; class AblyService { static final AblyService _instance = AblyService._internal(); late ably.Realtime _realtime; factory AblyService() { return _instance; } AblyService._internal(); Future init() async { final clientOptions = ably.ClientOptions( key: 'your-api-key', clientId: 'my-first-client', ); _realtime = ably.Realtime(options: clientOptions); } ably.Realtime get realtime => _realtime; Future close() async { await _realtime.close(); } } ``` Replace the contents of your `lib/main.dart` to initialize the Ably service: ```flutter // lib/main.dart import 'package:flutter/material.dart'; import 'ably_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await AblyService().init(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Ably Pub/Sub Flutter', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Ably Pub/Sub Flutter'), backgroundColor: Colors.blue, ), body: const Padding( padding: EdgeInsets.all(16.0), child: Center( child: Card( child: Padding( padding: EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Ably Pub/Sub Flutter', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue, ), ), ], ), ), ), ), ), ); } } ``` ### (Optional) Install Ably CLI Use the [Ably CLI](https://github.com/ably/cli) as an additional client to quickly test Pub/Sub features. It can simulate other clients by publishing messages, subscribing to channels, and managing presence states. 1. Install the Ably CLI: ```shell npm install -g @ably/cli ``` 2. Run the following to log in to your Ably account and set the default app and API key: ```shell ably login ``` ## Step 1: Connect to Ably Clients establish a connection with Ably when they instantiate an SDK instance. This enables them to send and receive messages in realtime across channels. In the [Set up Ably Realtime Client](#prerequisites-setup-ably-client) section, you added code to create an Ably Realtime client. This code creates a new Realtime client instance, establishing a connection to Ably when your application starts. At the minimum you need to provide an authentication mechanism. While using an API key is fine for the purposes of this guide, you should use [token authentication](https://ably.com/docs/auth/token.md) in production environments. A [`clientId`](https://ably.com/docs/auth/identified-clients.md) ensures the client is identified, which is required to use certain features, such as presence. To monitor the Ably connection state within your application, create a widget that displays the current connection state. Create a new file `lib/widgets/connection_state.dart`: ```flutter // lib/widgets/connection_state.dart import 'package:flutter/material.dart'; import 'package:ably_flutter/ably_flutter.dart' as ably; import '../ably_service.dart'; class AblyConnectionState extends StatefulWidget { const AblyConnectionState({super.key}); @override State createState() => _ConnectionStateState(); } class _ConnectionStateState extends State { late ably.Connection _connection; ably.ConnectionState _connectionState = ably.ConnectionState.initialized; @override void initState() { super.initState(); _connection = AblyService().realtime.connection; _connectionState = _connection.state; // Listen for connection state changes _connection.on().listen((ably.ConnectionStateChange stateChange) { setState(() { _connectionState = stateChange.current; }); }); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( 'Connection: ${_connectionState.name}!', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), ); } } ``` Update your `lib/main.dart` to include the `ConnectionState` widget: ```flutter // lib/main.dart // Add the following import import 'widgets/connection_state.dart'; // Inside MyHomePage widget, find the children array and add `AblyConnectionState()` widget. children: [ Text( 'Ably Pub/Sub Flutter', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue, ), ), // Add AblyConnectionState here AblyConnectionState(), ], ``` Now start your application by running: ```shell flutter run ``` You should see the connection state displayed in your UI (e.g., `Connection: connected!`). You can also inspect connection events in the [dev console](https://ably.com/accounts/any/apps/any/console) of your app. ## Step 2: Subscribe to a channel and publish a message Messages contain the data that a client is communicating, such as a short 'hello' from a colleague, or a financial update being broadcast to subscribers from a server. Ably uses channels to separate messages into different topics, so that clients only ever receive messages on the channels they are subscribed to. ### Subscribe to a channel Create a new file `lib/widgets/messages.dart` to handle message display and publishing: ```flutter // lib/widgets/messages.dart import 'package:flutter/material.dart'; import 'package:ably_flutter/ably_flutter.dart' as ably; import '../ably_service.dart'; class Messages extends StatefulWidget { const Messages({super.key}); @override State createState() => _MessagesState(); } class _MessagesState extends State { late ably.RealtimeChannel _channel; final List _messages = []; final TextEditingController _textController = TextEditingController(); @override void initState() { super.initState(); _channel = AblyService().realtime.channels.get('my-first-channel'); // Subscribe to messages _channel.subscribe().listen((ably.Message message) { setState(() { _messages.add(message); }); }); } Future _publishMessage() async { if (_textController.text.trim().isEmpty) return; try { await _channel.publish( name: 'my-first-messages', data: _textController.text.trim(), ); _textController.clear(); } catch (error) { debugPrint('Error publishing message: $error'); } } @override Widget build(BuildContext context) { return Column( children: [ Expanded( child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { final message = _messages[index]; final isMine = message.clientId == 'my-first-client'; return Container( margin: const EdgeInsets.symmetric( vertical: 4.0, horizontal: 8.0, ), padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: isMine ? Colors.green.shade100 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8.0), ), child: Text( message.data.toString(), style: const TextStyle(color: Colors.black87), ), ); }, ), ), Container( padding: const EdgeInsets.all(8.0), decoration: const BoxDecoration( border: Border(top: BorderSide(color: Colors.grey)), ), child: Row( children: [ Expanded( child: TextField( controller: _textController, decoration: const InputDecoration( hintText: 'Type your message...', border: OutlineInputBorder(), ), onSubmitted: (_) => _publishMessage(), ), ), const SizedBox(width: 8.0), ElevatedButton( onPressed: _publishMessage, child: const Text('Publish'), ), ], ), ), ], ); } @override void dispose() { _textController.dispose(); super.dispose(); } } ``` Update your `lib/main.dart` to include the `Messages` widget: ```flutter // lib/main.dart // Add the following import import 'widgets/messages.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await AblyService().init(); runApp(const MyApp()); } // Replace MyHomePage with the following: class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Ably Pub/Sub Flutter'), backgroundColor: Colors.blue, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ const Text( 'Ably Pub/Sub Flutter', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue, ), ), const AblyConnectionState(), ], ), ), ), const SizedBox(height: 16.0), const Expanded( child: Card( child: Messages(), ), ), ], ), ), ); } } ``` ### Publish a message Your application now supports publishing realtime messages! Type a message and press "Publish" to see it appear in your UI. With the app open on a device or emulator, open a new terminal on your computer and publish a message to the channel with the Ably CLI: ```shell ably channels publish my-first-channel 'Hello from CLI!' ``` Messages from the CLI will appear in your UI in a different color to the ones you sent from the app. ## Step 3: Join the presence set Presence enables clients to be aware of one another if they are present on the same channel. You can then show clients who else is online, provide a custom status update for each, and notify the channel when someone goes offline. Create a new file `lib/widgets/presence_status.dart` to handle presence functionality: ```flutter // lib/widgets/presence_status.dart import 'package:flutter/material.dart'; import 'package:ably_flutter/ably_flutter.dart' as ably; import '../ably_service.dart'; class PresenceStatus extends StatefulWidget { const PresenceStatus({super.key}); @override State createState() => _PresenceStatusState(); } class _PresenceStatusState extends State { late ably.RealtimeChannel _channel; final List _presenceData = []; @override void initState() { super.initState(); _channel = AblyService().realtime.channels.get('my-first-channel'); // Enter presence set _channel.presence.enter({'status': "I'm here!"}); // Subscribe to presence updates _channel.presence.subscribe().listen(( ably.PresenceMessage presenceMessage, ) { setState(() { _updatePresenceData(presenceMessage); }); }); // Get current presence members _loadPresenceMembers(); } Future _loadPresenceMembers() async { try { final members = await _channel.presence.get(); setState(() { _presenceData.clear(); _presenceData.addAll(members); }); } catch (error) { debugPrint('Error loading presence members: $error'); } } void _updatePresenceData(ably.PresenceMessage presenceMessage) { switch (presenceMessage.action) { case ably.PresenceAction.enter: case ably.PresenceAction.present: case ably.PresenceAction.update: _presenceData.removeWhere( (member) => member.clientId == presenceMessage.clientId, ); _presenceData.add(presenceMessage); break; case ably.PresenceAction.leave: case ably.PresenceAction.absent: _presenceData.removeWhere( (member) => member.clientId == presenceMessage.clientId, ); break; case null: // Handle null case break; } } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(bottom: 8.0), decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: Colors.grey)), ), child: Text( 'Present: ${_presenceData.length}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green, ), textAlign: TextAlign.center, ), ), const SizedBox(height: 16.0), Expanded( child: ListView.builder( itemCount: _presenceData.length, itemBuilder: (context, index) { final member = _presenceData[index]; final status = member.data is Map ? (member.data as Map)['status'] : null; return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( children: [ Container( width: 8.0, height: 8.0, decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), const SizedBox(width: 8.0), Expanded( child: Text( '${member.clientId}${status != null ? ' ($status)' : ''}', style: const TextStyle(color: Colors.black87), ), ), ], ), ); }, ), ), ], ), ); } } ``` Update your `lib/main.dart` to include the `PresenceStatus` widget: ```flutter // lib/main.dart // Import the PresenceStatus widget import 'widgets/presence_status.dart'; // Replace the line `const Expanded(child: Card(child: Messages())),` with the following: Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 2, child: Card( child: Container( decoration: const BoxDecoration( border: Border(right: BorderSide(color: Colors.blue)), ), child: const PresenceStatus(), ), ), ), const SizedBox(width: 16.0), const Expanded( flex: 3, child: Card( child: Messages(), ), ), ], ), ), ``` The application will now display a list of clients currently present on the channel. Your current client ID should appear in the list of online users. You can have another client join the presence set using the Ably CLI: ```shell ably channels presence enter my-first-channel --data '{"status":"From CLI"}' ``` ## Step 4: Retrieve message history You can retrieve previously sent messages using the history feature. Ably stores all messages for 2 minutes by default in the event a client experiences network connectivity issues. You can [extend the storage period](https://ably.com/docs/storage-history/storage.md) of messages if required. Update your `lib/widgets/messages.dart` file by finding the function `_publishMessage` and above this add the new function `_loadMessageHistory`: ```flutter Future _loadMessageHistory() async { try { // Retrieve the last 5 messages from history final history = await _channel.history( ably.RealtimeHistoryParams(limit: 5), ); // History responses are returned in reverse chronological order (newest first) // Reverse the array to show the latest messages at the bottom in the UI final messagesFromHistory = history.items.reversed.toList(); setState(() { _messages.clear(); _messages.addAll(messagesFromHistory); }); } catch (error) { debugPrint('Error loading message history: $error'); } } ``` Find the line `// Subscribe to messages` and above this add the following: ```flutter // Load message history first _loadMessageHistory(); ``` Test this feature with the following steps: 1. Publish several messages using your application UI, or send messages from another client using the Ably CLI: ```shell ably channels publish --count 5 my-first-channel "Message number {{.Count}}" ``` 2. Hot reload the app or restart it. This will cause the `Messages` widget to reinitialize and call the `_loadMessageHistory()` method. 3. You should see the last 5 messages displayed in your UI, ordered from oldest to newest at the bottom: ```json Message number 1 Message number 2 Message number 3 Message number 4 Message number 5 ``` ## Next steps Continue to explore the documentation with Flutter as the selected language: * Understand [token authentication](https://ably.com/docs/auth/token.md) before going to production. * Understand how to effectively [manage connections](https://ably.com/docs/connect.md#close?lang=flutter). * Explore more [advanced](https://ably.com/docs/pub-sub/advanced.md) Pub/Sub concepts. You can also explore the [Ably CLI](https://www.npmjs.com/package/@ably/cli) further, or visit the Pub/Sub [API references](https://ably.com/docs/api/realtime-sdk.md).