Flutter Webで「このサイトを離れますか？」を表示する(beforeunloadイベント)

# はじめに

[Flutter Web](https://flutter.dev/multi-platform/web)でスプラトゥーン2のプレイヤー向けの動画プレイヤーアプリを作成しました。プレイの録画ファイルをブラウザにドラッグアンドドロップすると、動画を開き、やられたシーンなど重要シーンを頭出しします。


<iframe width="560" height="315" src="https://www.youtube.com/embed/qH3e0y1un9E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


頭出しは画像認識で行っているので時間がかかります。そして、うっかりタブを閉じてしまった場合は、残念なことに、また最初からの頭出しになってしまいます。よって、動画を開いているときにタブを閉じたときはブラウザ標準の「このサイトを離れますか？」ダイアログを表示します。

![スクリーンショット 2021-12-05 6.47.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/94ea0e87-f8f9-a95e-3256-5e857b97fd14.png)

# 前提

## Flutterと状態管理に使用しているライブラリのバージョン

- Flutter 2.2.3-stable
- [hooks_riverpod](https://pub.dev/packages/hooks_riverpod) 1.0.0-dev.7
- [freezed](https://pub.dev/packages/freezed) 0.14.2

すこし古いのでバージョンアップしようとしたのですが、[Chromeによるデバッグ実行ができなくなる問題](https://github.com/rrousselGit/river_pod/issues/889)を回避できなかったため、現状動くバージョンでの解説になります。

## 動画ファイルの開閉状態

動画を開いている等のUIの状態はfreezedで作成したクラスとStateNotifierで管理しています。

```main_ui_model.dart
@freezed
abstract class MainUiModel with _$MainUiModel {
  const factory MainUiModel(
    bool isOpen /* trueの時は動画ファイルを開いている */,
    String fileName /* 開いた動画ファイルの名前 */
    /* 略 */
  ) = _MainUiModel;
}
```

```main_ui_model_state_notifier_provider.dart
class MainUiModelStateNotifier extends StateNotifier<MainUiModel> {
  /// 動画ファイルがドラッグアンドドロップ領域に落とされた
  /// [fileName] ファイル名
  void onDrop(String fileName) {
    state = state.copyWith(fileName: fileName, isOpen: true);
  }

  /// 動画ファイルを閉じる
  void onCloseFile() {
    state = state.copyWith(fileName: "", isOpen: false);
  }
}

final mainUiModelStateNotifierProvider =
    StateNotifierProvider<MainUiModelStateNotifier, MainUiModel>((ref) {
  return MainUiModelStateNotifier();
});
```

isOpenフラグがtrueの時のみ、タブを閉じたときに「このサイトを離れますか？」を表示したいです。

# JavaScriptでの実装方法

JavaScriptではbeforeunloadイベントを使って実装することができます。

**参考** [Window: beforeunload イベント - Web API | MDM](https://developer.mozilla.org/ja/docs/Web/API/Window/beforeunload_event)

```js
window.addEventListener('beforeunload', (event) => {
  event.preventDefault();
  event.returnValue = '';
});
```

# Flutter Webでの実装方法

Flutter Webではbeforeunloadイベントは[dart:htmlライブラリWindowクラスのonBeforeUnloadプロパティ](https://api.dart.dev/stable/2.15.0/dart-html/Window/onBeforeUnload.html)に実装されています。こちらは `Stream<Event>` クラスのインスタンスが取得できます。よって、[StreamProvider.autoDispose](https://pub.dev/documentation/riverpod/latest/riverpod/StreamProvider-class.html)を使ってこのように実装しました。

```confirm_tab_close_stream_provider.dart
final confirmTabCloseStreamProvider = StreamProvider.autoDispose<bool>(
    (ref) => window.onBeforeUnload.map((event) {
          // BeforeUnloadEventにキャスト
          final beforeUnloadEvent = event as BeforeUnloadEvent;
          // UIの状態を取得
          final mainUiModel = ref.read(mainUiModelStateNotifierProvider);
          // 動画を開いているときのみ「このサイトを離れますか？」を表示する。
          if (mainUiModel.isOpen) {
            beforeUnloadEvent.preventDefault();
            beforeUnloadEvent.returnValue = '';
          }
          // 流すインスタンスは何でも良い。
          return true;
        }));
```

こちらのStreamProviderをタイトル部分など常に表示されているHookConsumerWidgetで監視します。

```main_page.dart
/// タイトル部分
class TitleWidget extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 表示のための状態を監視
    final mainUiModel = ref.watch(mainUiModelStateNotifierProvider);
    // タブを閉じるときの確認担当。
    final futureConfirmTabClose = ref.watch(confirmTabCloseStreamProvider.last);
    // 長いアプリ名
    var title = Strings.titleLong;
    // 動画ファイルを開いている時は「短いアプリ名 - 動画ファイル名」にする
    if (mainUiModel.fileName.isNotEmpty) {
      title = sprintf("%s - %s", [Strings.title, mainUiModel.fileName]);
    }
    double fontSize = 19;
    final titleStyle = TextStyle(fontSize: fontSize, color: Colors.white);
    return Text(title, style: titleStyle);
  }
}
```