Flutter|webview_flutter で JavaScript と通信を行う方法
スマレジの テックファーム(SES 部門) でWebエンジニアとして働いている やまて(@r_yamate) と申します。
実務では、2023 年 3 月末で SES の派遣先で、テーブルオーダーシステムの機能改修業務の設計などを担当していた業務を終えたところです。
4月からの業務では、 Flutter で開発することになったため、一からキャッチアップ中です。
はじめに
この記事では、webview_flutter
パッケージを使用して、Flutter と JavaScript(React アプリ)間で通信を行う方法について書きます。本記事では、サンプルアプリを作成して、Flutter アプリと React アプリがそれぞれどのように通信を行うかをまとめます。
環境
Flutter 側
- Flutter 3.7.9
- Dart 2.19.6
- webview_flutter 4.0.7
- Android Studio Electric Eel | 2022.1.1
- Android Emulator
React 側
- React 18.2.0
- Node.js 19.8.1
- Docker Engine 20.10.22
目次
前回の関連記事
今回の記事は以下の記事の続きとして書いています。
Flutter パッケージ「webview_flutter」
webview_flutter は、Flutter アプリ内で Web コンテンツを表示するためのパッケージです。
このパッケージを利用することで、Flutter アプリ内に Web コンテンツである React アプリを組み込むことが可能になります。
そして、機能の一つとして JavaScriptChannel があります。 JavaScriptChannel は、ネイティブアプリ(Flutterアプリ)と WebView で表示した Web アプリ(Reactアプリ)間でデータをやり取りするための通信機能です。
※ WebView_flutter パッケージを使用する注意点としては、バージョン 3 系から 4 系への移行で大きく変更がある点です。 4 系を導入している場合は、 3 系の書き方で書いていると処理が動きません。
3 系の記事ばかりが検索にかかる中、以下の 4 系に対応している記事に大変助けられました。
1. 実装するサンプルアプリの確認
今回作成するサンプルアプリでは、Flutter で作成した画面に React アプリを組み込み、相互にデータの送受信を行ってみます。
1-1. GitHub リポジトリ
本記事で使用するサンプルアプリのソースコードは、以下の GitHub リポジトリに公開しています。
- Flutter 側
- React 側
1-2. 画面イメージ
Flutter 画面には、WebView を利用して React アプリを表示します。画面にはデータを送信するための「メッセージ送信」ボタンを配置します。
- ボタンを押すと、React(JavaScript)側から Flutter(Dart)側へデータが送信されます。
- Flutter(Dart)側から React(JavaScript)側へデータを送信し返します。
- 画面にメッセージを表示します。
1-3. 処理フローのイメージ
2. React 側の作成
React アプリを作成します。React アプリは、public フォルダ内に配置し、Flutter アプリ内で参照できるようにします。
以下の記事を参考に、必要最低限のものを起動できるように作成しました。
2-1. 実行したコマンド
(Node.js をインストールしてなかったので、)Homebrew でインストールします。
brew install node
Create React App を使って新しい React アプリを作成します。
npx create-react-app jschannel-sample-react-app
作成された 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 への送信に関わる部分
onSendMessageButtonClick
関数は、メッセージ送信ボタンがクリックされたときに呼び出されます。window.sendMessage.postMessage
を使用して、 JSON 形式のデータ(メッセージタイプとメッセージ本文)を Flutter アプリに送信します。
Flutter からの受信に関わる部分
window.flutterMessage
関数は、 Flutter アプリからメッセージを受信するために使用します。(ここではflutterMessage
という関数名にしていますが、自由に設定できます)- 受信したメッセージは、
message
引数として渡されます。 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 からの受信に関わる部分
_configureWebView
関数内で、addJavaScriptChannel
を使って JavaScriptChannel を追加します。チャンネル名は 'sendMessage' で、メッセージ受信時のコールバックとして_onJavaScriptMessageReceived
関数を設定しています。_onJavaScriptMessageReceived
関数が、Reactアプリからメッセージを受信した際に呼び出されます。受信したメッセージは、result.message
で取得できます。- 受信したメッセージをJSON形式からMapにデコードします。デコード後、送信されたメッセージタイプとメッセージ本文が取得できます。
React への送信に関わる部分
_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 を使用します。
3-2. HTTP(クリアテキストトラフィック)の許可
HTTP(暗号化されていない接続)を使ってネットワークリソースにアクセスするため、 android:usesCleartextTraffic
を true
に設定します。
<manifest ...> <application ... android:usesCleartextTraffic="true"> ... </application> </manifest>
確認. アプリの実行テスト
設定が完了したので、アプリを実行します。flutter run
を実行して、アプリが正常に動作することを確認します。
入力欄に、 Hello, world!
と入力して、
メッセージ送信ボタンを押すと、
Message from Flutter: Hello, world!
と入力した文字列が表示されるようになっています。
これで、 Flutter と React アプリ間で通信が行えるサンプルアプリが完成しました。
おわりに
本記事では、webview_flutter
を使った、Flutter と React アプリ間での通信方法について書きました。
ありがとうございました。
参考
今日は有給とって、息子の小学校入学式に出席。育ったなー。息子の育児 小学生編も楽しみ。 pic.twitter.com/nmQR1iQDhx
— やまて|ソフトウェアエンジニア2年目 (@r_yamate) 2023年4月10日
息子と自分の成長を楽しみに生きています。