Flutter|webview_flutter で JavaScript と通信を行う方法 - 転職したらスマレジだった件

転職したらスマレジだった件

スマレジのエンジニアやまてのテックブログです。マジレス大歓迎です。

Flutter|webview_flutter で JavaScript と通信を行う方法

スマレジの テックファーム(SES 部門) でWebエンジニアとして働いている やまて(@r_yamate) と申します。

実務では、2023 年 3 月末で SES の派遣先で、テーブルオーダーシステムの機能改修業務の設計などを担当していた業務を終えたところです。

4月からの業務では、 Flutter で開発することになったため、一からキャッチアップ中です。

はじめに

この記事では、webview_flutter パッケージを使用して、Flutter と JavaScript(React アプリ)間で通信を行う方法について書きます。本記事では、サンプルアプリを作成して、Flutter アプリと React アプリがそれぞれどのように通信を行うかをまとめます。

環境

Flutter 側

React 側

  • React 18.2.0
  • Node.js 19.8.1
  • Docker Engine 20.10.22

目次

前回の関連記事

今回の記事は以下の記事の続きとして書いています。

ryamate.hatenablog.com

Flutter パッケージ「webview_flutter」

webview_flutter は、Flutter アプリ内で Web コンテンツを表示するためのパッケージです。

pub.dev

github.com

このパッケージを利用することで、Flutter アプリ内に Web コンテンツである React アプリを組み込むことが可能になります。

そして、機能の一つとして JavaScriptChannel があります。 JavaScriptChannel は、ネイティブアプリ(Flutterアプリ)と WebView で表示した Web アプリ(Reactアプリ)間でデータをやり取りするための通信機能です。

※ WebView_flutter パッケージを使用する注意点としては、バージョン 3 系から 4 系への移行で大きく変更がある点です。 4 系を導入している場合は、 3 系の書き方で書いていると処理が動きません。

3 系の記事ばかりが検索にかかる中、以下の 4 系に対応している記事に大変助けられました。

www.autumn-color.com

github.com

1. 実装するサンプルアプリの確認

今回作成するサンプルアプリでは、Flutter で作成した画面に React アプリを組み込み、相互にデータの送受信を行ってみます。

1-1. GitHub リポジトリ

本記事で使用するサンプルアプリのソースコードは、以下の GitHub リポジトリに公開しています。

  • Flutter 側

github.com

  • React 側

github.com

1-2. 画面イメージ

Flutter 画面には、WebView を利用して React アプリを表示します。画面にはデータを送信するための「メッセージ送信」ボタンを配置します。

  • ボタンを押すと、React(JavaScript)側から Flutter(Dart)側へデータが送信されます。
  • Flutter(Dart)側から React(JavaScript)側へデータを送信し返します。
  • 画面にメッセージを表示します。

1-3. 処理フローのイメージ

2. React 側の作成

React アプリを作成します。React アプリは、public フォルダ内に配置し、Flutter アプリ内で参照できるようにします。

以下の記事を参考に、必要最低限のものを起動できるように作成しました。

www.s-toki.net

2-1. 実行したコマンド

(Node.js をインストールしてなかったので、)Homebrew でインストールします。

brew install node

Create React App を使って新しい React アプリを作成します。

npx create-react-app jschannel-sample-react-app

ja.reactjs.org

作成された React アプリのディレクトリに移動します。

cd jschannel-sample-react-app

React アプリをビルドします。

npm run build

2-2. Docker 関連ファイルの作成

  • 作成・編集:Dockerfile
FROM 'nginx'
RUN service nginx start
  • 作成・編集:docker-compose.yml
version: '3.9'

services:
  nginx:
    build: ./
    image: dockerdemo
    ports:
      - 80:80
    volumes:
      - ./build:/usr/share/nginx/html

Create React App で作成されたファイルと合わせるとこんな感じです。

2-3. Docker コンテナをビルド

Docker Compose を使用して、コンテナをビルドします。

docker compose up -d --build

2-4. ブラウザでの確認

ブラウザで http://localhost/ にアクセスしてみると、以下の画面が表示できました。

2-5. React 側の JavaScript のコード

  • 編集:src/App.js
import logo from './logo.svg';
import './App.css';
import React, { useState } from 'react';

function App() {
  const [messageFromFlutter, setMessageFromFlutter] = useState('');
  const [inputMessage, setInputMessage] = useState('');

  function onSendMessageButtonClick() {
    if (!window.sendMessage) {
      console.error('native error');
      return;
    }
    window.sendMessage.postMessage(JSON.stringify({ type: 'sendMessage', message: inputMessage }));
  }

  window.flutterMessage = (message) => {
    console.log('recv message: ${message}');
    setMessageFromFlutter(message.toString());
  };

  const handleInputChange = (event) => {
    setInputMessage(event.target.value);
  };

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
        <input
          type="text"
          placeholder="メッセージ入力"
          value={inputMessage}
          onChange={handleInputChange}
        />
        <button onClick={onSendMessageButtonClick}>メッセージ送信</button>
        <p>Message from Flutter: {messageFromFlutter}</p>
      </header>
    </div>
  );
}

export default App;

送受信に関わる部分は、 JavaScriptChannel を介して通信が行われます。

Flutter への送信に関わる部分

  1. onSendMessageButtonClick 関数は、メッセージ送信ボタンがクリックされたときに呼び出されます。
  2. window.sendMessage.postMessage を使用して、 JSON 形式のデータ(メッセージタイプとメッセージ本文)を Flutter アプリに送信します。

Flutter からの受信に関わる部分

  1. window.flutterMessage 関数は、 Flutter アプリからメッセージを受信するために使用します。(ここでは flutterMessage という関数名にしていますが、自由に設定できます)
  2. 受信したメッセージは、message 引数として渡されます。
  3. setMessageFromFlutter を呼び出して、受信したメッセージを messageFromFlutter state にセットします。これにより、画面上にメッセージが表示されます。

3. Flutter 側の作成

3-1. Flutter のコード

  • 編集:lib/main.dart
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _initializeWebView();
  }

  Future<void> _initializeWebView() async {
    _controller = WebViewController();
    await _configureWebView(_controller);
  }

  Future<void> _configureWebView(WebViewController webViewController) async {
    await webViewController.setJavaScriptMode(JavaScriptMode.unrestricted);
    await webViewController.loadRequest(Uri.parse('http://10.0.2.2'));

    await webViewController.addJavaScriptChannel(
      'sendMessage',
      onMessageReceived: _onJavaScriptMessageReceived,
    );
  }

  Future<void> _onJavaScriptMessageReceived(JavaScriptMessage result) async {
    if (kDebugMode) {
      print('js message: ${result.message}');
    }

    final jsonData = jsonDecode(result.message) as Map<String, dynamic>;

    if (kDebugMode) {
      print('requested: ${jsonData['type']}');
    }

    await _controller
        .runJavaScriptReturningResult("flutterMessage('${jsonData['message']}')");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: WebViewWidget(
        controller: _controller,
      ),
    );
  }
}

React からの受信に関わる部分

  1. _configureWebView 関数内で、addJavaScriptChannel を使って JavaScriptChannel を追加します。チャンネル名は 'sendMessage' で、メッセージ受信時のコールバックとして _onJavaScriptMessageReceived 関数を設定しています。
  2. _onJavaScriptMessageReceived 関数が、Reactアプリからメッセージを受信した際に呼び出されます。受信したメッセージは、result.message で取得できます。
  3. 受信したメッセージをJSON形式からMapにデコードします。デコード後、送信されたメッセージタイプとメッセージ本文が取得できます。

React への送信に関わる部分

  1. _onJavaScriptMessageReceived 関数の最後で、Reactアプリにメッセージを送信します。_controller.runJavaScriptReturningResult を使って、Reactアプリ内で定義した flutterMessage 関数を呼び出し、メッセージ本文を引数として渡します。

runJavaScriptReturningResult に渡す引数は文字列で 、今回の例ではflutterMessage() の括弧内に、JSON形式の文字列を入れています。

補足: Uri.parse('http://10.0.2.2'))

localhost にすると、接続が拒否されるため、10.0.2.2 というIPアドレスを設定しています。

開発マシンのループバック インターフェースで実行されているサービスにアクセスするには、代わりに特殊アドレス 10.0.2.2 を使用します。

developer.android.com

3-2. HTTP(クリアテキストトラフィック)の許可

HTTP(暗号化されていない接続)を使ってネットワークリソースにアクセスするため、 android:usesCleartextTraffictrue に設定します。

developer.android.com

  • 編集:android/app/src/main/AndroidManifest.xml
<manifest ...>
  <application
      ...
      android:usesCleartextTraffic="true">
    ...
  </application>
</manifest>

確認. アプリの実行テスト

設定が完了したので、アプリを実行します。flutter run を実行して、アプリが正常に動作することを確認します。

入力欄に、 Hello, world! と入力して、

メッセージ送信ボタンを押すと、

Message from Flutter: Hello, world! と入力した文字列が表示されるようになっています。

これで、 Flutter と React アプリ間で通信が行えるサンプルアプリが完成しました。

おわりに

本記事では、webview_flutter を使った、Flutter と React アプリ間での通信方法について書きました。

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

参考

pub.dev

github.com

www.autumn-color.com

github.com



息子と自分の成長を楽しみに生きています。