.NET MAUI HybridWebViewで作る!リアルタイム商品ピックアップアプリ開発記

こんにちは、サポーターズのちゅうこ(yamachu)です。 つい最近までコミックマーケット106(C106)に向けて、記事を書いたりアプリケーション開発をしていました。

私の所属するサークルでは、同人誌即売会のようなイベント会場で使うことを想定したレジシステムの開発を行っています。 作成しているレジシステムは大まかに以下の機能に大別されます。

  • レジとしての入力インタフェース
  • 在庫管理
  • レシート発行
  • 商品ピックアップ支援

C106では商品ピックアップの改善に注力し、そのためのモバイルアプリケーションを作成していました。

この記事では、そのために作成したモバイルアプリケーションを支える技術の一部を紹介しようと思います。

モバイルアプリケーションの技術要素

商品のピックアップに使用したモバイルアプリケーションの技術についてご紹介します。 今回使用した技術は以下の通りです。

  • .NET MAUI HybridWebView(C#)
  • React v19(TypeScript)

こう見ると、UIはReactで作ったんだなーは多くの人は察しが付くとは思います。 この中でも、.NET MAUI HybridWebViewなんてものは多分見たことのある人はほとんどいないでしょう…!

ざっくり説明すると、昔のXamarin.FormsのCustomRendererの例で挙げられたHybridWebViewみたいなものです(伝わる人0人説)。 もうちょいちゃんと言うと、C# コードからWebView上で動作するアプリケーションと、Window.postMessageを通して相互通信可能なControlを指します。 つまり、C# とWebView上で動作するReactアプリケーションが、相互にメッセージングを通して双方向のメソッドを呼び出せる機構を提供する仕組みみたいなものです。

なぜ .NET MAUI HybridWebView なのか

多くの人はこう考えるでしょう、『Reactを使ってるのであれば、React Native使えばよくね?』、『モバイルアプリケーション作りたいのであれば、KotlinやSwiftで書けばいいじゃん』と。 はい、私もそう思います。

しかしながら、今回作成したモバイルアプリケーションは以下のような要件があったため、.NET MAUI HybridWebViewが最適とは言わずとも、選択肢の一つとして十分に検討に値するものでした。

  • Raspberry Piなどのマイコンボードとの通信
  • ピックアップ対象の商品の配置の更新容易性

モバイルアプリケーション本体の話ではありませんが、今回のレジシステムではRaspberry Piを利用していました。 Raspberry Piに接続された外部モジュール(例えば商品棚を光らせるなど)を制御するために、Raspberry Piと何らかの方法で通信する必要がありました。 同人誌即売会のような環境において、無線を利用するのは非常にリスキーな選択ではありました(実際にC103、C104で無線を採用し痛い目を見ました)。 そのため有線でRaspberry Piと通信するために今回はiOSでUSB接続でMIDIを喋るようにしました。 MIDIなどを喋るとなると、ネイティブコードや、それに近いものを書く必要があるというわけです。 また、商品の配置はイベントごとに変わることが想定されていため、HTML/CSSやReactなどのWeb技術で柔軟に対応出来る方がベターではないかと考えました。

そうなると、その両方の良さを活かせるのが.NET MAUI HybridWebViewだったわけです。

.NET MAUI HybridWebView でのフロントエンドとの通信

実際に.NET MAUI HybridWebView と React アプリケーションがどう動いているのか図を軽く見てみましょう。 以下の図が全てではありませんが、主には postMessage を使用したり、HybridWebView側のパラメータの受け口に対してGETリクエストで叩くという感じです。

HybridWebView概要

そのためJavaScript側の実装は提供されているため、そのJavaScriptを自分のWebアプリケーション上で実行するとすぐにHybridWebView上で動作するアプリケーションに進化させることが出来ます。

React -> C#

C# 側で公開しているメソッドを呼び出す方法を見てみましょう。 React(JavaScript)側からC#で提供しているInitializeを叩きたいな〜というケースを見てみます。

HybridWebViewを提供している.NET MAUIのContentPageで以下のような実装をしてみます。

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        // JavaScript側からC# のMethodを呼び出す時に、どの対象のメソッドを叩くか登録
        myHybridWebView.SetInvokeJavaScriptTarget<MainPage>(this);
    }

    public async Task Initialize()
    {
        // Called from JavaScript
    }
}

これに対して、React側からは以下の呼び出しでInitializeを呼び出すことが出来ます。

const invokeDotNetAsync = useCallback(
  (methodName: string, paramValues?: [unknown, ...unknown[]]) => {
    return window.HybridWebView.InvokeDotNet(methodName, paramValues);
  },
  []
);

//

invokeDotNetAsync('Initialize');

今回のケースでは、Initializeは引数も戻り値もないですが、もちろん引数や戻り値もサポートされています。

C# -> React

では反対に、C# 側からReact側にメッセージングする仕組みを見てみましょう。 前述したように、window.postMessageを使用したメッセージングであるため、以下のようなJavaScript側の受け口を作っておきます。

const Event = "HybridWebViewMessageReceived" as const;

export function useHybridWebViewEvent(
  handler: (ev: WindowEventMap[typeof Event]) => void
) {
  const latestRef = useRef<typeof handler>(handler);

  useLayoutEffect(() => {
    latestRef.current = handler;
  }, [handler]);

  const onEvent = useCallback((ev: WindowEventMap[typeof Event]) => {
    latestRef.current(ev);
  }, []);

  useEffect(() => {
    window.addEventListener(Event, onEvent);
    return () => window.removeEventListener(Event, onEvent);
  }, [onEvent]);
}

//

const handleNewPickRequest = useCallback(
  (ev: WindowEventMap["HybridWebViewMessageReceived"]) => {
    const message = JSON.parse(ev.detail.message);
    const { methodName } = message;
    if (methodName !== "OnNextPickRequestReceived") {
      return;
    }
    const { value }: { value: PickRequest } = message;
    setCurrentPicking(value);
  },
  []
);
useHybridWebViewEvent(handleNewPickRequest);

HybridWebViewでは、HybridWebViewMessageReceived というイベント名でメッセージを受け取ることが出来ます。 そのため上記のHooksでは、そのイベントを受け取って、引数で渡されたハンドラに処理を委譲する仕組みを提供しています。

それをC# 側から以下のように呼び出します。

myHybridWebView.SendRawMessage(JsonSerializer.Serialize(new { methodName, value }));

これだけで、C# 側からReact側にメッセージを送ることが出来ます。

開発体験について

.NETでの標準的な(?)デバッグツールを使用して、アプリケーションをデバッグすることが出来ます。 VS CodeなどからAttachすることで、容易にデバッグ可能で、そこまで開発難易度は上がらずにモバイルアプリケーションを開発することが出来ました。 しかし、アプリ上のWebView内で動作するReactアプリケーションのデバッグは、そのままデバッグすることは出来ません。 例えばSafariやChromeのDevToolsを使用する必要があるため、見るものが増えてしまうというのはあります。

デバッガを利用してReactアプリケーションをデバッグするのは若干手間にはなりますが、Reactアプリケーション自体は通常のWebアプリケーションであるため、例えばHybridWebView依存の箇所をモック化することで、通常のWebアプリケーションとしてVS Codeなどでデバッグすることも可能です。 若干の慣れは必要になりますが、そこまで大きな障壁にはならないかと思います。

採用してみてどうだったか

.NET MAUI HybridWebViewを採用してみて、個人的にはそこまで大きな問題もなく、スムーズにアプリケーションを実装することが出来ました。 例えば要件にあったように、直前にUIを変更したい場合でも、Reactアプリケーションを修正してビルドし直すだけで済むため、非常に柔軟に対応することが出来ました。 またNative側の実装もC#で完結したため、MIDI上で実装したメッセージングのコードもC#で完結し、Raspberry Piとの間でもC#で完結したため、非常にスムーズに実装することが出来ました。

ほぼほぼ情報が出ていない技術であるためハマったときはつらそう…とは思いつつも、今回のケースにおいては十分な選択肢の一つであったと感じています。

まとめ

.NET MAUI HybridWebViewを使用して、ReactアプリケーションとC#コードが相互に通信するモバイルアプリケーションを作成しました。 .NET MAUI HybridWebViewはまだまだ情報が少ない技術ではありますが、要件によっては十分に選択肢の一つとして検討に値する技術であると感じました。

完全に趣味開発であるので、好きな技術を好きに使うのがやっぱり楽しいですね! また面白そうな技術があったら試して遊んでみようと思います。

参考資料

公式資料

今回作成したアプリじゃないけど、.NET MAUI HybridWebViewを使ってるアプリサンプル