【Laravel × Flutter】Laravel Echo・Laravel Echo Serverを使ってFlutterアプリと双方向通信を行う

先日業務でLaravel Echoを使ってFlutterアプリに対してお知らせの未読通知を実装することがありました。

備忘録も兼ねて投稿したいと思います。

# バージョン
```
$ php artisan --version
Laravel Framework 7.30.6

$ flutter --version
Flutter 3.0.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 85684f9300 (3 weeks ago) • 2022-06-30 13:22:47 -0700
Engine • revision 6ba2af10bb
Tools • Dart 2.17.5 • DevTools 2.12.2
```


# Laravel側の設定

まずはLaravel側でLaravel Echoの設定をしていきましょう。
今回はPusherではなくlaravel-echo-serverを使って実装していきたいと思います。


## Redisのインストール
まずはRedisをインストールします。僕はMacを使っているので下記コマンドでインストールします。
もし既にローカルにRedisがインストールされている場合は当然インストールは不要です。

https://qiita.com/zaburo/items/37cb9d5d40a86422716d

```
// インストール
$ brew install redis
```

redisは`redis-server`で起動します。一度起動してみましょう。

```
$ redis-server
42978:C 24 Jul 2022 16:23:49.242 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
42978:C 24 Jul 2022 16:23:49.242 # Redis version=7.0.3, bits=64, commit=00000000, modified=0, pid=42978, just started
42978:C 24 Jul 2022 16:23:49.242 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
42978:M 24 Jul 2022 16:23:49.243 * Increased maximum number of open files to 10032 (it was originally set to 2560).
42978:M 24 Jul 2022 16:23:49.243 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 7.0.3 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 42978
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

42978:M 24 Jul 2022 16:23:49.244 # WARNING: The TCP backlog setting of 511 cannot be enforced because kern.ipc.somaxconn is set to the lower value of 128.
42978:M 24 Jul 2022 16:23:49.244 # Server initialized
42978:M 24 Jul 2022 16:23:49.244 * Ready to accept connections

```

ここで表示されたポート（上記の場合は6379）は後で設定に使うので覚えておいてください。



## Laravel Echoのインストール

次にプロジェクトルートに移動して下記コマンドを実行し、Laravel Echo等をインストールします。

```
// laravel Echoに必要なライブラリのインストール
$ composer require predis/predis
$ npm install --save laravel-echo socket.io-client

// laravel-echo-serverをインストール
$ npm install -g laravel-echo-server

// laravel echoの初期設定を行いlaravel-echo-server.jsonを作成する
$ laravel-echo-server init

// ローカル環境の時のものです。参考にしてください
? Do you want to run this server in development mode? Yes
? Which port would you like to serve from? 6001
atomu@okunoMacBook-Pro mygolf % laravel-echo-server init
? Do you want to run this server in development mode? Yes
? Which port would you like to serve from? 6001
? Which database would you like to use to store presence channel members? redis
? Enter the host of your Laravel authentication server. http://localhost
? Will you be serving on http or https? http
? Do you want to generate a client ID/Key for HTTP API? Yes
? Do you want to setup cross domain access to the API? Yes
? Specify the URI that may access the API: http://localhost:8000
? Enter the HTTP methods that are allowed for CORS: GET, POST
? Enter the HTTP headers that are allowed for CORS: Origin, Content-Type, X-Auth-Token, X-Requested-With, Accept, Authorization, X-CSRF-TOKEN, X-Socket-Id
? What do you want this config to be saved as? laravel-echo-server.json
appId: f526a01938a20971
key: 7e9ec298b4b10e8dccc0e5c930bb8c94
Configuration file saved. Run laravel-echo-server start to run server.
```

作成された`laravel-echo-server.json`を見るとの"redis"の部分が{}になっていると思います。
この部分を下記のように編集します。

```laravel-echo-server.json
{
	"databaseConfig": {
        // この部分を記入する
		"redis": {
			"scheme" : "tls",
			"host" : "localhost",
            // redisを起動した時に表示されたポート
			"port" : "6379"
            // config/database.phpのredis.options.prefixの値
            "keyPrefix": "laravel_database_"
		},
	},
}
```

注意してほしいのは`keyPrefix`の部分です。
この部分はconfig/database.phpのredis.options.prefixに入る値と同じ値を設定するようにしてください。

```php:config/database.php
<?php
use Illuminate\Support\Str;
return [

　   　　・・・

    'redis' => [
        'client' => env('REDIS_CLIENT', 'phpredis'),
        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'redis'),
            // ここに入る
            'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
        ],

　　　   ・・・

];
```
少しややこしいですがこれで完成です。
ちなみにここで作成されたlaravel-echo-server.jsonは環境ごとに異なるので.gitignoreに追加しておくことをおすすめします。


## Laravelの設定
次にLaravel本体の設定をしていきましょう。

まずは`config/app.php`に記述されている`App\Providers\BroadcastServiceProvider::class,`のコメントアウトを外します。

```php:config/app.php
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// ここのコメントアウトを外す
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
SimpleSoftwareIO\QrCode\QrCodeServiceProvider::class,
App\Providers\ViewComposerServiceProvider::class,
```

そして次に.envを編集します

```.env
// 両方とも既にある場合はredisに変更、そもそも記載ない場合は追記
BROADCAST_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=predis
```

忘れずにキャッシュの更新を実施してくださいね。

```
$ php artisan config:cache
```

## 通知するイベントを作成
ここまで来たら次は実際にFlutterアプリに通知するイベントを作成します。
イベントは`php artisan make:event イベントのクラス名`で作成できるので任意の名前で作成してください。

今回僕はお知らせに関する通知を送信したいので`NewInformationEvent`としました。

```
$ php artisan make:event NewInformationEvent
Event created successfully.
```

通知するチャンネル名と送信する値をこのファイルに記述していきます。
今回は

 - チャンネル名：test-channel
 - 送信する値：message

のように設定していきます。

また`implements ShouldBroadcast`をクラス名の後に追記します。

完成形はこのような形になります。

```php:php:app/Events/NewInformationEvent.php
<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

// implements以降を追加
class NewInformationEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        // チャンネル名
        return new Channel('test-channel');
    }
 
    public function broadcastWith()
    {
        return [
            // 送信する値
            'message' => 'テストです！',
        ];
    }
}

```

これでLaravel側の設定は完了です！！



# Flutter側の設定
次にFlutter側の設定をしていきましょう。
Flutter側でLaravelから送信されたブロードキャストをリッスンするには下記パッケージを使います。

https://pub.dev/packages/socket_io_client

https://pub.dev/packages/laravel_echo

## パッケージ導入

まずはパッケージの導入

```
$ flutter pub add socket_io_client
$ flutter pub add laravel_echo
$ flutter pub get
```

## 通知を受け取る設定

main.dartに下記記述を追記します。

```dart:main.dart
import 'package:laravel_echo/laravel_echo.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;

void main() async {

  ・・・

  // 追記
  Echo echo = Echo({
    'broadcaster': 'socket.io',
    // laravel-echo-server initで設定したホスト：ポート
    'client': IO.io('http://localhost:6001'),
  });

  // php artisan make:event イベントのクラス名で作成したイベント名とチャンネル名
  echo.channel('test-channel').listen('NewInformationEvent', (e) {
    print(e);
  });

  ・・・
}
```

公式ドキュメント通りですね。これで終了です！

# テスト

それでは早速テストしてみましょう。
、、、と思って`flutter run`したのですが、残念ながらLaravel EchoパッケージがNull Safetyに対応していないとのこと。

公式を確認するとNull Safety対応しているのはベータ版のみだったので仕方なくベータ版をインストールしました。
あと`socket_io_client`についても最新版を入れると動かなかったので別バージョンを入れました。

```pubspec.yaml
dependencies:
  // 変更
  laravel_echo: ^1.0.0-beta.1
  // 変更
  socket_io_client: ^1.0.1
```

```
$ flutter pub get
```

それに伴いmain.dartも少し変更します。

```dart:main.dart
void main() async {

  ・・・

  IO.Socket socket = IO.io(
    'http://localhost:6001',
    IO.OptionBuilder()
        .disableAutoConnect()
        .setTransports(['websocket']).build(),
  );

  Echo echo = Echo(
    broadcaster: EchoBroadcasterType.SocketIO,
    client: socket,
  );

  ・・・
}
```

再度`flutter run`

```
$ flutter run
Launching lib/main.dart on iPhone 13 in debug mode...
Running Xcode build...                                                  
 └─Compiling, linking and signing...                        10.0s
Xcode build done.                                           43.7s      
```

よし！！

## Laravelから通知してみる

無事ビルドできたのでLaravelから通知してみましょう。

まずLaravel側でRedis・Laravel Echo Server・キューを起動します。


##### redisの起動
```
$ redis-server
```

##### Laravel Echo Serverの起動
```
$ laravel-echo-server start

L A R A V E L  E C H O  S E R V E R

version 1.6.3

⚠ Starting server in DEV mode...

✔  Running at localhost on port 6001
✔  Channels are ready.
✔  Listening for http events...
✔  Listening for redis events...

Server ready!
```

##### キューワーカーの起動
```
$ php artisan queue:work 
```

特にエラーが発生しなければOKです。

Laravelから通知するには通知したいところで`event(new PublicEvent()); `で通知できます。

今回はテストなのでtinkerから通知してみたいと思います。

```
$ php artisan tinker
Psy Shell v0.11.5 (PHP 7.4.24 — cli) by Justin Hileman
>>> use App\Events\NewInformationEvent;
>>> event(new NewInformationEvent());
=> []
```

Flutterのコンソール

```
flutter: {message: テストです！, socket: null}
```

できた！！


# おわりに
初めてLaravel-echo-serverを使用してみたのですがかなり面倒臭いですね。。。
素直にPusherを使う方がいいかなと思いました。

もしうまくいかない・分からないなどあればコメントに書いていただければと思います。

ありがとうございました！