Integrating Flutter with Laravel and Pusher
Source Code: https://github.com/maurobaptista/flutter-laravel-pusher
Expected Result
Setting up
This integration will use fresh installation of a Flutter project and, also, a fresh installation of a Laravel project, with some extra packages:
Flutter
1flutter create flutter_app
Packages:
- flutter_dotenv: ^2.1.0
- pusher_websocket_flutter: ^0.2.0
Laravel
1composer create-project --prefer-dist laravel/laravel laravel_app
Packages:
- pusher/pusher-php-server: ~4.0
Pusher
You must have an account at https://pusher.com/
After you create your account, add a name (I'll use flutter-laravel-app
), and choose you tech: Android (java)
and PHP
On you channel overview, click in App Keys
. On you Laravel .env file, add the keys information.
1PUSHER_APP_ID={replace with the app_id}2PUSHER_APP_KEY={replace with the key}3PUSHER_APP_SECRET={replace with the secret}4PUSHER_APP_CLUSTER={replcae with the cluster you choose}
Also in the .env
file, change the driver to broadcast:
1BROADCAST_DRIVER=pusher
Now click and Debug Console
, this is the interface where we will see wich message we are sending to flutter.
Sending the request to Pusher (Laravel Side)
Creating the trigger
For a matter to make it easy to trigger the event that will be broadcasted, We will set a command to do it.
1php artisan make:command SendBroadcast
Inside the app\Console\Commands\SendBroadcast
file, I will change the $signature
and the $desription
to:
1protected $signature = 'broadcast:send';2 3protected $description = 'Broadcast a message';
Add the code to handle the event, we are going to create the SendMessage
class in the next step.
1public function handle()2{3 $message = $this->ask("Which message do you want to broadcast?");4 event(new \App\Events\SendMessage($message ?: 'No Message :)'));5}
This way we can call in our command line: php artisan broadcast:send
Creating the event
1php artisan make:event SendMessage
In the file app/Events/SendMessage.php
we will make the class implement the ShouldBroadcast
1...2 3class SendMessage implements ShouldBroadcast4{5 ...6}
Now we need to set the attribute to send the message:
1/** @var string */ 2public $message; 3 4/** 5 * Create a new event instance. 6 * 7 * @return void 8 */ 9public function __construct(string $message)10{11 $this->message = $message;12}
As we are setting the $message
as public, Laravel will add the to the payload, as:
1[2 'message' => $message,3]
As we are going to broadcast to a public channel, we can return just the class Channel
.
1public function broadcastOn()2{3 return new Channel('flutter-laravel-app');4}
Test the broadcasting
Now you can run the command php artisan broadcast:send
, and if all is working as expected, you should see it reflecting in the Debug Console
in the pusher page.
Retrieving the messages (Flutter)
Adding Network data
We will need to add a file in the folder /android/app/src/main/res
called network_security_config.xml
with the content:
1<?xml version="1.0" encoding="utf-8"?>2<network-security-config>3 <base-config cleartextTrafficPermitted="true" />4</network-security-config>
If you do not do that you can face the error: No Network Security Config specified, using platform default
Setting the .env file
Inside your flutter project folder, add a file called .env
, and add the same Pusher data your have in Laravel .env file:
1PUSHER_APP_ID={replace with the app_id}2PUSHER_APP_KEY={replace with the key}3PUSHER_APP_SECRET={replace with the secret}4PUSHER_APP_CLUSTER={replcae with the cluster you choose}
Then add this file to your pubspec.yaml
file.
1assets:2 - .env
Do not forget to add the .env
file to your .gitignore
In the file /lib/main.dart
add the DotEnv as a singleton. Below you can see the whole main.dart
file, as we remove the boilerplater from it. The MessageScreen
class will be created next.
1import 'package:flutter/material.dart'; 2import 'package:flutter_dotenv/flutter_dotenv.dart'; 3 4import './screens/message_screen.dart'; 5 6Future main() async { 7 await DotEnv().load('.env'); 8 9 runApp(MyApp());10}11 12class MyApp extends StatelessWidget {13 // This widget is the root of your application.14 @override15 Widget build(BuildContext context) {16 return MaterialApp(17 title: 'Flutter Demo',18 home: MessageScreen(),19 );20 }21}
Setting the Pusher
First create a new file /lib/screens/message_screen.dart
and set it as a stateful widget. Note, that we are already importing the packages we are going to use, as pusher
and dotEnv
.
1import 'package:flutter/material.dart'; 2import 'package:flutter_dotenv/flutter_dotenv.dart'; 3import 'package:pusher_websocket_flutter/pusher.dart'; 4 5class MessageScreen extends StatefulWidget { 6 @override 7 _MessageScreenState createState() => _MessageScreenState(); 8} 9 10class _MessageScreenState extends State<MessageScreen> {11 @override12 Widget build(BuildContext context) {13 return Container();14 }15}
Now lets set the Pusher. We are going to call it in the initState
method, as we need to star
1class _MessageScreenState extends State<MessageScreen> { 2 StreamController<String> _eventData = StreamController<String>(); 3 Sink get _inEventData => _eventData.sink; 4 Stream get eventStream => _eventData.stream; 5 6 Channel channel; 7 8 String channelName = 'flutter-laravel-app'; 9 String eventName = 'laravel.app';10 11 List<String> messages = new List<String>();12 13 ...14 15 Future<void> initPusher() async {16 await Pusher.init(17 DotEnv().env['PUSHER_APP_KEY'],18 PusherOptions(cluster: DotEnv().env['PUSHER_APP_CLUSTER']),19 enableLogging: true20 );21 22 Pusher.connect();23 24 channel = await Pusher.subscribe(channelName);25 26 channel.bind(eventName, (last) {27 final String data = last.data;28 _inEventData.add(data);29 });30 31 eventStream.listen((data) async {32 messages.add(data);33 34 print(messages);35 });36 37 ...38}
First we are going to define the variables we are going to use:
The first three variable are going to handle the stream of new message coming in our app via the Pusher.
1StreamController<String> _eventData = StreamController<String>();2Sink get _inEventData => _eventData.sink;3Stream get eventStream => _eventData.stream;
Then the channel
variable will hold data from the Pusher channel.
1Channel channel;
To make it easier to change the information we are going to store the channel name and the event name defined in Laravel in string variables.
1String channelName = 'flutter-laravel-app';2String eventName = 'laravel.app';
The last one is where we are going to store the messages we are receiving:
1List<String> messages = new List<String>();
On the initPusher
method we need to:
Init the Pusher package with data from our access key and the cluster we should connect, and then we are going to connect into Pusher via the Pusher.connect()
:
1await Pusher.init(2 DotEnv().env['PUSHER_APP_KEY'],3 PusherOptions(cluster: DotEnv().env['PUSHER_APP_CLUSTER']),4 enableLogging: true5);6 7Pusher.connect();
Then we must subscribe to our channel, and then bind the event to our streamer.
1channel = await Pusher.subscribe(channelName);2 3channel.bind(eventName, (last) {4 final String data = last.data;5 _inEventData.add(data);6});
In the end we must listen to the stream and handle the data. For now, lets just add it to the messages list and then see it in flutter console.
1eventStream.listen((data) async {2 messages.add(data);3 4 print(messages);5});
To make it work as expected, we must add it to the iniState():
1class _MessageScreenState extends State<MessageScreen> { 2 ... 3 4 @override 5 void initState() 6 { 7 super.initState(); 8 9 initPusher();10 }11 12 ...13}
To avoid keep the connection with Pusher open, we must close it in the dispose
method:
1class _MessageScreenState extends State<MessageScreen> { 2 ... 3 4 @override 5 void dispose() 6 { 7 Pusher.unsubscribe(channelName); 8 channel.unbind(eventName); 9 _eventData.close();10 11 super.dispose();12 }13 14 ...15}
Let's check if it is working as expected
After you run you app, you should see something like that in your Debug Console
in the Pusher dashboard:
Now run the php artinan broadcast:send
and you should see the message in Flutter console.
Improving the App view
Storing only the message
First, let's store in the message variable on the message and not the whole payload.
Add the import 'dart:convert';
at the beginning of the file.
Then replace the listen callback to:
1eventStream.listen((data) async {2 Map<String, dynamic> message = jsonDecode(data);3 4 setState(() {5 messages.add(message['message']);6 });7});
Note that we are storing the message inside the setState
, so our app can be rebuild when we receive a new message.
Changing the view
Ok, now we can change our black app to show the messages we are receiving.
1@override 2Widget build(BuildContext context) { 3return MaterialApp( 4 title: 'Flutter + Laravel + Pusher', 5 home: Scaffold( 6 appBar: AppBar( 7 title: Text('Messages'), 8 ), 9 body: ListView.builder(10 itemCount: messages.length,11 itemBuilder: (BuildContext context, int index) {12 return Container(13 margin: EdgeInsets.all(15),14 child: Center(15 child: Text(16 messages[index],17 style: TextStyle(18 fontSize: 20,19 fontWeight: FontWeight.bold,20 ),21 ),22 ),23 );24 }25 )26 ),27);
You have some other way to approach that, as a StreamBuilder (which I believe would be cleaner), but here is just to see how to create the backend (Laravel) and the FrontEnd (Flutter) to load pusher messages.