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.
To complete this tutorial you'll need the following:
All events/connections can be viewed on the debug console on the Pusher dashboard.
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.
1$ cd pusher_example 2$ 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.
You can install the Pusher Channels Flutter SDK from
$ flutter pub add pusher_channels_flutter
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.
1class _MyAppState extends State<MyApp> { 2 PusherChannelsFlutter pusher = PusherChannelsFlutter.getInstance(); 3 String _log = 'output:\n'; 4 final _apiKey = TextEditingController(); 5 final _cluster = TextEditingController(); 6 final _channelName = TextEditingController(); 7 final _eventName = TextEditingController(); 8 final _channelFormKey = GlobalKey<FormState>(); 9 final _eventFormKey = GlobalKey<FormState>(); 10 final _listViewController = ScrollController(); 11 final _data = TextEditingController(); 12 13 void log(String text) { 14 print("LOG: $text"); 15 setState(() { 16 _log += text + "\n"; 17 Timer( 18 const Duration(milliseconds: 100), 19 () => _listViewController 20 .jumpTo(_listViewController.position.maxScrollExtent)); 21 }); 22 }
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.
1void onConnectPressed() async { 2 if (!_channelFormKey.currentState!.validate()) { 3 return; 4 } 5 // Remove keyboard 6 FocusScope.of(context).requestFocus(FocusNode()); 7 SharedPreferences prefs = await SharedPreferences.getInstance(); 8 prefs.setString("apiKey", _apiKey.text); 9 prefs.setString("cluster", _cluster.text); 10 prefs.setString("channelName", _channelName.text); 11 12 try { 13 await pusher.init( 14 apiKey: _apiKey.text, 15 cluster: _cluster.text, 16 onConnectionStateChange: onConnectionStateChange, 17 onError: onError, 18 onSubscriptionSucceeded: onSubscriptionSucceeded, 19 onEvent: onEvent, 20 onSubscriptionError: onSubscriptionError, 21 onDecryptionFailure: onDecryptionFailure, 22 onMemberAdded: onMemberAdded, 23 onMemberRemoved: onMemberRemoved, 24 // authEndpoint: "<Your Authendpoint Url>", 25 // onAuthorizer: onAuthorizer 26 ); 27 await pusher.subscribe(channelName: _channelName.text); 28 await pusher.connect(); 29 } catch (e) { 30 log("ERROR: $e"); 31 } 32 }
The method onTriggerEventPressed()
implements the client to client communication. This will send the text from the front end with the desired event name.
1void onTriggerEventPressed() async { 2 var eventFormValidated = _eventFormKey.currentState!.validate(); 3 4 if (!eventFormValidated) { 5 return; 6 } 7 SharedPreferences prefs = await SharedPreferences.getInstance(); 8 prefs.setString("eventName", _eventName.text); 9 prefs.setString("data", _data.text); 10 pusher.trigger(PusherEvent( 11 channelName: _channelName.text, 12 eventName: _eventName.text, 13 data: _data.text)); 14 }
Now let’s add more controllers for some other events. These are attached to the init function in onConnectPressed()
.
1void onConnectionStateChange(dynamic currentState, dynamic previousState) { 2 log("Connection: $currentState"); 3 } 4 void onError(String message, int? code, dynamic e) { 5 log("onError: $message code: $code exception: $e"); 6 } 7 void onEvent(PusherEvent event) { 8 log("onEvent: $event"); 9 } 10 void onSubscriptionSucceeded(String channelName, dynamic data) { 11 log("onSubscriptionSucceeded: $channelName data: $data"); 12 final me = pusher.getChannel(channelName)?.me; 13 log("Me: $me"); 14 } 15 void onSubscriptionError(String message, dynamic e) { 16 log("onSubscriptionError: $message Exception: $e"); 17 } 18 void onDecryptionFailure(String event, String reason) { 19 log("onDecryptionFailure: $event reason: $reason"); 20 } 21 void onMemberAdded(String channelName, PusherMember member) { 22 log("onMemberAdded: $channelName user: $member"); 23 } 24 void onMemberRemoved(String channelName, PusherMember member) { 25 log("onMemberRemoved: $channelName user: $member"); 26 }
Initiate the parameters by initPlatformState()
.
1// Platform messages are asynchronous, so we initialize in an async method. 2 Future<void> initPlatformState() async { 3 // If the widget was removed from the tree while the asynchronous platform 4 // message was in flight, we want to discard the reply rather than calling 5 // setState to update our non-existent appearance. 6 if (!mounted) return; 7 SharedPreferences prefs = await SharedPreferences.getInstance(); 8 setState(() { 9 _apiKey.text = prefs.getString("apiKey") ?? ''; 10 _cluster.text = prefs.getString("cluster") ?? 'eu'; 11 _channelName.text = prefs.getString("channelName") ?? 'presence-channel'; 12 _eventName.text = prefs.getString("eventName") ?? 'client-event'; 13 _data.text = prefs.getString("data") ?? 'test'; 14 }); 15 } 16 17@override 18 void initState() { 19 super.initState(); 20 initPlatformState(); 21 }
That’s the most complicated part done with!
It's time to put everything together with a UI.
1@override 2 Widget build(BuildContext context) { 3 return MaterialApp( 4 home: Scaffold( 5 appBar: AppBar( 6 title: Text(pusher.connectionState == 'DISCONNECTED' 7 ? 'Pusher Channels Example' 8 : _channelName.text), 9 ), 10 body: Padding( 11 padding: const EdgeInsets.all(8.0), 12 child: ListView( 13 controller: _listViewController, 14 scrollDirection: Axis.vertical, 15 shrinkWrap: true, 16 children: <Widget>[ 17 if (pusher.connectionState != 'CONNECTED') 18 Form( 19 key: _channelFormKey, 20 child: Column(children: <Widget>[ 21 TextFormField( 22 controller: _apiKey, 23 validator: (String? value) { 24 return (value != null && value.isEmpty) 25 ? 'Please enter your API key.' 26 : null; 27 }, 28 decoration: 29 const InputDecoration(labelText: 'API Key'), 30 ), 31 TextFormField( 32 controller: _cluster, 33 validator: (String? value) { 34 return (value != null && value.isEmpty) 35 ? 'Please enter your cluster.' 36 : null; 37 }, 38 decoration: const InputDecoration( 39 labelText: 'Cluster', 40 ), 41 ), 42 TextFormField( 43 controller: _channelName, 44 validator: (String? value) { 45 return (value != null && value.isEmpty) 46 ? 'Please enter your channel name.' 47 : null; 48 }, 49 decoration: const InputDecoration( 50 labelText: 'Channel', 51 ), 52 ), 53 ElevatedButton( 54 onPressed: onConnectPressed, 55 child: const Text('Connect'), 56 ) 57 ])) 58 else 59 Form( 60 key: _eventFormKey, 61 child: Column(children: <Widget>[ 62 ListView.builder( 63 scrollDirection: Axis.vertical, 64 shrinkWrap: true, 65 itemCount: pusher 66 .channels[_channelName.text]?.members.length, 67 itemBuilder: (context, index) { 68 final member = pusher 69 .channels[_channelName.text]!.members 70 .elementAt(index); 71 return ListTile( 72 title: Text(member.userInfo.toString()), 73 subtitle: Text(member.userId)); 74 }), 75 TextFormField( 76 controller: _eventName, 77 validator: (String? value) { 78 return (value != null && value.isEmpty) 79 ? 'Please enter your event name.' 80 : null; 81 }, 82 decoration: const InputDecoration( 83 labelText: 'Event', 84 ), 85 ), 86 TextFormField( 87 controller: _data, 88 decoration: const InputDecoration( 89 labelText: 'Data', 90 ), 91 ), 92 ElevatedButton( 93 onPressed: onTriggerEventPressed, 94 child: const Text('Trigger Event'), 95 ), 96 ]), 97 ), 98 SingleChildScrollView( 99 scrollDirection: Axis.vertical, child: Text(_log)), 100 ]), 101 ), 102 ), 103 ); 104 }
To run the web version of this application the following needs to be added to web/index.html
.
1<head> 2 <script charset="utf-8" src="https://js.pusher.com/7.0/pusher.min.js"></script> 3 ... 4</head>
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
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:
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
.
1dynamic onAuthorizer(String channelName, String socketId, dynamic options) { 2 return { 3 "auth": "foo:bar", 4 "channel_data": '{"user_id": 1}', 5 "shared_secret": "foobar" 6 }; 7 }
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.