Getting started: Pub/Sub with Flutter
This guide will get you started with Ably Pub/Sub in a new Flutter application.
It will take you through the following steps:
- Create a client and establish a realtime connection to Ably.
- Attach to a channel and subscribe to its messages.
- Publish a message to the channel for your client to receive.
- Join and subscribe to the presence set of the channel.
- Retrieve the messages you sent in the guide from history.
- Close a connection to Ably when it is no longer needed.
Prerequisites
- Sign up for a free Ably account.
- Create a new app and get your API key from the dashboard.
- Your API key will need the
publish
,subscribe
,presence
andhistory
capabilities.
This step is optional. If you prefer not to use the Ably CLI to interact with your Pub/Sub Flutter application, you can still follow along by using multiple devices or emulators.
npm install -g @ably/cli
Run the following to log in to your Ably account and set the default app and API key:
ably login
ably apps switch
ably auth keys switch
Create a Flutter project
Create a new Flutter project and navigate to the project folder:
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:
1
2
3
4
dependencies:
flutter:
sdk: flutter
ably_flutter: ^1.2.35
Then run:
flutter pub get
Set up Ably Realtime Client
Create a new file lib/ably_service.dart
to manage your Ably connection:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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<void> init() async {
final clientOptions = ably.ClientOptions(
key: '{{API_KEY}}',
clientId: 'my-first-client',
);
_realtime = ably.Realtime(options: clientOptions);
}
ably.Realtime get realtime => _realtime;
Future<void> close() async {
await _realtime.close();
}
}
Replace the contents of your lib/main.dart
to initialize the Ably service:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 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,
),
),
],
),
),
),
),
),
);
}
}
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 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 in production environments. A clientId
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 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<AblyConnectionState> createState() => _ConnectionStateState();
}
class _ConnectionStateState extends State<AblyConnectionState> {
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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:
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 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 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<Messages> createState() => _MessagesState();
}
class _MessagesState extends State<Messages> {
late ably.RealtimeChannel _channel;
final List<ably.Message> _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<void> _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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 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:
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 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<PresenceStatus> createState() => _PresenceStatusState();
}
class _PresenceStatusState extends State<PresenceStatus> {
late ably.RealtimeChannel _channel;
final List<ably.PresenceMessage> _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<void> _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<String, dynamic>
? (member.data as Map<String, dynamic>)['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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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:
ably channels presence enter my-first-channel --client-id "my-cli" --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. This can be extended for longer if required.
Update your lib/widgets/messages.dart
file by finding the function _publishMessage
and above this add the new function _loadMessageHistory
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Future<void> _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:
1
2
// Load message history first
_loadMessageHistory();
Test this feature with the following steps:
- Publish several messages using your application UI, or send messages from another client using the Ably CLI:
ably channels publish --count 5 my-first-channel "Message number {{.Count}}"
- Hot reload the app or restart it. This will cause the
Messages
widget to reinitialize and call the_loadMessageHistory()
method. - You should see the last 5 messages displayed in your UI, ordered from oldest to newest at the bottom:
1
2
3
4
5
Message number 1
Message number 2
Message number 3
Message number 4
Message number 5
Step 5: Close the connection
Connections are automatically closed approximately two minutes after the last channel is detached. However, explicitly closing connections when they're no longer needed is good practice to help save costs and clean up listeners.
You can close the connection using the AblyService
, find the line _textController.dispose()
in the dispose
function and below this add:
1
2
3
4
// Add 10 second delay and close Ably connection
Future.delayed(const Duration(seconds: 10), () async {
await AblyService().close();
});
This ensures the connection is closed after ten seconds, freeing resources and deactivating listeners.
Next steps
Continue to explore the Ably Pub/Sub documentation with Flutter as the selected language:
Read more about the concepts covered in this guide:
- Revisit the basics of Pub/Sub
- Explore more advanced Pub/Sub concepts
- Understand realtime connections to Ably
- Read more about how to use presence in your apps
- Fetch message history in your apps
You can also explore the Ably CLI further, or visit the Pub/Sub API references.