How to add realtime communication to your Flutter app with Pusher Channels
Familiarity with Flutter environment and Dart language would be beneficial to complete this tutorial, but aren't compulsory as this is a step by step guide.
In this tutorial, we’ll create a Flutter project from scratch using Pusher Channels to send and receive events in real time from our application.
Flutter is an open source framework by Google for building beautiful, natively compiled, multi-platform applications from a single codebase.
You can use Channels to bring scalable realtime features to your apps via our pub/sub WebSocket-based service, providing seamless bi-directional data transactions for highly engaging user experiences.
Set up
To complete this tutorial you’ll need the following:
- A Pusher account and a Channels project set up in the dashboard. If you don’t already have one you can sign up for the sandbox plan which is completely free and will be more than ample to complete this sample project so you can start experimenting with realtime for Flutter.
- The latest version of Flutter installed on your system. If you’re just starting with Flutter, you can follow the tutorial here to install.
- An IDE of your choice.
Platform support
- Android support for API level greater than 19
- iOS > 10.0
- Web browsers: Chrome, Firefox & Safari
All events/connections can be viewed on the debug console on the Pusher dashboard.
Creating your first Flutter/Pusher app
1. Set up your project
If you already have a Flutter project you can jump to installing the Pusher SDK.
Creating your first app with Flutter is as easy as
$ flutter create pusher_example
This will create a directory called pusher_example. We can now run the sample app to see if everything is working properly.
$ cd pusher_example
$ flutter run
If you do not have Android or iOS Flutter set up on your system by default it should run in web mode and open a browser with the app. This is a simple button counter app.
2. Install the Pusher SDK
You can install the Pusher Channels Flutter SDK from
$ flutter pub add pusher_channels_flutter
3. Create app class
Now, let’s create a state class that will contain our app.
Open lib/main.dart
in the IDE of your choice.
Class _MyAppState
will be our main factory to implement all features. It will contain all the variables and functions for event listening on Channels.
class _MyAppState extends State<MyApp> {
PusherChannelsFlutter pusher = PusherChannelsFlutter.getInstance();
String _log = 'output:\n';
final _apiKey = TextEditingController();
final _cluster = TextEditingController();
final _channelName = TextEditingController();
final _eventName = TextEditingController();
final _channelFormKey = GlobalKey<FormState>();
final _eventFormKey = GlobalKey<FormState>();
final _listViewController = ScrollController();
final _data = TextEditingController();
void log(String text) {
print("LOG: $text");
setState(() {
_log += text + "\n";
Timer(
const Duration(milliseconds: 100),
() => _listViewController
.jumpTo(_listViewController.position.maxScrollExtent));
});
}
With our class initiated with the required variables in place, we can get to work on controllers.
The method onConnectPressed()
establishes a connection to Pusher and declares all the different event listeners.
void onConnectPressed() async {
if (!_channelFormKey.currentState!.validate()) {
return;
}
// Remove keyboard
FocusScope.of(context).requestFocus(FocusNode());
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("apiKey", _apiKey.text);
prefs.setString("cluster", _cluster.text);
prefs.setString("channelName", _channelName.text);
try {
await pusher.init(
apiKey: _apiKey.text,
cluster: _cluster.text,
onConnectionStateChange: onConnectionStateChange,
onError: onError,
onSubscriptionSucceeded: onSubscriptionSucceeded,
onEvent: onEvent,
onSubscriptionError: onSubscriptionError,
onDecryptionFailure: onDecryptionFailure,
onMemberAdded: onMemberAdded,
onMemberRemoved: onMemberRemoved,
// authEndpoint: "<Your Authendpoint Url>",
// onAuthorizer: onAuthorizer
);
await pusher.subscribe(channelName: _channelName.text);
await pusher.connect();
} catch (e) {
log("ERROR: $e");
}
}
The method onTriggerEventPressed()
implements the client to client communication. This will send the text from the front end with the desired event name.
void onTriggerEventPressed() async {
var eventFormValidated = _eventFormKey.currentState!.validate();
if (!eventFormValidated) {
return;
}
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("eventName", _eventName.text);
prefs.setString("data", _data.text);
pusher.trigger(PusherEvent(
channelName: _channelName.text,
eventName: _eventName.text,
data: _data.text));
}
Now let’s add more controllers for some other events. These are attached to the init function in onConnectPressed()
.
void onConnectionStateChange(dynamic currentState, dynamic previousState) {
log("Connection: $currentState");
}
void onError(String message, int? code, dynamic e) {
log("onError: $message code: $code exception: $e");
}
void onEvent(PusherEvent event) {
log("onEvent: $event");
}
void onSubscriptionSucceeded(String channelName, dynamic data) {
log("onSubscriptionSucceeded: $channelName data: $data");
final me = pusher.getChannel(channelName)?.me;
log("Me: $me");
}
void onSubscriptionError(String message, dynamic e) {
log("onSubscriptionError: $message Exception: $e");
}
void onDecryptionFailure(String event, String reason) {
log("onDecryptionFailure: $event reason: $reason");
}
void onMemberAdded(String channelName, PusherMember member) {
log("onMemberAdded: $channelName user: $member");
}
void onMemberRemoved(String channelName, PusherMember member) {
log("onMemberRemoved: $channelName user: $member");
}
Initiate the parameters by initPlatformState()
.
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_apiKey.text = prefs.getString("apiKey") ?? '';
_cluster.text = prefs.getString("cluster") ?? 'eu';
_channelName.text = prefs.getString("channelName") ?? 'presence-channel';
_eventName.text = prefs.getString("eventName") ?? 'client-event';
_data.text = prefs.getString("data") ?? 'test';
});
}
void initState() {
super.initState();
initPlatformState();
}
That’s the most complicated part done with!
It’s time to put everything together with a UI.
build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text(pusher.connectionState == 'DISCONNECTED'
? 'Pusher Channels Example'
: _channelName.text),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
controller: _listViewController,
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: <Widget>[
if (pusher.connectionState != 'CONNECTED')
Form(
key: _channelFormKey,
child: Column(children: <Widget>[
TextFormField(
controller: _apiKey,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your API key.'
: null;
},
decoration:
const InputDecoration(labelText: 'API Key'),
),
TextFormField(
controller: _cluster,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your cluster.'
: null;
},
decoration: const InputDecoration(
labelText: 'Cluster',
),
),
TextFormField(
controller: _channelName,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your channel name.'
: null;
},
decoration: const InputDecoration(
labelText: 'Channel',
),
),
ElevatedButton(
onPressed: onConnectPressed,
child: const Text('Connect'),
)
]))
else
Form(
key: _eventFormKey,
child: Column(children: <Widget>[
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: pusher
.channels[_channelName.text]?.members.length,
itemBuilder: (context, index) {
final member = pusher
.channels[_channelName.text]!.members
.elementAt(index);
return ListTile(
title: Text(member.userInfo.toString()),
subtitle: Text(member.userId));
}),
TextFormField(
controller: _eventName,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your event name.'
: null;
},
decoration: const InputDecoration(
labelText: 'Event',
),
),
TextFormField(
controller: _data,
decoration: const InputDecoration(
labelText: 'Data',
),
),
ElevatedButton(
onPressed: onTriggerEventPressed,
child: const Text('Trigger Event'),
),
]),
),
SingleChildScrollView(
scrollDirection: Axis.vertical, child: Text(_log)),
]),
),
),
);
}
Widget
4. Running a web-specific configuration
To run the web version of this application the following needs to be added to web/index.html
.
<head>
<script charset="utf-8" src="https://js.pusher.com/7.0/pusher.min.js"></script>
...
</head>
5. iOS-specific integration
The Pusher Channels Flutter plugin adds the pusher-websocket-swift cocoapod to your project. In this case you will likely need to run a
$ pod install
6. Running the application
That’s it! Now we are ready to run our application. If everything has gone smoothly, you can issue the following command
$ flutter run
And should be able to see a screen like below in your browser:
7. TLDR; Integrating authentication
To use private and presence channels the pusher.init()
function provides two parameters authEndpoint
and onAuthorizer
.
You can run an auth server using pusher-channels-auth-example. And define the onAuthorizer
in _MyAppState
.
dynamic onAuthorizer(String channelName, String socketId, dynamic options) {
return {
"auth": "foo:bar",
"channel_data": '{"user_id": 1}',
"shared_secret": "foobar"
};
}
To read more on auth go here.
Check out our use cases to read more about what you can build on your Flutter apps using Pusher.
16 February 2022
by Ashmeet Singh, Raphael Sousa Santos