Flutter InAppWebviewを使ったWeb表示

プログラミング

概要

FlutterでInAppWebviewを利用したアプリのサンプルソースコードを紹介します。InAppWebviewは以下のpub.devで公開されているライブラリでヘッドレスWebviewなども提供してくれています。

flutter_inappwebview | Flutter package
A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser win...

インストール

pubspec.yamlのdependencies内に以下を追加しましょう。(最新バージョンは適宜pub.devを参照ください)

flutter_inappwebview: ^5.4.3+7

Webview上のカメラ機能などを利用する方は、以下も追加しておきましょう。

permission_handler: ^10.0.0

実装

今回はStatelessWidgetで実装していきたいと思います。(筆者がStatefulはあまり好んでいないため、、、)

まずmain内に以下を記述します。

WidgetsFlutterBinding.ensureInitialized();
  if (Platform.isAndroid) {
    await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
    await Permission.camera.request();
    await Permission.microphone.request();

    var swAvailable =
        await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.SERVICE_WORKER_BASIC_USAGE);
    var swInterceptAvailable = await AndroidWebViewFeature.isFeatureSupported(
        AndroidWebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST);

    if (swAvailable && swInterceptAvailable) {
      AndroidServiceWorkerController serviceWorkerController = AndroidServiceWorkerController.instance();

      await serviceWorkerController.setServiceWorkerClient(AndroidServiceWorkerClient(
        shouldInterceptRequest: (request) async {
          print(request);
          return null;
        },
      ));
    }
  }

続いて、WebviewSampleというクラスを作成し、StatelessWidgetを継承させ以下を記述します。
大まかな概要としては画面上部に検索用のTextFiledを用意し、下にWebviewを表示させるようにしています。また、フッター部分にはBackボタンやReloadボタンなどを用意することでWebviewの画面遷移をできるようにしています。
ちなみに大部分は公式ページから取ってきているので、細かい説明は公式から読むと良いと思います。

class WebviewSample extends StatelessWidget {
  WebviewSample({Key? key}) : super(key: key);

  final GlobalKey webViewKey = GlobalKey();
  InAppWebViewController? webViewController;
  InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
    crossPlatform: InAppWebViewOptions(
      useShouldOverrideUrlLoading: true,
      mediaPlaybackRequiresUserGesture: false,
    ),
    android: AndroidInAppWebViewOptions(
      useHybridComposition: true,
    ),
    ios: IOSInAppWebViewOptions(
      allowsInlineMediaPlayback: true,
    ),
  );

  late PullToRefreshController pullToRefreshController;
  String url = "";
  double progress = 0;
  final urlController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    pullToRefreshController = PullToRefreshController(
      options: PullToRefreshOptions(
        color: Colors.blue,
      ),
      onRefresh: () async {
        if (Platform.isAndroid) {
          webViewController?.reload();
        } else if (Platform.isIOS) {
          webViewController?.loadUrl(urlRequest: URLRequest(url: await webViewController?.getUrl()));
        }
      },
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text("WebViewSample"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            TextField(
              decoration: const InputDecoration(
                prefixIcon: Icon(Icons.search),
              ),
              controller: urlController,
              keyboardType: TextInputType.url,
              onSubmitted: (value) {
                var url = Uri.parse(value);
                if (url.scheme.isEmpty) {
                  url = Uri.parse("https://www.google.com/search?q=$value");
                }
                webViewController?.loadUrl(urlRequest: URLRequest(url: url));
              },
            ),
            Expanded(
              child: Stack(
                children: [
                  InAppWebView(
                    key: webViewKey,
                    initialUrlRequest: URLRequest(
                      url: Uri.parse("https://inappwebview.dev/"),
                    ),
                    initialOptions: options,
                    pullToRefreshController: pullToRefreshController,
                    onWebViewCreated: (controller) {
                      webViewController = controller;
                    },
                    onLoadStart: (controller, url) {
                      this.url = url.toString();
                      urlController.text = this.url;
                    },
                    androidOnPermissionRequest: (controller, origin, resources) async {
                      return PermissionRequestResponse(
                          resources: resources, action: PermissionRequestResponseAction.GRANT);
                    },
                    shouldOverrideUrlLoading: (controller, navigationAction) async {
                      var uri = navigationAction.request.url!;

                      if (!["http", "https", "file", "chrome", "data", "javascript", "about"]
                          .contains(uri.scheme)) {
                        return NavigationActionPolicy.CANCEL;
                      }

                      return NavigationActionPolicy.ALLOW;
                    },
                    onLoadStop: (controller, url) async {
                      pullToRefreshController.endRefreshing();
                      this.url = url.toString();
                      urlController.text = this.url;
                    },
                    onLoadError: (controller, url, code, message) {
                      pullToRefreshController.endRefreshing();
                    },
                    onProgressChanged: (controller, progress) {
                      if (progress == 100) {
                        pullToRefreshController.endRefreshing();
                      }
                      this.progress = progress / 100;
                      urlController.text = url;
                    },
                    onUpdateVisitedHistory: (controller, url, androidIsReload) {
                      this.url = url.toString();
                      urlController.text = this.url;
                    },
                    onConsoleMessage: (controller, consoleMessage) {
                      print(consoleMessage);
                    },
                  ),
                  progress < 1.0 ? LinearProgressIndicator(value: progress) : Container(),
                ],
              ),
            ),
            ButtonBar(
              alignment: MainAxisAlignment.center,
              children: <Widget>[
                ElevatedButton(
                  child: const Icon(Icons.arrow_back),
                  onPressed: () {
                    webViewController?.goBack();
                  },
                ),
                ElevatedButton(
                  child: const Icon(Icons.arrow_forward),
                  onPressed: () {
                    webViewController?.goForward();
                  },
                ),
                ElevatedButton(
                  child: const Icon(Icons.refresh),
                  onPressed: () {
                    webViewController?.reload();
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

カメラなどを使う場合

Webアプリ側でカメラ機能などを利用する場合は、上記に加えて各プラットフォーム上での権限設定などが必要になります。まずmainの方に以下を追加します。

  await Permission.camera.request();
  await Permission.microphone.request(); // マイクを使う場合のみ

続けてAndroidの権限を設定するために、AndroidManifestに以下の権限を追加します。<provider>タグの方は<applicatoin>タグ内に記述してください。

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.VIDEO_CAPTURE" />
<uses-permission android:name="android.permission.AUDIO_CAPTURE" />
<provider
    android:name="com.pichillilorenzo.flutter_inappwebview.InAppWebViewFileProvider"
    android:authorities="${applicationId}.flutter_inappwebview.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>

続けてiOSの方はInfo.plistファイルに以下を記述します。

<key>NSMicrophoneUsageDescription</key>
<string>Flutter requires acess to microphone.</string>

<key>NSCameraUsageDescription</key>
<string>Flutter requires acess to camera.</string>

string内は説明文なので適宜変えても問題ありません。

筆者は動作確認に以下のReactのDemoカメラページを利用させて頂きました。

React App

最後に

今回はFlutterでWebviewを利用してみました。非常に使いやすいライブラリで、ますますFlutterのエコシステムが便利なことを痛感させられます。もう純粋なAndroid/Kotlinには戻れなそう。。。

以上になります。

最後まで読んで頂きありがとうございます!

面白かった、参考になった、と少しでも感じて頂けましたら
ブログランキング上位になるための応援をして頂けないでしょうか!
今後も面白い記事を更新していきますので、ぜひ宜しくおねがいします!
プログラミング

コメント