18年続く老舗マッチングサービスにFlutterを導入しかけの話

これは[Diverse Advent Calendar](https://qiita.com/advent-calendar/2018/diverse)の5日目の記事です。

昨日は @k-motoyan さんの[DockerのOpenCVでHaar-like特徴分類器を使うとき](https://qiita.com/k-motoyan/items/3230c45c803be85a6d25)でした。
顔検出ができれば、顔に合わせてトリミングしたり補正をかけたりすることで、プロフィールの写真をもっとよく見せることも可能ですね！
私ももっと画像処理系とか勉強したい…！


# まえおき

![yb x flutter.png](https://qiita-image-store.s3.amazonaws.com/0/41896/9755ae65-67c3-0368-ee08-b6b62e243827.png)


弊社Diverse（ダイバース）では[youbride（ユーブライド）](https://youbride.jp/)という婚活サービスを運営しております。
このyoubride、すでに18年間も運用され続けており、日本の中では老舗と言っても間違いないサービスです。

このyoubrideのスマートフォンアプリを、Flutterで作り直す、という話が持ち上がり、そして実行に移されています。
経緯の話は以前にDiverse開発者ブログで公開しておりますので、こちらをご覧ください。

* [Flutter Meetup #4をホストしました＆発表しました](https://developer.diverse-inc.com/entry/2018/09/07/110941)


さて、導入が決まって…その後どうなったのでしょうか？
今日はそんな「導入しかけ」の話です。


# 構成

本題に入る前に、youbride特有の過渡期の構成を説明します。
なぜなら、作り直すのはスマートフォン用クライアントアプリだけではないためです。

![Untitled (1).png](https://qiita-image-store.s3.amazonaws.com/0/41896/ee529b9d-5f8c-8c68-eef8-b722aa47ea3c.png)


## 新クライアント
今回Flutterで作る対象です。
初めはAndroidでリリース予定、iOSの課金などプラットフォームの機能に対応できればiOSでもリリースするかも？

サーバとはgRPCでやり取りする予定。

## 旧クライアント（Android, iOS）
Android版とiOS版が別々のソースで存在します。
元々は、Android版のコードベースが複雑になりすぎ、保守コストが容認できる限界を超えてしまったためAndroid版だけをリプレースする予定でした。
しかし「Material Designでも意外とiOSイケる」ということ、保守コストも低減できるということから、iOSもFlutterの新クライアントに統合する話が持ち上がっています。

サーバとはHTTPを使ったREST（っぽい） APIでやり取りしています。

## 新APIサーバ

新クライアント以外に新設が予定されているものです。
Rubyです。

クライアントとはgRPCでやり取りする予定。

現在は開発段階のため、開発者の手元でdockerを使って動かします。

## 旧サーバ

Perlです。
ドメインロジック、Webページ（PC向け、スマホ向け、ガラケー向け）、APIサーバを担当しています。

今回、APIサーバ機能を新APIサーバに移譲し、ドメインロジックの処理とWebページ側の機能に専念してもらう予定です。

クライアントにはREST（っぽい） APIと、アプリ内で表示するWebページを提供していました。

社内に結構リッチな開発用サーバ環境が整っており、常にhttpsでつなぐことができていました。


# 新クライアント視点の要求

今回説明する項目に関係あるものだけ。

* gRPCで通信できる必要があります
    * そして、新サーバはまだローカルでしか動かないためTLSなしの通信ができないといけません
* WebViewを表示する必要があります
    * youbrideでは、法令上すぐに表示を変更する可能性が比較的高い場所、デザインの更新が頻繁に行われると予想される画面を、WebViewをメインとした画面で構成してきました
    * これをネイティブで実装し直す余力はないので、新クライアントでもWebViewのまま扱います
* プラットフォームの機能を呼び出す可能性があります
    * pluginを作ればいいのですが、youbrideの中でしか使わないロジックのうち、どうしてもプラットフォーム上でないと実現できないものをどこにしまっておくのか


# 挑戦している事柄

[経緯を語った段階](https://developer.diverse-inc.com/entry/2018/09/07/110941)でも調査はある程度していたのですが、やはり実際に取り掛かると大変だったりすることがあります。
逆に、すんなりできてしまって万歳を叫ぶ場面もありました。

そんな悲喜こもごもな事柄を紹介します。

## gRPC

[gRPC](https://grpc.io/)はprotoファイルにAPIのスキーム定義を記載しておくことで、リクエストとレスポンスのエンティティと通信クライアント部分を自動で生成できます。

protoファイルは専用のリポジトリで管理しております。
このリポジトリのmaterへのマージがあると、サーバとクライアントのリポジトリに変更されたエンティティと通信クライアントのプルリクエストが自動的に作成されるようにCIを整備しました。

[Dartは公式にgRPCに対応している](https://medium.com/dartlang/announcing-official-grpc-support-for-dart-6c9b50357af6)（と思われる。後述）ので、Flutterプロジェクトに対しても依存関係を記述するだけで簡単に導入することができました。

一つ解せないのが、[公式対応している](https://medium.com/dartlang/announcing-official-grpc-support-for-dart-6c9b50357af6)とアナウンスがあるのにAPIリファレンスが存在しないことです。（2018年12月4日現在）
[gRPCのドキュメントのページ](https://grpc.io/docs/)のみならず、[pubにも存在しません](https://pub.dartlang.org/documentation/grpc/latest/)。

サーバ - クライアント間の通信となると認証や認可がつきものですから、トークンを通信に乗せる必要があります。
REST APIですとHTTPのヘッダに乗せるのが一般的です。
gRPCではMetadataを一回一回の通信に付属できるのですが、DartでのMetadataを設定するAPIがわからず、結局ソースを読みに行きました。

また、通信についてもTLSを使用する場合、しない場合でオプションを変える必要があります。
こちらは[ドキュメントにTLSを使用しない例が載っていた](https://grpc.io/docs/tutorials/basic/dart.html#creating-a-stub)ため、すぐに解決しました。


## flutter_webview_plugin

[flutter_webview_plugin](https://github.com/fluttercommunity/flutter_webview_plugin)は現状、FlutterでWebViewを使用する際のデファクトスタンダードとなっているpluginです。

Flutterはすべての画面描画をFlutterViewという、自前で画面描画するためのView（AndroidだとSurfaceView、iOSだとUIViewにCoreGraphicsを使って描画）を画面いっぱいに引き伸ばしたものを使って行っています。
Flutterの世界は基本的にFlutterViewの中で完結しており、FlutterViewは一つのアプリケーションの中に一つしか存在しません。

現状ではflutter_webview_pluginはプラットフォームのWebViewを**FlutterViewの上に重ねて表示する**という荒業でWebViewの表示を実現しています。
そしてその荒業ゆえか、WebViewのインスタンスはシングルトンで、画面中に一つしか表示できません。

![Flutterとflutter_webview_plugin.png](https://qiita-image-store.s3.amazonaws.com/0/41896/25941955-0779-0e6e-c5d9-38119d31c36b.png)


そして、そんな構成になっているため、当然のことながら以下のことはできません。

* WebViewの表示域上にダイアログを表示する
* Widget内で、WebViewの上に重なるWidgetを配置する
* （MaterialAppの表示域内で、）Navigationで画面遷移する際、遷移元と遷移先両方の画面でWebViewを使用する
    * 厳密には無理ではないですが、NavigationのpopとWebViewのbackを連動させるとか工夫しないといけない

上記全て、youbrideでは起こりうるパターンです。
プラットフォームの機能だけで実装されていた旧クライアントでは問題なくても、Flutterを使った新クライアントでは問題になります。


### 次の手段案

今朝の[Flutter 1.0のリリース記事](https://developers.googleblog.com/2018/12/flutter-10-googles-portable-ui-toolkit.html)と同時にiOS対応がアナウンスされたPlatform Viewsという機能で改善される可能性があります。
Platform ViewsはFlutterのWidgetツリーの中にプラットフォームのView / UIViewを表示する（ように見せてくれる）機能です。
Widgetツリー内なので、PlatformViewの上に重なるようにFlutterのWidgetを表示できます :raised_hands:

[これを使ったwebviewの開発が進んでおり](https://github.com/flutter/plugins/tree/master/packages/webview_flutter)、ちょうど今日からyoubrideチームでも検証を始めました。
まだハイエンドマシンでないとスクロールがカクついたり、タップへの反応が体感で0.5秒ほど遅かったりするので、パフォーマンスチューニングが進めばおそらくこれを採用することになると思います。


## 画面などの状態の管理

当初は[ScopedModel](https://pub.dartlang.org/packages/scoped_model)を使用しようとしていました。

しかし上記のflutter_webview_pluginがシングルトンであるため、ScopedModelでその初期化の待ち合わせまで管理してしまうと、結局Modelの再利用ができません。

現在のところは1画面を構成する包括的なWidget（我々はPageと呼んでいます）をStatefulWidgetで実装することにし、画面の状態はStateで管理するようにしています。
画面をまたぐような、アプリケーション全体的な状態の管理にはStateを使うのは難しい予感がしているため、そちらのケースではScopedModelなどを使用するかもしれません。

## プロダクト特有のpluginの管理

セキュリティに関連するため詳細はお伝えできないのですが、現在はpluginがないプラットフォームの機能を使用する機会がありました。
ドメインにべったりな機能であるためOSS化して公開するつもりのない、プロジェクト固有のプラグインをどう管理するか、議論になりました。

結局のところ、youbride新クライアントのリポジトリでは、以下のようにディレクトリを切ってpluginを管理しています。

```
youbride-client/
　├ android/
　├ images/
　├ ios/
　├ plugins/                    ←固有プラグインを入れるディレクトリ
　│　└ awesome-youbride-plugin/
　└ test/
```

プロジェクト固有でないものについては、OSS化して公開することにしています。
まだOSS化したいpluginは出てきていませんが、そのうちFlutterコミュニティに還元していきたいものです。


# おしまいに

実プロダクトに導入するとなると、やはり公開されているlibraryやpluginでは要件を微妙に満たしてくれなかったり、使う上で不便な箇所を見つけたりすることがあります。

逆に、（特にMaterial Design Componentについては）期待した以上に思い通りに動いてくれる場合もあり、大いに助かっている箇所もあります。

youbrideは新クライアントリリースに向けて、この後も突き進んでいきます＼\ ٩( 'ω' )و /／

明日は @imaizume さんがisIPhoneXフラグをやめるためにやったことを書いてくれます。
iPhoneX対応、大変ですよね…ノッチとか…わかる…Androidもノッチが2つある端末とか出たんで…