Diverse developer blog

株式会社Diverse(ダイバース) 開発者ブログです。

BAR DUNBARのデジタルサイネージをFlutterでシステム化した話

こんにちは。Diverse developer blogです。

今回は、Diverseが運営する「BAR DUNBAR」の店舗内に常設しているデジタルサイネージをシステム化したいという要望に対して普段はアプリやWebなどを主戦場としていたエンジニアがどのようにしてシステムを完成させたのかというお話をさせていただきます。

デジタルサイネージシステム完成イメージ

www.youtube.com

本記事を読み進めやすくするために、まずは完成した動画をご覧ください。 2台のディスプレイでは、通常動画が常に同じ再生位置で流れています。 そしてリモコンのボタンを押すと、両画面が同時にお祝い動画へ切り替わります。 この記事では、この仕組みをどう作ったのかを順を追ってお話しします。

BAR DUNBARについて

「BAR DUNBAR」はDiverseが運営する、お客様にお酒だけではなく、人とのつながりや共感を提供し “バーコミュニティ”という文化をより楽しんでもらうためのコミュニティバーです。

コミュニティバー「DUNBAR」をオープンしました | 株式会社Diverse

■ Instagram
https://www.instagram.com/bar_dunbar/

■ 食べログ
https://tabelog.com/tokyo/A1316/A131601/13299543/

■ 住所
東京都目黒区下目黒2-20-28 いちご目黒ビル 2F
https://maps.app.goo.gl/ZjAQBnfRsU53Mug4A

目次

タスクに取り組む前の不安と予測

まず前提としてこのタスクに取り掛かるにあたり、完成までの道筋が全く想像できないというのが率直な感想でした。

デジタルサイネージといえばコンビニなどに常設されている店内動画であるとか、街中にある動画広告のようなイメージしかなく実際にどのような仕組みで運用されているのか理解できていないままタスクに取り組むこととなりました。

今思い出すと複雑に考えていたなと振り返るのですが、当時の私が話を聞いた時点で考えていたのは
管理画面や配信サーバーがあり、それらを介して現地の再生端末を制御するものなのでは?
とイメージしていました。

要件/要望のヒアリング

タスクに取り組むことが決まり、チームメンバーも決まった翌日に早速現地でヒアリングを実施しました。

※その時点でのデジタルサイネージの運用はyoutubeにアップロードした動画を
左右それぞれのディスプレイで自動ループ設定にして流しつづけるという
原始的な運用方法がとられていました。

ヒアリングした結果出た要件/要望は以下の4つでした。

  1. 左右のディスプレイに流れている動画の時間軸を同期させたい(要望)
  2. お客さんの誕生日等の特別なイベント時に、乾杯のタイミングで通常動画から乾杯用の動画へすぐに切り替えたい(必須要件)
  3. 特別な動画に切り替える作業については調理や接客でとても忙しいバーテンダーさんが可能な限り負担なくできるものにしたい(強い要望)
  4. 常時流している通常動画を周年や季節が変わるタイミングで変更したい(要望)

この話を聞いた時点で「イベントが発生したタイミングで手動で動画をすぐに切り替えたい」という要件が入っていたため

  • お祝い動画の再生をシームレスに切り替えられること
  • 容量の大きい動画ファイルへネットワークを介さずにアクセスできること
  • 動画キャッシュ機構の利用しやすさ(そもそもローカルに動画があればキャッシュを気にする必要も無くなる)

といった点を考えると、Webで構築するよりも、動画ファイルをローカルで扱いやすいアプリとして構築した方が相性が良いと考えました。

【今回の対象となるサイネージ用のディスプレイ】

現地調査と開発言語の決定

その前提を踏まえた上で、次に取り組んだのが、サイネージ用のディスプレイに私たちが独自に開発したアプリをインストールすることができるのか?という疑問を解決することです。

現地調査した結果、幸いにもディスプレイにはAndroid TV OSが搭載されており、Google Play Storeも存在していました。さらに開発者オプションも有効にすることができたため、ADB経由でサンプルアプリをインストールできることを確認できました。

【店舗に設置されているディスプレイのスペック】

- モデル: Sony BRAVIA Professional Display
- 画面サイズ: 43インチ / 4K
- OS: Android TV 11
- 映像処理チップ: Realtek RTD シリーズ (H.264 ハードウェアデコーダ内蔵)
- GPU: ARM Mali
- 設置台数: 2台
- 設置向き: 物理的に90°反時計回り回転で壁掛け

【開発者向けオプションの有効化】

実際にアプリのインストールが可能であると判断できたため、次に決めるべきことはアプリを開発するための言語をどうするのか?ということです。

結論からいうとDiverseで開発実績もあるFlutterを採用することにしました。 今回はAndroidアプリの開発ということで単一コードでマルチプラットフォームで動かせるというFlutterの核となるメリット自体は活かせないですが、社内にはFlutterに精通したメンバーが多数いるのと、知見も豊富なため、今後のメンテナンスも考慮し、Flutterを採用しました。

動画の再生機能から着手

まずはローカルにてAndroid EmulatorとAndroidの実機を用いて開発に着手しました。

動画の再生には video_player パッケージを利用しています。ただし一つ制約があり、店内に置かれているサイネージ用ディスプレイは物理的に90°反時計回りに設置されているため、動画をそのまま再生すると異なる向きで再生されてしまいます。

開発当初は動画の再生widgetをRotatedBox(quarterTurns: 1)で制御しようと考えていました。しかし動画レイヤーにはNative SurfaceViewを使う必要があるにもかかわらず、Flutter widgetの回転はNative SurfaceViewには伝播しないという制約があります。

そこで試行錯誤した結果、widgetの回転ではなく動画自体をFFmpegで90度時計回り(CW)に回転してlandscapeとして保存するという原始的な手法を採用することになりました。

動画再生の落とし穴(1)

ローカルでの動画の再生については一切問題なかったのですが、現地に赴き試してみると画面が真っ黒になりいつまで経っても動画が再生されないという現象が起こりました。

通常のアプリであればVideoPlayerControllerのinitialize()が失敗すれば何かしらのエラーメッセージがLogcatに出るはずです。しかしLogcatには何も出ず、デバッグの手掛かりが何も掴めないという不測の事態となってしまいました。

そこから数時間試行錯誤した結果、私達は一つの仮説に辿りつきました。

「Realtekハードウェアデコーダは高ビットレートのH.264ストリームと相性が悪いのではないか?」

というものです。

ためしにFFmpegで動画のビットレートを5Mbps程度に落としたところ仮説は見事に当たり動画が再生されることを確認できました。

「Realtek」とは半導体チップを作っている台湾の会社のことであり
ディスプレイで使用しているチップ(Realtek RTD シリーズ)には
映像デコーダや音声処理、HDMI入力処理、CPU, GPUなどが全部詰まっています。
本記事では便宜上、このディスプレイに搭載されているRealtek製SoC/デコーダ周辺の挙動を
まとめて「Realtek」と呼んでいます。

動画再生の落とし穴(2)

しかし動画は再生できるものの、Logcatには「MediaCodec.setOutputSurface: IllegalArgumentException」というエラーメッセージが永遠に出続け、安定して動画が再生されない現象が再現されました。

調査した結果Flutterが内部でtextureの再バインドを試みる際、Realtekデコーダが setOutputSurface() を拒否するためということが分かりました。 これを解決する策は video_player のVideoViewType.platformViewを使うことでした。

VideoPlayerController.file(
  file,
  viewType: VideoViewType.platformView,
)

これでネイティブSurfaceViewに直接描画する形式になり、texture viewの再バインドは走らなくなりました。

動画再生の落とし穴(3)

まだまだ落とし穴はありました。Flutterの新レンダラーであるImpellerを有効にした状態でBRAVIAのMali GPU+Platform Viewを扱うと、動画の再生処理中に黒画面になってしまうことも分かりました。こちらはImpellerを無効にすることで、従来のレンダラーである Skia/GLES にフォールバックさせることで解決する事ができました。

<meta-data
    android:name="io.flutter.embedding.android.EnableImpeller"
    android:value="false" />

以上の3点の問題を解決することで、無事、サイネージ用のディスプレイで通常のループ動画を安定して流すことができるようになりました。

※ローカルでは上手くいっていることを現地で実際に試してみると、予期せぬトラブルが往々にして発生することを身をもって学ぶことができました。

複数ディスプレイでの動画の再生位置の同期の手法

複数のディスプレイで再生されている動画の再生位置を同じにしたいという要望を実現するために2つのアプローチを検討しました。

(1) 中央サーバー方式

1つ目は動画の再生を制御する中央サーバーを用意し、ディスプレイをスレーブとすることで、一元管理する手法です。

                 [店内 LAN]
                     │
        ┌────────────┴────────────┐
        │                         │
   [管理サーバー]              [BRAVIA #1]
   (Raspberry Pi 等)          (Flutter アプリ)
        │                         ↑
        │                         │ WebSocket
        ├─────────────────────────┤
        │                         │
        ↓                         ↓
   [BRAVIA #2]               [BRAVIA #N]
   (Flutter アプリ)          (Flutter アプリ)

こちらの方式を採用すれば再生位置の同期だけではなく、再生する動画の種類を選択できることや、動画の切り替えなども管理画面を通していつでも制御できるという強力なメリットがあります。

しかし、サーバーの用意や別途管理画面を用意したり、アプリ側にもWebSocketによるイベントの制御を行う必要があり、今の規模を考えると少し大袈裟なシステムであると感じたため、このアプローチは見送ることになりました。

(2) NTP-based 分散方式

2つ目は各ディスプレイが独立してNTPに現在時刻を問い合わせ、共通の時計を基準として再生位置を決定する方式です。

                     ntp.nict.jp
                          |
              (各機が独立に問い合わせ)
                          |
        +-----------------+-----------------+
        |                 |                 |
        v                 v                 v
    BRAVIA #1         BRAVIA #2         BRAVIA #N
 ntp offsetを保持  ntp offsetを保持   ntp offsetを保持
   (Flutter app)     (Flutter app)     (Flutter app)
        |                 |                 |
        +-----------------+-----------------+

こちらの方式が優れている点は外部依存がNTPサーバーだけになるので、構成が比較的シンプルになることです。ディスプレイの管理台数が2台と少ないため、店舗運用のシンプルさを優先し、NTP方式を採用することになりました。

動画の再生位置は (NTP現在時刻 mod 動画長) で決定します。

同期時刻の調整

NTP同期は構成としてはシンプルですが、実用上ほぼ同期しているように見せるには、いくつかの調整が必要でした。

(1) 外れ値の除去

まず NTP から返される時刻は、起動のタイミングによっては外れ値が混じることがあったので、アプリの起動時に ntp.nict.jp を 5 回叩き、その中央値をオフセットとすることで外れ値を除去する調整を入れました。

(2) 累積ドリフトの調整

また通常動画をループしていると微妙に端末時計に差分が発生し、累積的に位置ずれが発生するため30分ごとにNTPを再同期する調整もいれました。

*ドリフトとは、各端末の時計や再生位置が
時間経過とともに少しずつずれていく現象を指します。

(3) ドリフト調整の3段階補正

それでも微妙にずれることがあったので15秒ごとに「期待位置」と「実際位置」を比較し、差分に応じて補正する対応も入れました。

  • < 50ms: in sync (何もしない)
  • 50〜1000msソフト補正 → setPlaybackSpeed(1.0 ± up to 2%) で緩やかに補正
  • ≥ 1000msハード補正 → 即 seek + speed を 1.0 にリセット

ソフト補正で速度を数%変えても、視聴者にはほぼ気付かれないくらいの調整となります。ハード補正を行うと動画が一瞬だけ飛んだように見えますが動画をじっと注視していない限りは意外と気付かれない程度のものとなります。

(4) ループ境界での再同期

動画が末尾に到達して先頭に戻る瞬間に端末ごとに数ms〜数百msのズレが発生し、それが累積する現象もありました。これを検出するために、「再生位置が末尾5秒より後 → 先頭5秒より前」というジャンプを検知したら、その瞬間にNTP位置に再seekするロジックも入れています。

計測結果

経過時間 expected actual diff 状態
00:00:00 Initial sync: seek to 148,230ms (of 352,233ms)
00:00:15 163,241 163,229 +12 ms in sync
00:02:30 298,241 298,219 +22 ms in sync
00:05:00 95,775 95,793 −18 ms in sync
00:07:30 245,775 245,750 +25 ms in sync
00:10:00 43,309 43,317 −8 ms in sync
00:12:30 193,309 193,322 −13 ms in sync
00:15:15 338,309 338,243 +66 ms speed=1.0044 (soft)
00:17:30 128,843 128,830 +13 ms in sync
00:20:00 278,843 278,857 −14 ms in sync
00:22:30 76,377 76,349 +28 ms in sync
00:25:00 226,377 226,371 +6 ms in sync
00:27:30 24,411 24,390 +21 ms in sync
00:30:00 174,411 174,415 −4 ms in sync
【実測精度】
これらの調整を入れた状態で30分連続稼働させて計測すると位置ずれの平均数値は19〜48msとなり
一般的な体感として違和感が出にくい目安である100ms未満に抑える事ができました。

お祝い動画の切り替えイベントの検知

デジタルサイネージシステムの必須要件として通常動画からお祝い動画へ切り替えられるというものがあり、かつ「調理や接客でとても忙しいバーテンダーが、IT操作を意識せずに、限りなく少ない負担で操作できるようにしたい」という要望もありました。

その話を聞いたときに複数のアイデアが思い浮かびました。

  • 調理場にある管理用のiPadに動画切り替え用のショートカット/アプリ/Webページを作成してタップしてもらう
    • 🔺画面切り替え、タップやスワイプ操作が発生し、思っているよりかは簡単ではなさそう
  • バーテンダーさんが身につけているSmartwatch用のアプリを作成して、そのアプリをタップしてもらう
    • 🔺Smartwatch用のアプリ開発/運用コストが発生、Smartwatchを瞬時に操作するのは意外に難しい、普段使いのSmartwatchを使うため機種の限定が難しい
  • AmazonDashのような専用の物理ボタン機器を購入し、ボタンを押してもらう
    • ❌外部機器購入コストの発生、専用ボタンの追加開発が必要、物理デバイスに依存するので気軽に試すことができない。

どの手法も、可能性としてはありだが、簡単に試すことができず、またバーテンダーさんの負担を限りなく抑えるという要望に応えるには不十分であり、もっとも頭を悩ませたところです。

テレビのリモコンは使えないのか? - 正解は意外と身近に存在する

イベントの検知手法について頭を悩ませていたところ、突然アイデアが舞い降りてきました。

「例えばテレビを見ているときに、地震が発生して、リモコンでニュース番組に切り替えるくらいの感覚で操作できればよいのではないか?」

リモコンが使えるという発想は元々なかったのですが、Android TV OSであること、チームメンバーと話している中でbluetoothを使えばいいのでは?というような話がでていたこと、操作の容易性を考えると結局は物理ボタンがいいと考えていた事、これらの思考が有機的につながり、最終的には最も身近にあったリモコンを使うという発想に至ることができました。リモコンが使えるのであれば外部機器の追加購入も不要であり、イベントの送信部分に関しても追加で実装する必要はなくなります。

さっそく実装の調査を開始したところHardwareKeyboard.instance.addHandlerを利用することにより最下層のレイヤーで全てのキーをキャッチできることが分かりました。さらにリモコンのいいところはチャンネル切り替えのための数字も使え、色ボタンもキャッチすることができるので、押すボタンによってお祝い動画の内容も変えるということの実現も容易になったということです。

お祝い動画の切り替えイベントの他機種への伝播手法

次に一方をお祝い動画に切り替えたらもう一方が連動して切り替わる仕組みの検討をすることにしました。もちろん原始的には一方をリモコンで切り替えて、続けてもう一方をリモコンで切り替えるという手法を取ることはできます。しかし調理や接客で忙しいバーテンダーさんが物理的に切り替える作業が必要なため、どれだけ早く操作しても1秒〜3秒のずれはどうしても発生してしまうこととなります。結果的に特別なイベントの演出の一体感を損なうことにも繋がりかねないため、できるだけ同時のタイミングで切り替えられる方式を模索することとなりました。

(1) WebSocket方式

まず検討したのはFirebase Realtime Database等の外部WebSocketサービスを利用し、各ディスプレイがイベントを常時subscribeする方式です。

【WebSocket方式】

 [リモコン (数字キー押下)]
              |
              v
          BRAVIA #A
    +--------------------------+
    | HardwareKeyboard が捕捉  |
    | -> Firebase RTDB に write|
    | -> ローカルでも event    |
    |    動画を発火 (同期)     |
    +------------+-------------+
                 |
                 | HTTPS / WebSocket
                 | write: /events/current = {
                 |   "event_id": "cheers",
                 |   "triggered_at": <ts>
                 | }
                 v
    ============= [インターネット] =============
                 |
                 v
        +-----------------------+
        | Firebase Realtime DB  |
        |   (Google Cloud)      |
        +-----------+-----------+
                    |
                    | subscribe 中の全機に push
                    | (WebSocket)
                    v
    ============= [インターネット] =============
                 |               |
                 v               v
            BRAVIA #B        BRAVIA #C
        +----------------+ +----------------+
        | onValue 発火   | | onValue 発火   |
        |    ↓           | |    ↓           |
        | event 動画発火 | | event 動画発火 |
        +----------------+ +----------------+

                    結果: 全機が同期して event 動画に切替
         (インターネット往復ぶんのレイテンシ: 50〜500 ms)

この構成は現実的ではありましたが、以下の懸念点がありました。

  • プロダクトの規模に対して外部サービスを経由させる必要性が薄い
  • インターネットを往復させるため50-500ms程度のレイテンシがどうしても発生してしまう
  • 一方のアプリでリモコンイベントの受信後、イベントの書き込み処理 → もう一方がその書き込みを検知する仕組みが必要となり、想定よりも実装が複雑になる

これらの理由により、WebSocketによる実装は一旦断念することとなりました。

(2) UDPブロードキャスト方式

次に検討したのはUDPブロードキャストを利用するという方式です。

UDPブロードキャストは、同じLAN上のすべての機器に、一斉に同じデータを送る仕組みとなります。送信側は個別の宛先を知らなくても、"ブロードキャストアドレス" (255.255.255.255 など) 宛に送ることで、そのネットワーク上の全機に届かせることができます。任天堂Switchのローカル通信であったり、ネットワーク機器がLANに繋がったときに「DHCPサーバーいませんか?」というような信号をブロードキャストで叩いているなど、身近な機器でも利用されている一般的な技術となります。

構成としては以下のようになります。

【UDPブロードキャスト方式】

    [リモコン (数字キー押下)]
              |
              v
          BRAVIA #A
    +--------------------------+
    | HardwareKeyboard が捕捉   |
    | -> UDP broadcast 送信     |
    | -> ローカルでも event      |
    |    動画を発火 (同期)       |
    +------------+-------------+
                 |
                 | UDP send
                 | 宛先: 255.255.255.255 : 5556
                 | payload: "event:xxxxxx"
                 v
    ================ [店内 LAN] ================
                 |               |
                 v               v
            BRAVIA #B        BRAVIA #C
        +---------------+ +---------------+
        | listen socket | | listen socket |
        |    ↓          | |    ↓          |
        | event 動画発火 | | event 動画発火  |
        +---------------+ +---------------+
        
           結果: 全機が同時に event 動画に切替
          (LAN 内なのでレイテンシは 数 ms 以下)

デジタルサイネージシステムにおいてUDPブロードキャスト方式が優れている点は

  • 追加機器の購入/外部サービスが不要であり、気軽に試すことができる
  • レイテンシは同じLAN内で通信することにより数ms以下に抑えることができる
  • 実装も比較的シンプル

というものがあります。ただしTCPとは異なり、到達保証や順序保証がないというデメリットもありますが、今回の用途においては許容できると判断したためUDPブロードキャスト方式を採用することになりました。

アプリサイズの肥大化に対応する

当初からの方針として、再生する動画はアプリの中にバンドルした上で参照することにしていました。ただし実際にバンドルしてみると通常動画が300MBもあったり、それに加えてお祝い動画も追加すると合計で500MBの巨大なアプリとなってしまいました。これでは開発 → deployというサイクルを回すたびに10分の待ち時間が発生してしまうこととなります。

肥大化に対応するために、動画自体はディスプレイ本体の内部ストレージにコピーした上でアプリはそのコピーを参照するという方式に変更したところ最終的には40MB程度に抑えることができました。

権限の落とし穴

「動画を端末ストレージに置く」だけなら簡単そうですが、Android 11以降のscoped storage制約が落とし穴でした。

adb shell mkdir -p xxx

で作成したディレクトリはshellユーザーの所有となります。この状態でファイルを配置するとアプリ側から

PathAccessException: Directory listing failed
(OS Error: Permission denied, errno = 13)

というエラーがでて読めませんでした。ディレクトリがshellユーザーの所有であるために権限がないということですね。

解決策としては動画を配置する前にアプリ側でディレクトリをapp所有で作った後に、adb pushで動画を配置するという方式をとることで解決できました。

インストールスクリプトの開発

これらの手順を踏みながらアプリのインストール作業を行うことは煩雑となるため

  1. adb/ ffprobe 等のツールのインストールチェック
  2. 実際に転送する動画がassets/videos以下に存在するのかをチェック
  3. ffprobeによってコーデック/解像度/ビットレートを自動でバリデーションする
  4. APKをビルド/インストールする
  5. インストールしたアプリを一瞬起動し、アプリの所有権限でディレクトリを作成する
  6. 動画をadb pushする

これら一連の作業を自動で行なってくれるツールを作成することにより、難しいことを考えなくとも一気通貫でインストール作業を完了できるようにしました。

通常動画素材の切り替え方式

要望の一つとして通常動画も季節やイベントによって切り替えたいというものがありました。

こちらはディスプレイ側に動画をあらかじめコピーしておき、アプリ側で月・日・時と動画ファイルのマッピング定義をアプリ側に持たせ、再生する動画をそのマッピング定義により決定するというロジックで実装しています。

class LoopScheduleEntry {
  final int month, day, hour;
  final String filename, label;
}

const List<LoopScheduleEntry> kLoopSchedule = [
  LoopScheduleEntry(month: 1,  day: 1,  hour: 0, filename: kDefaultLoopFilename,     label: 'default'),
  LoopScheduleEntry(month: 7,  day: 7,  hour: 0, filename: kAnniversaryLoopFilename, label: 'anniversary'),
  LoopScheduleEntry(month: 7,  day: 13, hour: 2, filename: kDefaultLoopFilename,     label: 'default'),
];

これにより、あらかじめ配置済みの動画の範囲であれば、周年や季節に応じた切り替えを自動化できるようになりました。

今後の課題

ただし、通常動画も固定するのではなく、更新したいという要望も追加で発生しているため、どうすれば簡単に更新作業が行えるのかという点についてはこれからの課題として残っています。

タスクに取り組むことで得られた学び

(1) シミュレーションでは完璧だったことが現場ではうまくいかないことが往々にしてある

本文でも触れていますが、ローカルで開発し、様々な検証を行い、準備万端の状態で店舗へ赴き、予測では数分で終わるだろうと思っていたことが、動画の再生が思うようにいかなかったり、予期せぬエラーに見舞われたりと実際には順風満帆に進行することはありませんでした。

準備は万全だったとしても、現場ではエラーが発生するという心構えをもって臨むということが大切だと学びました。

(2) 課題を解決する方法は意外とシンプル

タスクに取り組む前は、複数の外部サービスの利用や外部機器の追加購入をし、ネットワークを介して複雑なシステムを開発するようなものをイメージしていました。

しかし最終的に完成した設計は外部サービスを使わず、外部機器の追加購入も不要であり、外部ネットワークの利用も最低限なシンプルな設計にすることができました。

複雑に考えていたことが、一つ一つの要素を分解すると最終的には意外とシンプルな設計に落ち着くこともあることを学びました。

(3) AIは強力だが全てを解決しない。もっとも重要な部分は人間の発想が決め手になることもある

本プロジェクトはAIと相談しながら進行し、また実装についてもAIの力を借りています。比較的つまずくこともなく進行できたこと、また未知の技術に取り組めたことはAIの力によるところが大きいです。

ただし、AIは基本的には人間が入力した情報や質問によってアウトプットが左右される一面があり、人間が現場での状況や情報を全てAIに与えるのは実質的には不可能であり、それによってアイデアの元になる情報が欠けてしまうことがあります。

それに対して人間は現場での情報を自然に把握していたり、あえて言語化していない情報も無数に持っています。

リモコンを最終的なイベント送信デバイスとして利用できないか?という件はAIとの壁打ちをしている中では一切登場しないまま発想したものなので、人間の観察や発想の中から生まれてきたものだと考えています。(その発想に至ってからはイベント送信デバイスとしてのリモコンについてAIと壁打ちし、実装までをスムーズに進行することができました。ありがとうございます😊)。

最後に

普段開発しているサービスでは実装した機能が実際にユーザーにどのように使われているのか?とか、どのように喜ばれているのかということを生の反応として知るのが構造的に難しいという一面があります。

デジタルサイネージシステムは実店舗で運用され、お客さんの反応も即座に得られ、開発者と利用者の距離が近いので普段の開発とはまた違った開発体験を得ることができました。

今後もバーという実店舗と様々な技術を組み合わせることで店舗に訪れてくれたお客さんの満足度を高めるような施策に取り組んでいきたいと考えています。

NotebookLMでYYCの仕様を質問できるようにした

こんにちは、Diverse developer blogです。

はじめに

プロダクトの仕様について、エンジニアなら誰しも一度は「これってどういう仕様でしたっけ?」と質問された経験があると思います。

プロダクトを運営していると、仕様の全てを把握するのは難しく、仕様確認の依頼はエンジニアのところに集まってきます。その都度コードを読みに行くことになり、こうしたコストは地味に積み重なっていきます。

今回はそのやり取りを、NotebookLMに肩代わりしてもらう仕組みを整えました。

これまでの仕様確認フロー

これまでYYCでは仕様調査の一部を Devin に任せてきました。コードベースを一定理解しているAIに質問ができる、というのは非常に便利です。

エンジニア以外のメンバーが自分で仕様を確認できる手段ができたことで、エンジニアへの問い合わせが減り、運用としても定着していました。

運用見直しのきっかけ

しかしDevinの料金体系が変更され、これまで無料で気軽に使えていた範囲が有料化されました

cognition.ai

そのまま課金して使い続ける選択肢もありましたが、仕様確認の用途であればコストを抑える方法があるはず。

そう考え、代わりになるツールを検討した結果、NotebookLMを採用することにしました。

なぜ NotebookLMを選んだのか

判断の軸になったのは、次の4つの観点です。

1. 料金

Devin で行っていた仕様調査と同じことが、NotebookLM なら追加コストなしでできる。

2. Google Workspace との統合

弊社ではGoogle Workspace を使っているため、社員のアカウントとアクセス管理がそのまま利用できる。

3. Devinの良いところを継承している

具体的には次の2点

  • 引用付きの回答:答えの根拠(どのファイルのどの部分を参照したか)が明示されるので、AI の答えを鵜呑みにせず裏取りできる

  • 参照範囲の限定:NotebookLMにアップロードしたソース以外は参照しないので、ハルシネーションが起こりにくい

4. Google エコシステムの将来的な恩恵

NotebookLM と Workspace は Google が継続的に改善している領域で、便利に改善してくれる流れにも乗れる。例えば、今手動で行なっているNotebookLMのソースの更新も、将来的に自動化できる可能性があり、新しいツールや契約を増やさずに、できることが広がっていくという期待ができる。

これらを踏まえ、引き続きコストがかからず・安全に・コードベースで仕様確認ができるという点が決定打でした。

NotebookLMにソースコードを渡したいが、膨大なコードは「そのまま」では渡せない

NotebookLMにはソース1ファイルあたり約 50 万語という上限があります。

support.google.com

一方、YYCは長年培ってきた大量のコードを抱えており、様々なコードが入り混じっています。とても1ファイルに収まる規模ではありません。

NotebookLMのソースとしてプロダクトのコードをアップロードするには、「単にコードを集めるツール」ではなく「何を含めて何を含めないかを決める、適切な単位に分けて渡せるツール」が必要でした。

Repomixを使ってNotebookLM向けにコードを分割する

この問題を解決するために、Repomix を使って NotebookLM 向けのコードパックを生成しつつ、その分割方法を設計しました。

github.com

Repomix とは、リポジトリのファイル群を1つのテキストファイルに集約してくれるCLI です。includeやignore のglob指定、出力フォーマット選択(plain / xml / markdown)、コメント圧縮といった「LLM向けにコンテキストを渡す」前提の機能が揃っています。

まずは「どのコードを含めて、どの単位でファイルに分けるか」を整理することから始めました。実際の思考フローは、次のような順で進みました。

  1. 容量制限

    → ソース1ファイルあたり約50万語という上限を超えないようにする

  2. 不要コードの除外

    → 仕様確認に使わないコード(テストコードなど)は含めない

  3. 残すコードの判断軸

    →「仕様やSQLの質問に答えられる」を満たす範囲で取捨選択

  4. サーバーとクライアント両方の参照

    → 画面の情報を参照しつつ、裏側のサーバーの仕様も答えられる構成にしたいが、両方を1ファイルにまとめると上限を超えてしまう

  5. 分割方針の決定

    → サーバーは論理レイヤーで複数ファイルに切り分け、クライアントは1ファイルに収める

この結果、サーバー側5ファイル + クライアント側1ファイルとして書き出し、それらをすべて同じノートブックにまとめてアップロードする構成に落ち着きました。


設計はこれで決まったので、あとは実行できる形にまとめるだけです。

弊社ではMakefile にターゲットを作成し、ファイルを書き出す運用にしました。

以下は実際に Makefile に追加した内容です(一部抜粋)。

REPOMIX_OPTS := --style plain --compress
REPOMIX := npm run --silent repomix --
REPOMIX_DATE := $(shell date +%Y%m%d)

repomix: 
    @echo "==> server/Data層 (+本番スキーマ)"
    @$(REPOMIX) $(REPOMIX_OPTS) --output repomix-server-data-$(REPOMIX_DATE).txt \
        --include "Data層ファイル, スキーマ" \
        --ignore "***/**/*"
    @echo "==> server/Model層"
    @$(REPOMIX) $(REPOMIX_OPTS) --output repomix-server-model-$(REPOMIX_DATE).txt \
        --include "Model層ファイル" \
        --ignore "***/**/*"
    @echo "==> server/Handler+Site層"
    @$(REPOMIX) $(REPOMIX_OPTS) --output repomix-server-handler-$(REPOMIX_DATE).txt \
        --include "Handler+Site層ファイル" \
        --ignore "***/**/*"
    # ... 管理画面系、その他、クライアントと続く

分割の単位は以下の6つ

ファイル 含まれるもの
repomix-server-data-{date}.txt Data 層のソースコード + DB スキーマ
repomix-server-model-{date}.txt Model 層のソースコード
repomix-server-handler-{date}.txt Controller 層(リクエスト処理層)
repomix-backend-admin-{date}.txt 管理画面系のコード(複数の管理プロダクトを一括)
repomix-backend-misc-{date}.txt 上記以外のバックエンドコード(共通ユーティリティ等)
repomix-client-{date}.txt クライアント
💡 --compress と --style オプションについて

--compress
コード圧縮を有効にします。本質的な構造要素を抽出・保持しながら、実装の詳細を除外することで、LLM処理のためのトークン数を削減することができます。

--style
出力形式を指定できます。今回はNotebookLMが読みやすいplainで出力するようにしました。

実際の使い方

あとは make repomix で生成された6ファイルを NotebookLM のノートブックにドラッグ&ドロップでアップロードするだけです。

「メッセージを1通送るのに、ポイントはいくつ必要?」「足あとがつくのはどのタイミング?」「特定のユーザーのプロフィールを取得するSQLを教えて!」などの質問にコードを根拠にした回答が返ってきます。

まとめ

今回の取り組みで、Devinに頼っていた『コードベースの仕様確認』を追加コストなしで継続できるようになりました

ポイントは、NotebookLM のソース容量制限という壁を Repomix で突破できたことです。「何を含めて、どう分けるか」さえ一度設計してしまえば、あとは出力してアップロードという作業だけで、いつでも最新の状態に更新できます

また、Google Workspace 上で完結するため、アクセス管理も既存の仕組みをそのまま使えます。新しいツール契約や情シスとの調整なしに移行できたのも、大きなメリットでした。

NotebookLM 側ではソース自動同期に向けたアップデートも進められており、手動で行なっているソースファイルの生成も、いずれ不要になりそうな見込みです。

workspaceupdates.googleblog.com

こうしたエコシステム側の進化に合わせて、開発フローの中で自然に使い続けられる仕組みにしていきたいと思っています。

Diverseのジュニアエンジニア採用の魅力 ~ エンジニアをどう育てているか ~

こんにちは。Diverse developer blogです。今回は、Diverseで実施しているジュニアエンジニア採用について、どのような採用枠なのか、入社後にどう育成しているのかをご紹介します。

ジュニアエンジニア採用とは

ジュニアエンジニア採用は『業務でのエンジニア経験が未経験〜5年以内の方を対象にした採用枠』です。エンジニアとして今後成長できるポテンシャルのある方を、長期目線で育てていくことを前提にしています。

Diverseの自社マッチングサービス『YYC』は、累計登録ユーザー数が1,900万人を超えるプロダクトで、20年以上の開発・運営の実績があります。SNS、マッチング、ライブ配信の機能を持つこのプロダクトの開発チームに、サーバーサイドを主軸としながら他の領域にも挑戦していく、という育成方針でジュニアエンジニアは配属されます。

育成プログラムのステップ

入社から9ヶ月以降まで、段階的に目標を設定しています。後述するとおりDiverseに入社するすべてのエンジニアに共通するもので、記載している期間は目安です。

入社オンボーディング

働く上で必要なツールのセットアップと、Notionに自己紹介ページの作成をしてもらいます。最初の数日で、社内のコミュニケーションに参加できる状態を作ります。

試用期間中(0〜3ヶ月)

この期間は、社員とプロダクトの理解を最優先にしています。具体的には以下のような内容です。

  • 週次1on1
  • スクラムへの参加
  • プロダクトを実際に使う
  • 開発環境の構築
  • 簡単なスプリントタスクにトライ

いきなり大きなタスクを任せるのではなく、まずは「何を作っているチームなのか」「どんなユーザーが使っているのか」を知ってもらう期間として設計しています。

試用期間後(3〜9ヶ月)

プロダクトの仕様、事業目標とチーム目標の理解を深めてもらいます。社員のサポートを受けながら、スプリントタスクを自分で完了できるようになることを目指します。

9ヶ月以降

事業目標とチーム目標に関わる成果を出すフェーズに入ります。同時に、次のグレードへのスキルアップを目指してもらいます。

育成プログラムは研修か

カジュアル面談でよく聞かれる質問があります。「この育成プログラムは研修ですか?」という質問です。質問の背景には、早く現場に入って実務で自分を伸ばしていきたい、業務から離れた研修は避けたい、という気持ちがあるのだと思います。

先にお答えすると、この育成プログラムはすべて実務です。業務から切り離した研修プログラムではありません。週次1on1もスクラムへの参加もプロダクトを使うことも、Diverseのエンジニアが日常的にやっていることで、それに最初から加わってもらいます。

もう一つ重要な点があります。この育成プログラムはジュニアエンジニアのためだけに作ったものではなく、Diverseに入社するすべてのエンジニアに共通で受けてもらう内容です。

経験の浅い方は9ヶ月ほどかけて各ステップを進みますが、豊富な経験を持っている方であれば同じステップを1週間で終えることもあります。記載している「0〜3ヶ月」「3〜9ヶ月」「9ヶ月以降」はあくまで目安で、できる人は早く終わって次に進めますし、時間がかかる人はそれなりにかかる、というだけの話です。

なぜ経験を問わず、同じ育成プログラム(オンボーディングとも社内では呼んでいます)を受けてもらうのかというと、どれだけ業務経験があってもプロダクトの仕様や事業の特性、チームの雰囲気は入社してから理解する必要があるからです。プロダクトを実際に使う、スクラムに参加する、といった内容は経験豊富な方にとっても価値があり、むしろそういう方ほど早く吸収して次のステップに進んでいきます。

Diverseは1スプリント1週間のスクラムで開発しており、年間約48回のリリースサイクルを回しています(詳細は2023年の振り返り記事を参照)。この短いサイクルの中で、自分のペースで進んでいける設計になっています。

developer.diverse-inc.com

チーム開発の特徴

Diverseでは、Engineer・Designer・Biz・CS・QAメンバーが1つのチームとなり、スクラムで開発を進めています。

チームでは「なぜこの機能を作るのか」「どうすればユーザーに価値が届くのか」を議論します。ジュニアエンジニアとして入社した方も、このチーム議論に最初から参加します。実装だけを切り出して任せるのではなく、上流の議論にも触れてもらうことで、「何を作るべきか」を考える習慣を早い段階でつけてもらうためです。

開発環境

ジュニアエンジニアとして入社した方が触れる開発環境は以下のとおりです。

  • インフラ:AWS(ECS, Aurora MySQL8 など)
  • 構成管理:Terraform
  • 分析基盤:Looker / Re:dash
  • 監視:Mackerel / Sentry
  • CI/CD:GitHub Actions / CircleCI
  • その他:GitHub / Slack / Asana / Notion / Miro
  • 開発手法:スクラム開発
  • 言語やフレームワーク:Perl / Python(Lambda) / TypeScript(Vue) / Dart(Flutter)

サーバーサイドの言語にはPerlを利用しています。20年以上運営しているサービスですが、現在のYYCのバックエンドは最新のPerl5.40とDebian Bookwormで稼働しています。2024年にPerl5.8とCentOSから最新のLTSバージョンまでアップデートするプロジェクトを完了させています(詳細はこちらの記事)。

developer.diverse-inc.com

長年運営されているサービスと聞くとレガシー環境を想像される方も多いかもしれませんが、言語もOSも最新のLTSバージョンで稼働しており、新規で作るものはユースケースに応じてPythonやTypeScriptなども採用しています。

AIツールも、エンジニアはClaudeCodeやCodexを会社の経費で利用可能です。ジュニアエンジニアもAI使って開発することは可能です。

ジュニアエンジニア採用の実績

ジュニアエンジニア採用は2年半ほど前に募集を開始し、これまでに1名を採用しました。入社から2年が経過した現在、サーバーサイドエンジニアとして活躍しています

今後、ジュニアエンジニアとして入社する方は、この先輩社員をメンターにしながら一緒に働くことができます。同じ立場でスタートした社員が身近にいる環境は、「どうやって成長していけばいいのか」の具体的なイメージが湧きやすく、質問もしやすいはずです。育成ステップを実際に経験してきた人だからこそ伝えられる話があります。

メンターになる社員の経験談

ここからは、当社のジュニアエンジニアとしてどのような環境で働き、どのように成長していけるのかを、実体験をもとにお伝えします。


未経験の私がエンジニアとして活躍できるようになるまで

私は未経験からエンジニアとしてDiverseに入社し、現在3年目になります。以前はエンジニアとは全く異なる業界で働いていました。未経験からの転職では、「どんな環境でスタートするのか」が大きな不安になると思います。そこで、先ほどご紹介した「育成プログラム」の各ステップを、私が実際にどのように歩んできたのか。弊社でのリアルな成長のプロセスをお伝えできれば嬉しいです。

【STEP 1】試用期間中(0〜3ヶ月): 「ハテナ」だらけのスタートから、チーム開発の基礎へ

入社直後の私は、独学で少し触れていたもののターミナルのコマンド入力すらおぼつかない状態でした。環境構築や、文字を一行修正するだけのタスクでも時間がかかり、エンジニアとしてやっていけるか不安に感じていました。それでも、定期的な1on1やプロダクト理解のための説明会など、手厚いフォローがあったおかげで、まずは「何を学ぶべきか」を整理することができました。

入社2ヶ月目からは、管理画面のデザイン一新と改修を担当することになりました。実はこの時、エンジニアとしてこの改修をメインで担当したのは未経験の私一人でした。

「未経験の私に務まるかな?」と不安もありましたが、弊社には『Fail Fast, Go Higher — 大胆に試そう、成長しよう』という行動指針が根付いています。この価値観によって、気負いすぎず「まずはやってみよう」と前向きに取り組むことができました。

もちろん丸投げではありません。わからないことがあれば常に質問できる環境ではありますし、私が大胆に試せるようにしっかりサポートしてくださる安心感がありました。

一通りの実装を一人でやり切る責任感を早い段階で味わえたのは、この価値観を大事にする会社ならではの経験だったと思います。

【STEP 2】試用期間後(3〜9ヶ月): 膨大なコードに圧倒されながらも、ペアプロで一歩ずつステップアップ

入社半年が過ぎる頃には、実際のプロダクトに関わるタスクや、クライアントとのAPI連携なども任せてもらえるようになりました。

歴史のあるプロダクトゆえにコード量も膨大で、構造の理解に苦しむことも多かったです。そんな時は迷わず、先輩エンジニアに「ペアプログラミング」を依頼してタスクを進めることもありました。先輩エンジニアの実装の進め方を隣で見ることで、一人で悩んで進めるよりも何倍も速く成長できたと感じています。

また、自分一人では理解が難しい技術的なところでぶつかった時も、相談すると快く勉強会を開催してもらったりしました。個人の技術力ももちろん大事ですが、それ以上に「チームとしてどう成果を出すか」を弊社では大切にしているので、周りの知識をどんどん吸収して成長していくことを、みんながポジティブに応援してくれました。

【STEP 3】9ヶ月以降〜現在: 3年目の今、実感するエンジニアとしての面白さ

現在はマッチング機能だけでなく、ライブ配信機能などプロダクト内の多岐にわたる機能開発に携わっています。

弊社のプロダクトは非常に多機能なので、一つのサービスにいながら様々なアーキテクチャに触れられるのが、エンジニアとして大きな魅力です。

また、リリースした施策の結果を数値で共有してもらえる場もあり、自分の実装がどれくらいユーザーに届き、プロダクトを動かしているのかをダイレクトに知ることができます。

入社したての頃は「一行の修正」に必死でしたが、今では「プロダクトの成長を左右する機能」を任せてもらえるようになりました。自分のできることが増えていくにつれて、ユーザーやプロダクトに与えられる価値が日に日に大きくなっているのを実感できること。 それが今の私にとって、一番のやりがいです。

最後に、これから入社する皆さんへ

最初はわからないことだらけで当たり前です。私もそうでした。 弊社は、未経験でも早い段階から実際のタスクを任せてもらえ、自分の手でリリースまで一貫して経験できる環境です。複雑な仕様に頭を抱えることもありますが、それを乗り越えるたびに確実にレベルアップできます。

次は私が、皆さんが「大胆に試せる」ように全力でバックアップし、同じチームのエンジニアとして一緒に切磋琢磨し、高め合えるような関係を築いていきたいと思っています。皆さんと一緒に働ける日を楽しみにしています!

まとめ:ジュニアエンジニア採用で大事にしていること

育成にはそれなりにコストがかかります。先輩エンジニアが時間を割いて質問に答える、1on1を毎週やる、インプット期間中は大きな成果を求めない、といった投資が必要です。

このコストを払ってもジュニアエンジニアを採用している理由は、考え方の合う人材を中長期で育てていきたいからです。過去にジュニアエンジニアとして入社して成長したメンバーの実績もあり、先輩エンジニアは自分から聞きに行けば、基本的な質問でも答えてくれる文化があります。

また、Diverseには継続的な改善を怠らない姿勢があります。機能開発と並行しながらPerlとDebianのアップデートプロジェクトをやり切った実績からもわかるように、目の前の開発だけでなく、中長期でプロダクトと開発環境を良くしていくことを重視しています。ジュニアエンジニアとして入社した方にも、こうした改善の取り組みへ段階的に関わってもらいたいです。

逆に言うと、ジュニアエンジニアとして入社する方には『自分で開発したり、自分から聞きに行く姿勢』を期待しています。わからないことを放置せず、素直に質問できる方と一緒に働きたいと考えています。

最後に、ジュニアエンジニア採用は『未経験〜5年以内のエンジニアを、9ヶ月以降に成果を出せる状態まで育てていく採用枠』です。累計登録ユーザー数1,900万人・運営20年以上のプロダクトでスクラム開発に最初から関わりながら段階的にスキルアップしていくことができます。

ブログの内容やDiverseに興味のある方は、ぜひ以下の採用ページからカジュアル面談へ。様々なご意見をお待ちしています。

diverse-inc.co.jp

Figma MCP vs Pencilを実務で比較してみた話

こんにちは、Diverse Developer Blog です。

はじめに

YYCのフロントエンド開発では、Figmaデザインを基にしたUI実装が不可欠ですが、この工程には依然として多くの手作業が残っています。

近年、AIを使ってこの工程を効率化するアプローチが増えてきており、「どこまで実装を自動化できるのか?」が気になっていました。

そこで今回は、

  • Figma MCP
  • Pencil

の2つを対象に比較検証しました。

本記事では、「実装工数の削減」と「デザイン再現性」の観点から見えた、比較検証の結果を共有します。

ツールの特徴

まずは、比較する2つのツールの立ち位置を整理します。

Figma MCP

Figma公式が公開しているMCPサーバーを介し、LLM(Claude等)が直接Figma API経由でデザイン情報を取得する仕組みです。

  • 公式リポジトリ: https://github.com/GLips/Figma-Context-MCP
  • AIがFigmaデザインを読み取り、コード化するための参照用として設計
  • get_file(ファイル取得)、get_nodes(要素取得)、get_image(画像取得)など、デザイン情報取得に特化
  • 実行環境:クラウドベース

👉 既存のデザインをAIに参照させるための仕組み

Pencil (Pencil.dev)

Pencilは、AIエージェントとの連携を前提に設計されたデザインツールです。

  • 公式サイト: https://www.pencil.dev/
  • "Built for AI agents": AIによる操作を前提に設計
  • "Canvas as a Database": キャンバスが単なる画像ではなく、AIが編集可能なJSON構造データとして扱う
  • "Bidirectional editing": AIと人間が双方向に編集可能
  • 実行環境:ローカルファースト(.penファイル)、Gitで管理可能

👉 AIがデザインを読み取りつつ、直接編集・再構築できる環境

特徴まとめ(2026年4月現在)

観点 Figma MCP Pencil
役割 デザイン情報の参照 デザインの編集・再構築
AIとの関係 読み取り+書き込み 読み取り+書き込み
データの扱い API経由で取得 JSON構造データ(直接操作可能)
管理方法 クラウド ローカルファイル、Git
コスト Figmaプランに依存 無料

検証方法

実際のプロダクト開発で使用するFigmaデザインを対象に、以下の2パターンで検証しました。

パターンA:Figma MCP

  • Figmaデザインをそのまま使用
  • Claude Code(Opus 4.6)からMCP経由で参照し、コード生成

パターンB:Pencil

  • FigmaデザインをPencilに移行(ほぼコピペで再現可能)
  • Claude Code(Opus 4.6)からMCP経由でPencilデータを参照し、コード生成

対象

  • Webブラウザ(Vue3、SCSS)
  • モバイルアプリ(Flutter)

※ 同一UI・同一要件で比較

検証結果

Webブラウザ(Vue3、SCSS)

  • 両者ともに、UI構造はほぼ再現
  • 一覧画面のヘッダー・フッター固定なども、プロンプト指定なしで実装
  • 全体的にPencilの方がデザイン再現度は高い傾向
    • グラデーション
    • ボーダー有無
    • 余白やサイズなど細かいスタイル

一方で、以下の点は両者共通の課題として見られました。

  • アイコン画像の選定が不安定(生成ごとにブレる)
  • カラー変数・スタイル定数の参照が不安定

モバイルアプリ(Flutter)

  • 両者ともに、UI構造はほぼ再現
  • ベースのFigmaデザインのコンポーネント階層も再現した実装
  • Figma MCPの方がデザイン忠実度が高い結果に

また、Webと同様に以下の傾向が見られました。

  • アイコン画像の選定が生成ごとにブレる
  • カラー変数・スタイル定数の参照が安定しない

初回生成後の修正フロー

スタイルやアセット参照の課題については、プロンプトの工夫やskillsの整備により、おおよそ8割程度の完成度までは安定して出力可能になりました。

その後、主に以下の修正を行っています。

  • コンポーネント構成の最適化 (Figmaデザインと実装の設計差分や、エンジニアごとの粒度差による調整)
  • スタイルの微調整
  • ビジネスロジック(JS/Dart)など動的処理の実装

まとめ

再現性について

再現性は以下の3軸で評価しました。

  • UI構造(レイアウト・階層)
  • スタイル(色・余白・装飾)
  • アセット参照(画像・アイコン・変数)

その結果、

  • UI構造:両者とも高精度
  • スタイル:
    • Web → Pencilが優位
    • モバイル → Figma MCPが安定
    • リスト型のようなシンプルなUIでは差は小さい
  • アセット参照:両者とも不安定

現時点では「構造の自動生成」には極めて強いものの、「詳細なスタイリング」と「プロジェクト固有のアセット参照」には、人間によるコンテキストの補完(プロンプトでの補足や、skillsの整備、デザインシステム側での命名規則の徹底)が不可欠です。

実装工数削減について

今回の検証では、実際にプロダクトへ組み込む前提で評価しました。

  • どちらも生成コードをそのまま使うのは難しい
  • 特に以下の修正が必要になるケースが多い
    • アイコン差し替え
    • カラー変数の置き換え
    • コンポーネント分割・整理

一方で、UIの骨組みはほぼ完成しているため、ゼロから実装するよりは確実に工数削減になりました。

体感としては、

  • 50~80%程度の工数削減効果は見込める
  • “完全自動化”にはまだ届かない

という位置付けでした。

評価項目 Figma MCP Pencil 備考
UI構造の再現 両者ともに精度が高い
スタイル再現(Webブラウザ) ⚪︎ PencilのJSON構造がCSSと相性良
スタイル再現(モバイルアプリ) ⚪︎ Figmaの階層構造がFlutterと親和性高
アセット参照 アイコン画像やスタイル変数の紐付けに課題
導入の容易さ ⚪︎ Pencilは移行コストが発生
工数削減(体感値) 50~
80%
50~
80%
下書き生成ツールとして極めて優秀

最後に

アウトプットの品質自体に大きな差はありませんでしたが、ワークフローの観点では差が見られました。

  • Pencilはエディタ上でデザインを開きながら、AIと対話し修正できる
  • デザインをコードと同様にバージョン管理できる

これらを踏まえると、Pencilが無料である現時点では、品質・スピード・ワークフローに置いてメリットが大きいと感じました。

一方で、デザインデータの扱いについては運用設計が重要になります。 当社としても、将来的にPencilをマスターデータとして扱うことは現実的な選択肢の一つだと考えていますが、現時点では段階的な移行が前提になると認識しています。

特に、デザインレビューの進め方やデザイナー・エンジニア間の役割分担といった既存のプロセスを変えずにツールのみを移行すると、データの二重管理や認識の不一致が発生しやすくなります。

そのため当社では、まずはFigmaをデザインの正としつつ、Pencilを実装領域で活用する二層構造の運用を採用し、今後は新規開発領域から段階的にPencil中心のワークフローへ移行していく方針が現実的だと考えています。

出会いの「温度感」をマッチング検索に乗せるために Two-Tower Model と Solr KNN でやったこと

こんにちは、Diverse Developer Blog です。

今回は、YYC のマッチング検索に、SageMaker 上で学習した Two-Tower Model のユーザー Embedding を使ったベクトル検索 (Solr KNN) を追加した話をします。

チャンスタイムやデイリーミッションといった既存のルールベースの行動導線は引き続き動かしつつ、「自分と温度感が近い相手」を ML ベースの検索で並行して返せるようにする、という構成です。

TL;DR

  • これまで YYC のマッチング機会は、チャンスタイム(新規登録直後)やデイリーミッション(継続期)といったルールベースの行動導線で設計していました
  • ただ、ユーザーの目的が多様化するにつれて「全員一律の導線」では受け止めきれない部分が出てきたため、温度感 という軸を決定論的に定義し、それを教師情報として 64 次元のユーザー Embedding を学習する仕組みを追加しました
  • 学習は Amazon SageMaker 上の Two-Tower Model で行い、推論結果は Solr の Dense Vector フィールドに投入、KNN 検索で「温度感の近い相手」を返せるようにしています
  • 既存のルールベース導線とは置き換えではなく併用で、検索結果の一部を ML 側から差し込む形で運用しています

1. これまでの設計

YYC には、ユーザーのライフサイクルに応じた行動導線がいくつかあります。

  • チャンスタイム(新規登録直後): チュートリアル完了時に時間制限で、一定時間のあいだプロフィール閲覧やメッセージ送信が無料になる施策です。初動のアクションを後押しして、最初の 1 通までの距離を縮める役割を担っています。
  • デイリーミッション(継続期): 一定の条件で抽出された相手に対してあいさつ(初回メッセージ)が無料で送れる仕組みで、日々の行動を後押しして継続利用につなげる役割を担っています。
  • 検索ロジックのルールベースの並び替え :「新規ユーザー優先」「最終ログインが近い順」など、経験則ベースの並び替えです。

いずれも「誰にどの動線を踏ませるか」をルール側で先に決め打つ設計で、登録直後はチャンスタイム、その後はデイリーミッションという役割分担になっていました。

ルールベースの強み

先に書いておくと、ルールベースの設計自体に問題があるわけではありません。数年単位で YYC を支えてきた土台で、以下のような強みがあります。

  • 説明可能性が高い: 「なぜこの動線が出たか」をコードと config で完全に説明できます。運営や CS からの問い合わせにもすぐ答えられます。
  • 即効性がある: 新しい施策を思いついてから数日で本番に出せます。
  • 数値設計が容易: 送信上限や有効時間などのパラメータを A/B で直接調整できます。
  • 障害耐性が高い: 壊れても原因が特定しやすく、切り戻しも簡単です。
  • 運用知見が蓄積しやすい: 長年の施策で「どの数字を動かすと何が起きるか」が体感値として溜まっています。

新規プロダクトや、シグナルが十分に溜まっていないフェーズでは、ルールベースは今でも最短ルートだと思います。

見えてきた課題

一方で、ユーザー層と目的の多様性が広がってくると、次のような課題が見えてきました。

  • ユーザー多様性への追従が遅い: 新しい利用目的や層が出てくるたびに、ルールを増やすか既存ルールを改修するしかありません。
  • 個別最適ができない: 全員一律のルールなので、同じ温度感を持つ人同士をピンポイントで繋ぐことができません。
  • トラブルの事後学習ができない: ブロックや通報があっても、ルール側に自動で反映されるわけではなく、改善のたびに人間が仮説を立てて手を動かす必要があります。
  • 初動と継続で導線が分断する: チャンスタイム(新規)とデイリーミッション(継続)の接続が、ロジックとしても体験としても連続しません。
  • ルールの複雑化: これが一番重くなっていく課題です。

ルールの複雑化についてもう少し書くと、

  • 例外が例外を呼ぶ: 「このユーザー群だけ除外」「この時間帯だけ特別扱い」「このアプリバージョン以降はこう」といった分岐が、一度入ると消せずに積もっていきます。
  • ルール同士の相互作用が読めなくなる: 単体では正しいルールが、他のルールと組み合わさることで予期しない動作をします。
  • 変更のリスクが非対称: 新しいルールの追加は安全に見える一方、既存ルールの削除や改修は副作用が読めず怖くなり、結果として削除されないコードが溜まり続けます。

ルールベースは初速こそ速いのですが、メンテコストは時間とともに増えていきます。ある時点からは、新しい施策を入れるより「既存ルールと衝突しないか確認する」ほうが時間を食うようになります。

ユーザーの目的が多様化するほど、全員一律の導線では受け止めきれない領域が広がってきたので、ここを ML 検索で補完することにしました。


2. 温度感を 1 次元スコアとして定義する

まず取り組んだのは、モデルに学習させる前に「温度感とは何か」を自分たちで定義することです。

ユーザーの「出会いに対する姿勢」は、実は 1 本の軸にかなり綺麗に乗ります。

  じっくり  ←─────────────────────────────→  カジュアル
   0.0                                        1.0
  • 左寄り: じっくり話したい、ゆっくり関係を築きたい層
  • 中央: 一般的な出会い目的の層
  • 右寄り: 気軽につながりたい、カジュアル志向の層

さらに、それぞれのユーザーには「自分のスコア位置」だけでなく「どの範囲の相手なら噛み合うか」という希望レンジもあります。ざっくり書くとこういうイメージです。

   じっくり                                            カジュアル
    0.0                                                   1.0
     ├──────────────────────────────────────────────────────┤

       ◆                                                       ← ユーザー A
       ├───────┤  A の希望レンジ

               ◆                                               ← ユーザー B
          ├───────────────┤  B の希望レンジ

                             ◆                                 ← ユーザー C
                        ├──────────────────────────┤  C の希望レンジ

ポイントは、希望レンジがユーザー (カテゴリ) ごとに形が違うという点です。左寄りの層は同じ左寄りの相手を狭めに希望することが多く、中央・右寄りの層は逆に広めのレンジを持ちやすい、といった非対称な分布になります。実際にはこれをもっと細かい複数カテゴリに分けていますが、構造としては同じです。

この軸を決定論的なロジックとして実装しています。

スコア算出のしくみ

  1. フラグ優先判定: 悪質行為フラグや属性フラグなど、確実にカテゴリが決まる条件を最優先で判定します。
  2. 目的ベーススコア: フラグに該当しないユーザーは、プロフィールの「出会いの目的」から平均スコアを算出します。
  3. 希望レンジ: 自分のスコアに対して「どの範囲の相手なら噛み合うか」を希望の中心値として定義します。

この段階ではまだ機械学習は出てきません。プロフィール・属性・目的コードから、決定論的に scorematch_center の 2 値を出しているだけです。

ただ、この 2 値が後段の学習モデルの教師情報になります。


3. Two-Tower Model で 64 次元に埋め込む

次に、各ユーザーを 64 次元のベクトル (Embedding) に変換する学習パイプラインを、Amazon SageMaker 上に構築しました。採用したのは、推薦系で定番の Two-Tower Model です。

   [User Tower]                [Target Tower]
        │                            │
        ▼                            ▼
   64 次元ベクトル ──コサイン類似度──▶ マッチングスコア
  • User Tower と Target Tower は同じ構造の別重みです
  • 入力は user_id / sex / score / match_center の 4 カラムのみ
  • 出力は 64 次元の密ベクトルです

なぜ学習させるのか

ここで「scorematch_center が決定論的に出るなら、その 2 値でソートすればいいのでは?」と思われるかもしれません。

実は、Embedding の先頭 2 次元はスコアをそのままピン留めしているので、そこだけ見ればロジック単体でも再現できます。ML を使う価値は、むしろ残りの 62 次元のほうにあります。

この 62 次元は実際のユーザー行動 (特にブロック) から学習されるので、

  • 温度感スコアが同じでも、「実際にはブロックされがちな特徴」を持つ人は遠ざけられます
  • プロフィールに明示されていない共通点で近い人同士は、自然と近くなります
  • 過去の行動パターンから、「この相手は希望に合いそうか」という期待度合いを織り込めます

静的な属性ベースのスコアリングに、行動データから学習した「振る舞いの近さ」を重ねる、ということです。先頭 2 次元で説明可能性を担保しつつ、残り 62 次元で汎化を稼ぐ、という役割分担になっています。

損失関数

学習の損失関数は 2 つの項を組み合わせています。

1 つ目は、推薦系で標準的に使われる InfoNCE です。バッチ内のペアを正例・負例としてコントラストさせ、良い組み合わせは近く、悪い組み合わせは遠くなるように学習させます。これに加えて、後述するブロックを明示的な負例として投入しています。

2 つ目は、Embedding の先頭 2 次元を scorematch_center に誘導する MSE 項です。解釈可能なスコアがあるので、それを Embedding の先頭次元にそのまま載せてしまおう、という発想です。これによって、

  • 「なぜ近いのか説明できない」マッチングを返しにくくなります
  • 温度感レンジでのフィルタリングを、ベクトル検索側で低コストに再現できます

ブロックを負例として使う

ユーザー間の明示的なブロックは、マッチングでは強い負例シグナルです。学習パイプラインでは、直近 90 日のブロックを weight = -1.0 の負例インタラクションとして投入しています。

ただし、この負例が担っているのは「ブロック当人同士を離す」ことではなく、「ブロック相手に似た別人を遠ざける」ことです。

ブロック関係にある当人同士は、Solr の検索フィルタ側で確定的に除外しているので、設計としては二層になっています。

担当 効果
Solr の検索フィルタ 確定フィルタ ブロック当人同士は 100% 出ない
負例 -1.0 ML 学習 ブロック相手に似た別人を Embedding 空間で遠ざける

フィルタで当人は既に落ちているので、ML 側の負例は「似た傾向の他人がまた現れる」事故を確率的に減らす役割に専念できます。片方が外れても最低限の安全性は担保されます。


4. 推論結果の配信(Solr KNN)で配信する

学習した Embedding は Solr の Dense Vector フィールドに投入し、KNN 検索で「自分のベクトルに近いユーザーを k 件返す」形で配信しています。

既存の属性フィルタ (性別・年齢・エリア・ブロック除外など) は Graph Pre-Filter として併用できるので、ベクトル検索と条件検索のハイブリッドになります。


5. 何が変わるのか

今回追加した温度感ベクトル KNN 検索と、これまでのルールベース検索との違いは次のとおりです。

観点 これまで (ルールベース) 追加した ML 検索 (温度感ベクトル KNN)
推薦の基準 新規ユーザー優先・最終ログイン順などの並び替え ユーザー同士の温度感の近さ
新規目的への対応 ルール追加や改修 プロフィール更新 → 次回学習で反映
トラブル抑止 人手でルールを足す ブロック負例でモデルが学習
説明可能性 ルールを辿れば説明可 先頭 2 次元にスコアを固定して担保
検索性能 属性 INDEX 頼り Solr KNN で高速ハイブリッド検索

6. 直近のネクスト: 数字を見ながら ML 側に寄せていく

ここまでで「決定論的スコア + Embedding ベースの類似検索」という土台はできましたが、現時点では既存のルールベース導線と ML 検索を併用しながら効果を計測している段階です。マッチング検索全体を ML 側に寄せるかどうかは、数字を見ながら判断していくことになります。

直近のネクストは、

  1. マッチ成立率、メッセージ継続率といったプロダクト側の KPI で ML 検索の効果を計測する
  2. 結果を見ながら、学習データ・特徴量・損失設計を改善していく
  3. 数字が安定して良くなったところで、ML 検索の比率を上げていき、最終的にはマッチング検索をルールベース側から ML 側に寄せていく

という流れを想定しています。ML モデルは学習データや特徴量を入れ替えれば継続的に改善できるので、しばらくはルールベースと併用しながら、数字を見て少しずつ振り方を ML に寄せていく構成で考えています。

その先の改善余地としては、以下のような項目も見えています。

  • シグナル設計の余地: 現状はブロックを負例として使っているのみで、ポジティブな行動 (メッセージ継続、マッチング成立など) をどう反映させるか?
  • コールドスタート: 行動ログがない新規ユーザーに向けてどう最適化するか?
  • 再学習サイクル: 現状は日次バッチですが、更新頻度と計算コストのバランスをどう取るか?

まとめ

今回は、出会いの「温度感」を マッチング検索に乗せるために、Two-Tower Model と Solr KNN でやったことを書きました。

  • 温度感を決定論的なスコアとして定義した上で、それを教師情報として Two-Tower Model を学習させ、Solr KNN で配信する構成にしました
  • Embedding の先頭 2 次元にスコアをピン留めすることで、説明可能性を保ちつつ、残り 62 次元で行動データからの学習を効かせています
  • ブロックは確定フィルタ (Solr) と ML 負例の二層で効かせる設計にしました
  • ML 検索の効果はまだ計測中で、既存のルールベース導線と併用しながらチューニングを続けていく予定です

aws.amazon.com

FlutterKaigi 2024に参加してきました

はじめに

昨年に引き続き、今年もFlutterのカンファレンス、FlutterKaigi2024に参加してきました。

今年は11月21日と22日の2日間にわたり開催され、例年以上に多くのセッションに参加することができました。

会場は有明セントラルタワーホール&カンファレンスで、セッションは2つのホールで行われました。スポンサーブースも2箇所に設置され、会場は非常に賑やかでした。

今年もFlutter製でFlutterkaigiのアプリが作成されており、リポジトリが公開されているのでこちらからも多くの学びが得られそうです。

github.com

どのセッションも非常に学びが多く素晴らしいものでしたが、中でも特に印象深かったセッションについての感想を綴ろうと思います。

印象に残ったセッション

出前館アプリにおけるFlutterアプリ設計とそれを支えるCICD環境の進化

元々React Nativeで作成されていた出前館のクライアントアプリをflutter化するにあたり、どういったフォルダ構成やアセット管理また状態管理をどのように行うかの発表がなされていました。

まずフォルダ構成として

  • Type/Domain
  • Feature
  • Package

この三つが候補としてあげられており、プロジェクトの規模等を考えてFeature型を採用されたようでした。弊社も元々はType/Domain型で運用しておりましたが、プロジェクトの規模が大きくなるにつれファイルを検索しずらい等の問題が発生したためFeature型に移行した経緯があります。

アセット管理にはflutter_genを使っているとのことでした。BuildContextをextensionしてcontext経由でアセットへアクセスしているという工夫もお話しされていました。弊社でもflutter_genを採用しており、flutter_genが自動生成するSvgGenImage, AssetGenImageをextensionしてなるべく不要な引数やデフォルト引数を設定することでアセットへのアクセスを簡易にする工夫をしています。アセット管理に関するベストプラクティスについて、本発表を機会に今後もっと追求していければと思いました。

状態管理にはBLoCを状態を示すStateにはFreezedを採用されているとのことでした。BLoCについては未経験だったためどういった使い方をするのかを知れてよかったです。Freezedは弊社でも採用しており利便性の高さは共感するところでありました。

最後にCI/CD環境についてお話しされていました。当初はBitriseを採用するところから始まったというお話しからはじまり、BtoBアプリのflutter化に追従する形でTeamCityへ移行されていったというお話しをされていました。複数チームが存在する中で、オンプレ環境でバージョンの異なるビルド環境でCI/CDをしていくことで発生する苦労があることを知れました。

発表概要

https://2024.flutterkaigi.jp/session/b57f3efd-b31d-4d57-aaa3-f45f1b17f7272024.flutterkaigi.jp

発表資料

speakerdeck.com


気をつけたい!Desktop対応で陥りやすい罠とその対策

本セッションではDesktop対応をするにあたり事前に知っておかなければ陥ってしまう特有の罠とその対策についてお話しされていました。Desktop対応とありますが、Webも含む広義の意味で使用されています。なのでPadやブラウザ等のマルチデバイスでのレスポンシブ対応における内容も含まれていました。

まず文字入力時の文字数制限がアプリでは問題なかったものがWebだと入力言語を切り替えた際に制限できなくなる現象についてお話しされていました。こちらはTextFieldのmaxLengthEnforcementにenforcedを指定することで解決できるようでした。知らなければ絶対に陥ってしまう罠なので本発表でTipsを知れてよかったです。

次にスクロール可能なWidgetがWeb対応することによってできなくなる現象についてお話しされていました。こちらはマウスによるドラッグを有効にするプロパティを明示的に指定することで解決されたとのことです。アプリ版のみであればタッチによるドラッグしか考えなくてよいですが、デスクトップ等の対応をする場合はマウスによるドラッグも念頭においておく必要があることが知れました。

次に画面レイアウトに関わるレスポンシブ対応についてお話しされていました。 BottomNavigationBarをWebで利用するとタブ間のすき間が大きくなり使いづらくなってしまうため、左側にメニュー一覧を表示することができるNavigationRailを採用されたとのことでした。Material3のWindow size classesを基準にBottomNavigationBarとNavigationRailを切り分けていました。

またNavigationRailの最下部にメニューを表示したいというデザイナーさんからの要望を実現するためにtrailingプロパティを利用して実現しようとしたが、想定していたものと全く違う表示になってしまい、それでも実現するために試行錯誤した結果コードがとても複雑化してしまったようです。 そこでデザイナーさんに相談した上でデザインを調整してもらい解決できたようです。この事例のようにflutter側では解決できないこと・解決しようとするととても複雑になってしまいメンテナンス性が損なわれてしまう場合には相談してデザインを変えることで解決するということは実際の現場では大切なことであると感じました。

最後にWeb特有の課題についてお話しされていました。アプリほどには成熟していないDesktop領域では実装してみると様々な課題にぶちあたることになり、その際にどういった行動をとればいいのかが明確になりました。

  • flutterのissueを検索する
  • Widgetのプロパティを詳しくみてみる
  • 公式のドキュメント、MigrationGuideをちゃんと読む

とても基本的だとされていることですが、ちゃんとできてないことも多いので、本発表を期に改めて意識したいことだと思いました。

発表概要

https://2024.flutterkaigi.jp/session/6cda6895-57f8-47dc-ab29-ebcbd9c7f95a2024.flutterkaigi.jp

発表資料

speakerdeck.com


マッチングアプリ『Omiai 』のFlutterへのリプレイスの挑戦

Omiaiからは長年iOS, Androidそれぞれのプラットフォーム言語で開発されてきたアプリを技術的負債や仕様差異を解消するためにflutter化により解消していくお話しでした。リプレイス中にも既存アプリの機能追加は止めたくないということでAdd-to-Appによるアプローチをとっていたのは、弊社でも同様の経験があり当時のことを思い出させてくれるようでした。

リプレイスするにあたりマンパワーに依存することにより発生するコード規約違反や技術的負債の発生に対する対策として

  • 依存関係を明確にするためマルチパッケージ構成をとる
  • ユニットテストを十分に継続的に書く
  • 実装内容をパターン化し新規参入者に対する学習難易度を下げる

といったことを徹底されているようでした。

依存関係が明確にされていることで、依存に関する誤った実装がそもそもできないというように仕組みで解決されていたので素晴らしいと思いました。また各レイヤーでの責務が明確化されておりユニットテストを書くのも容易になったということも学ぶことができました。

コードへのドキュメーションコメントを書くのも必須にしており、そのチェックにpublic_member_api_docsというlintルールを活用していました。コメントを必須にすることによってソースコードが「実行可能な仕様書」にしているというのが興味深かったです。

またVS Codeのスニペット機能をかなり活用していたのも興味深かったです。 これを機に弊社でもスニペット機能は活用していこうと考えております。

発表概要

https://2024.flutterkaigi.jp/session/369e19d9-1f27-40e6-b294-64c6ca9bfcae2024.flutterkaigi.jp

発表資料

www.slideshare.net


アニメーションを最深まで理解してパフォーマンスを向上させる

弊社のプロダクトでも各所にアニメーションを使っていますが、flutterでのアニメーションは若干特殊なところもあり、正しい実装なのか、他の代替実装やライブラリはあるのか、改善できる要素はあるのかなど、知見を増やすことを目的に本セッションに参加しました。

本セッションでは、flutterにおけるアニメーションの最新動向や入門解説、アニメーションの詳細な説明まで幅広く解説した、本セッションでflutterのアニメーションに関する知識を網羅的に学べる内容でした。

特にパフォーマンスの計測と分析のセクションでは、1000個のロゴを回転させたアプリを例に、パフォーマンス低下の原因とdevtoolsを使った改善方法の解説はとてもわかりやすく学びが多かったセッションでした。

発表概要

https://2024.flutterkaigi.jp/session/4156b633-8043-4d94-8da8-39c367088c592024.flutterkaigi.jp

発表資料

speakerdeck.com


実践的パッケージ戦略

弊社でも、既存プロダクトのマルチパッケージ化を検討しており、ぜひ知見を得たいと思い、本セッションに参加しました。

本セッションではクリーンアーキテクチャを例に、riverpodを使った簡単なコードで説明するセッションでした。

まず3つのレイヤーを使ったシンプルなプロジェクトを例に解説していました。

次に上記の実装を依存性逆転の原則(DIP)に従い、以下のように依存関係を変更した実装の手順を説明をいただきました。

依存関係を逆転すると、当然直接モジュールを参照していたところは壊れるため、

下位になったUseCaseは依存してたモジュールを使用する際に、インターフェースを追加してそれを介して呼ぶように修正します。

上記になったGatewayは、UseCaseのUserRepositoryの具象クラスを実装して、それを返すproviderを更新します。

最後に、AppのProviderScopeのoverridesに更新したproviderを設定して完成です。

しかし、依存してるモジュールの数が増えていくと、都度overridesに追加設定していく必要がでてくる問題が発生します。

そこで、Kyoheiさん自身が作成した override_pod というツールを使うことで、build_runnerで自動生成して、前述の問題を解決できるようです。

今回のようにレイヤーごとにパッケージに分けて、マルチパッケージな構成するにあたり、melosというツールの紹介がありました。melos をつかったマルチパッケージプロジェクトでそのままtestを実行すると、期待した正しいカバレッジが得られないことがあるため、Kyoheiさん自身が作成したlcov_excluderというツールを使って除外設定することで、melosと併せて使うと便利とのことでした。

普段業務で、依存関係をひっくり返すようなことはする機会がなかったですが、実際今回の発表を例にriverpodでの解決方法は非常に興味深く学びの多かったセッションでした。

発表概要

https://2024.flutterkaigi.jp/session/5fa4f6aa-47ef-40d9-b6bb-216493ffc1b12024.flutterkaigi.jp

発表資料

speakerdeck.com


Shorebirdを活用したFlutterアプリの即時アップデート:Code Pushの実践と可能性

Shorebird のCode Pushとは、Flutterをforkしたプロダクトで、Google Play StoreやAppStoreのアプリ審査フェーズをスキップして、Flutter アプリの変更差分をユーザに反映することができる夢のようなツール(サービス)です。

アプリリリースするまでに、必ず審査を通過しなくてはいけませんが、その審査結果はまちまちで、早くても半日〜1日かかってしまって、早くユーザに届けたいプロダクト側はいつももどかしい思いをした経験はあるのではないでしょうか?

その問題をこのShorebirdのCode Pushを使えば解消できる!?というものでした。

ただし、当サービスを利用するにあたって若干制約があるようでした

  • flutter v3.24.0以上の環境が必要(意外と高い)
  • 本家flutterでビルドしたアプリは不可
  • dartコードの差分のみ適用可能(assetの追加削除やネイティブコードの差分が含まれる場合は通常通りの各ストアのリリースフロー)
  • 各ストアのガイドラインには準拠(当然)
  • ユーザ側はアプリを2回起動が必要

料金プランは、従量課金制で5,000パッチ(1端末1パッチ計算)までは無料プランで使用可能なので、まず気軽に使い始めてもよさそうですね!

発表概要

https://2024.flutterkaigi.jp/session/dd12ee26-4d87-4081-a01a-621411231a942024.flutterkaigi.jp


Flutterアプリで可用性を向上させたFeatureFlagの運用戦略とその方法

トランクベース開発を実現するための仕組みであるFeature Flag に関するセッションで、実際にプロダクトで直面した課題と、それを踏まえた実装方法や運用について解説されていました。

弊社でもFeatureFlagを導入しており、発表前からとても注目していたセッションの1つでした。

FeatureFlagは新機能の公開/非公開をリリース後でも簡単に切り替えができる仕組みで、これにより、以下のようなメリットが得られます。

  • 開発中の機能をマージすることができる
  • PRの粒度を小さくできる (コンフリクトを抑える、レビューコストの軽減)
  • リリース後に大きな問題が起きてもすぐにロールバックできる (Firebase RemoteConfigなどとの連携)
  • ABテストの実施が容易になる

発表の中で特に印象的だったのは、UI層でのFeatureFlagの扱い方で、FeatureFlagをラップしたFlagBuilderというWidgetを用いているという点です。

FlagBuilder は、onBuilder と offBuilder という2つのコールバックを持ち、引数に渡した FeatureFlag の値に応じて、それぞれのコールバックが呼ばれる仕組みになっているようです。

if 文や三項演算子を使って条件分岐を行うと、分岐が散らばったり、可読性が低下することがありますが、FlagBuilder 内に分岐を集約することで、この問題を解消できている点が非常に良いと感じました。

また、既存の実装に関しては、コピペで offBuilder に残す運用を採用することで、デグレの発生を抑える取り組みが行われているとのことでした。

ロジック面においても、Feature Flag による制御を基本的に domain レイヤー(UseCase) のみに制限することで、条件分岐が分散しないようになっているようです。

Danger を使ってレビュー時に機械的にFeature Flag の制御を忘れに気づけるようにしている点も非常に参考になりました。

発表概要

https://2024.flutterkaigi.jp/session/454605d6-b1b5-40f4-85c6-1636a6c32b1a2024.flutterkaigi.jp

発表資料

speakerdeck.com


DevTools Extensions で独自の DevTool を開発する

DevTools Extensions を使って独自のデバッグツールを作成する方法について、ライブコーディングを交えてとても詳しく解説されているセッションでした。

(NeoVimを使いこなしていてめちゃくちゃかっこよかったです 笑)

まず、 DevToolsに独自の機能を追加できることを知らなかったので、その点に驚きました。

セッションでは、DevTools Extensions を活用した具体的なツールとして、以下の3つが紹介されていました。

  • GraphQL Cache Inspector
    • GraphQLClientのキャッシュを可視化
    • 正規化されたTree構造で出力
  • Loading State Toggle
    • Loading状態のUIへの切り替え
    • Skeleton などのデザイン確認に使用
  • Dio Logger Toggle
    • Http通信のログ出力を ON/OFF
    • ログの追跡を容易にする

dev toolの作り方は下記の3ステップで比較的簡単に作成できるようです。

  • config.yamlの作成
  • Devtools extensionsのパッケージを作成
  • Flutter webでbuild

GraphQL Cache InspectorはReactのツールを参考に作成したとのことで、

どんなツールを作成するかのアイディアはWeb界隈のツールを参考にするのもオススメとのことでした。

DevTools Extensions を活用することで、アプリ内にデバッグ機能を埋め込むことなく、開発中のデバッグや日々の開発がより快適に行えるのは非常に嬉しいポイントですね!

発表概要

https://2024.flutterkaigi.jp/session/83119f09-e90c-468b-9a5e-69d2df646dfb2024.flutterkaigi.jp

発表資料

speakerdeck.com


Effective Form ~ Flutterによる複雑なフォーム開発の実践 ~

ユーザーからの入力を受け取るためのインターフェースであるフォームの実装方法や注意点について詳しく解説されたセッションでした。

まず、例として挙げられたのはログインフォームです。

このフォームでは、EmailとPasswordを入力値として受け取り、Loginボタンが表示される比較的シンプルな画面が想定されています。

フォームのフローは以下の通りです。

  1. UIの表示
  2. 入力状態の管理
  3. バリデーション
  4. Dirty管理
  5. Submit

特に注目したいのが、4番目のDirty管理です。

EmailのTextFieldにシンプルなバリデーションを適用した場合、初期状態でいきなりInvalid状態になってしまうという問題が紹介されました。

Dirty管理は、こうしたケースでバリデーション結果をユーザーにフィードバックするタイミングを適切に制御するための考え方とのことです。

Dirty管理のポイント

  • 入力フィールドが変更されたタイミングを追跡
  • ユーザーが変更を確定したタイミングで「dirty状態」と見なす
  • エラー表示や変更確認ダイアログの表示タイミングを管理できる

このDirty管理を実現する方法として、formzというパッケージが紹介されました。

formzパッケージには、FormzInputという抽象クラスが用意されており、入力値を「pure」と「dirty」の2つの状態で管理できるようです。

TextFieldのonChangedでpure状態に、onSubmittedでdirty状態に切り替えることで、Dirty管理を実現できます。

また、validatorメソッドをオーバーライドすることで、バリデーションロジックを実装できます。

displayErrorの値を使うことで、dirty状態の場合のみエラーを表示することができ、エラー表示のタイミングを柔軟に制御できる点が非常に便利ですね!

さらに、より複雑なフォームにおける実装課題とその戦略についても解説がありました。

  • 管理すべき状態が多い → Single Source of Truthの担保 (状態の一貫性が担保できる)
  • 状態更新による多彩な動的制御が必要 (状態が決まれば挙動が決まる)
  • 大量の機能と複雑な依存 (状態の管理範囲が最小化される)

弊社のアプリでも複数の場所でフォームを使用しているため、Dirty管理や設計戦略を踏まえて改善に活かしていきたいと思います。

発表概要

https://2024.flutterkaigi.jp/session/c56e7002-02e3-44d5-825c-b64934d122882024.flutterkaigi.jp

発表資料

speakerdeck.com

最後に

冒頭でも触れましたが、今年は2日間にわたる開催で、内容が非常に充実していました。 どのセッションもとても多くの学びがあり、参加者の皆様の熱意にも多くの刺激をいただきました。 登壇者の皆様、そして運営スタッフの皆様、本当にありがとうございました。 来年のFlutterKaigiも楽しみにしています。

YYCのバックエンドをPerl5.40とDebian Bookwormへ更新したプロジェクトを振り返る

こんにちは Diverse developer blogです。今回はプロジェクトの構想から完了まで、1年半ほどかけて行った「PerlとDebianの更新プロジェクト」を振り返ります。

なぜやったのか?

弊社のYYCは20年以上稼働しているサービス(SNS, マッチング, ライブ配信)です。ユーザーの要望に応えるため、機能開発を優先してきたことで開発環境の改善が遅れていました。

特にバックエンドの開発言語(Perl 5.8)と、コンテナOS(CentOS)のアップデートが遅れており、今後の機能開発やセキュリティ対応に課題が生じていました。

そのため、プロジェクトの目標は「開発言語とコンテナOSをLTS(long-term support)バージョンまで更新する」ことにしました。そして、課題を一つ一つ段階的に解決して、ついに目標を達成しました。現在のYYCのバックエンドは、最新のPerl5.40とDebian Bookwormで稼働しています。

どうやったのか?

ここらはプロジェクトの進行を時系列順に紹介します。

1. 段階的リプレイスで失敗した

プロジェクト当初は、当時のYYCとは別にYYC v2を開発して画面や機能ベースで段階的にリプレイスする方式をとりました。しかし、新旧のYYCから共通のDBを安全に操作するのが難しくなり、途中でリプレイスを中止しました。

当時の判断や取り組みは、YAPC::Kyoto 2023で登壇した資料 にまとまっています。この取り組みは失敗になり、段階的なリプレイスに半年ほど費やしました。しかし、サンクコストを気にせずに方針を転換して、今では正しかったと思います。

2. リファクタリングに方針転換した

段階的なリプレイスで開発したコードやリソースは完全に削除して、既存のYYCをリファクタリングする方針に切り替えました。まずは、開発環境を理想の状態に近づけることを目指しました。そして、この作業で出た差分を本番環境へ徐々に適応しました。実施した順に具体的な作業内容を箇条書きで紹介します。

  1. CentOSをDebian Busterへ移行する
    • 開発環境のコンテナOSをまずはDebianベースへ移行した
  2. mod_perl v1をStarmanとPSGI(Perl Web Server Gateway Interface)へ移行する
    • PSGIでも動作するか開発環境で検証した
    • Perlのバージョンを固定している原因のひとつがmod_perl v1だったため、mod_perlは置き換え可能か調査した
  3. Perlは5.16までアップデートする
    • 5.18にアップデートするとコードの修正箇所が多いため
      • 例えば foreach my $hoge qw(a b c) {} が使えない
      • 多数のモジュールがインストールに失敗する
    • 5.16でモジュールの整理整頓と非推奨の警告修正を優先した
  4. ファイルベースで管理していたライブラリをcpanfile経由へ移行する
    • プロジェクトで使用中のライブラリを洗い出し、必要なライブラリのみを移行した
    • すべてのパッケージを最新バージョンにアップグレードした
    • 移行できないライブラリ(社内専用、CPANに存在しない)は、専用ディレクトリを用意してコードをそのままコミットして利用する方式をとった

3. 段階的にLTSバージョンまでアップデートする

PerlもDebianも段階的にアップデートしました。まずは、Debian Busterで本番環境でも正常に稼働する確認がとれてから、Perlのアップデートへ移りました。段階的な手順は以下のとおりです。

  1. Perl5.8 + mod_perl + busterとPerl5.16 + PSGI + Starman + busterのどちらでもバックエンドが起動して単体テストとE2Eテストが通ることを確認した
  2. パフォーマンステスト(taurus)で性能劣化がないことを確認した
  3. Perl5.16 + PSGI + Starman + busterをリリースして正常に稼働した

最後にPerl5.38.2をアップデート対象にしてコードの修正をしつつ、2024年6月30日にBusterはEOLをむかえるため、Bookwormの検証も同時に開始しました。ところが、この作業中にPerl5.40がリリースされました。Perl5.38.2で動作確認がとれていたのでPerl5.40へアップデートしてリリースしました。Perl5.40 + PSGI + Starman + bookwormでリリース後、use v5.40; をすべてのPerlのコードに追記して、プロジェクトは完了しました。

4. アップデート対応で工夫したこと

このプロジェクトと同時並行で、現行環境での機能開発も継続して行っていました。そのため、Perl 5.16 + PSGIへの移行時は、新しい環境の変更をできるだけ現行環境にも取り込んでいく必要がありました。特に以下の2点を重視していました。

  • 新しい環境の変更点を現行環境の開発中でも意識できるようにして移行をスムーズに進めたい
  • コード自体は事前にリリースしておき、環境の切り替えはコンテナイメージの変更のみで行い、切り戻しをしやすい状態にしたい

これらを実現するために以下の工夫をしました。

  • Perl5.8 + mod_perl と、Perl5.16 + PSGI どちらでもプロジェクトのコードが動くように、ベースクラスで環境の違いを吸収した
  • Apache::ConstantsApache::Singletonなど環境に依存するパッケージを直接使っている場合は、プロジェクト内に同様の役割のクラスを作成して事前に移行した
  • パッケージの最新化で Perl5.8 環境から挙動やインタフェースが変わる場合には、一時的にプロジェクト内で該当のパッケージを継承したり MixIn をすることで、Perl 5.16 環境と同じように動くようにした
  • CI では Perl 5.8 + mod_perl と Perl 5.16 + PSGI の2つの環境を用意し、どちらの環境でも動作確認できるようにした

2つの環境をまたがって作業すると、混乱することはありました。しかし、切替はイメージの変更だけで行えたので、不具合の特定やロールバックはしやすい状態で移行できました。正常に移行できた後、新旧の両方で動くように工夫したコードはすべて削除しました。

やってよかったことは?

1. 課題と解決策がわかりやすくなった

アップデートを安全かつ効率的に進めるために不要なコードや機能をたくさん削除しました。考慮するコンテナの数やライブラリの数も減りました。開発言語とOSのアップデートと同時に、利用中のミドルウェアやライブラリも最新化できました。この結果、問題の特定が楽になり、新たな課題の発見とその解決策がわかりやすくなりました。

2. 取れる選択肢が増えた

開発言語もOSもLTSバージョンなので、ライブラリやパッケージが利用できないケースが減りました。以前まではOSが古いため、PerlだけでなくNode.jsなどの他のライブラリのバージョンも更新できない状況でしたが、その心配はなくなりました。

まとめ

プロジェクトを完了して、古く閉ざされた開発環境から解放されました。開発するモチベーションも上がっています。このようなプロジェクトを経験できたのは、多くのユーザーがYYCを長年利用してくれた結果だと思います。今後は、もっと早く計画的に対応できるよう、Diverseでは継続的な改善を怠らない体制を維持していきます。

ブログの内容や弊社に興味のある方は、ぜひ以下の採用ページからカジュアル面談へ!様々な意見をお待ちしています。

diverse-inc.co.jp