📱

WebViewとアプリ内/外ブラウザ起動

2022/04/24に公開約7,400字

本記事では、以下gifの挙動の違いを解説しています。

iOS Android

はじめに

Flutterアプリ(ネイテイブアプリ)でWebサイトを表示したい場合に、ご存知の通りWebViewやブラウザ起動がありますが、各OSでのそれぞれの挙動の違いや特性について改めて整理しました。Flutter公式プラグインである以下2つを本記事では取り扱っています。

https://pub.dev/packages/webview_flutter
https://pub.dev/packages/url_launcher

※WebView表示のプラグインとして flutter_inappwebview | Flutter Package も有名で私も使っていた時期がありますが、HTTPS通信時のホスト名検証がない点や, 結構な数のイシューが放置されたままになっているなどの理由から、現在は webview_flutter を利用しています。

Webサイトを表示する3パターンの方法

ネイテイブアプリでWebサイトを表示する方法として主に下記の3つが上げられます(呼び名はイメージしやすいように便宜上定めています)。

  1. WebView埋め込み型
    • FlutterのPlatformViewを使ってネイテイブのWebViewクラスを埋め込みで表示
    • 必ずしもフルスクリーンである必要はなく自由な幅・高さで表示ができる
    • UserAgentの指定やURLのホワイトリスと対応などカスタマイズ性が高い
  2. アプリ内ブラウザ
    • Safariアプリをアプリから離脱せずに表示し、Safariアプリと同等のブラウジング操作ができる
    • FlutterのViewに対してモーダル遷移で表示される(見た目的には画面遷移のような形)
    • 基本的には表示のみでWebView埋め込み型ほどカスタマイズ性能は無い
    • Androidの場合はWebView埋込み型がフルスクリーン形式で表示されている
  3. 外部アプリケーション起動
    • 端末のデフォルトブラウザで起動する(アプリから一旦離脱する)
    • Universal Links / App Linksに対応している場合はアプリケーションが起動する

ネイテイブレベルで分解すると以下の表に分類できます。

画面 WebView埋め込み型 アプリ内ブラウザ 外部アプリケーション起動
iOS WKWebView SFSafariViewController 端末のデフォルトブラウザ(大半はSaferiアプリ)もしくはアプリ
Android WebView 同左 端末のデフォルトブラウザもしくはアプリ

以降、実際にFlutterプラグインで実装する様子をサラッと見ていきます。

WebView埋め込み型

webview_flutter を使うことで「WebView埋め込み型」が実現できます。実装自体は非常に簡単です。

final controller = Completer<WebViewController>();
WebView(
    initialUrl: url,
    onWebViewCreated: controller.complete,
    navigationDelegate: (request) {
      // requestに応じてアクセスをブロックできるなどカスタマイズ性が高い
    },
),

On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.

内部的には、iOSではWKWebView, AndroidではWebViewを利用して、FlutterのPlatformView(AndroidだとAndroidView、iOSだとUiKitView)を使ってネイテイブビューを描画しています。

また、特にAndroidでのPlatformViewでは、Hybrid compositionorVirtual displayが利用され、これらは毎フレームごとにメモリコピーが行われることでFlutterのUI全体のパフォーマンスに影響を与える可能性があるとドキュメントに記載されておりましたが、冒頭でお見せしたgifで分かるようにWebViewで表示する程度ではその違和感は感じませんでした。

アプリ内ブラウザ、外部アプリケーション起動

url_launcher を使うことで「アプリ内ブラウザ」「外部アプリケーション起動」が実現できます。こちらも実装は非常に簡単です。

launchUrl(Uri.https('flutter.dev', ''));
// ref. https://github.com/flutter/plugins/blob/ca63d964d9fdfff9be36533ea4248d0d0ca28fd0/packages/url_launcher/url_launcher/lib/src/types.dart#L12
enum LaunchMode {
  /// Leaves the decision of how to launch the URL to the platform
  /// implementation.
  platformDefault,

  /// Loads the URL in an in-app web view (e.g., Safari View Controller).
  inAppWebView,

  /// Passes the URL to the OS to be handled by another application.
  externalApplication,

  /// Passes the URL to the OS to be handled by another non-browser application.
  externalNonBrowserApplication,
}

バージョン6.1.0以前はforceSafariVCforceWebViewでプラットフォームごとに個別でプロパティを設定する必要がありましたが、これらがLaunchModeという形で統一されました。LaunchMode.platformDefaultはプラットフォームごとに下記の挙動となっています。

  • [LaunchMode.platformDefault] is supported on all platforms:
    /// - On iOS and Android, this treats web URLs as
    /// [LaunchMode.inAppWebView], and all other URLs as
    /// [LaunchMode.externalApplication].
    /// - On Windows, macOS, and Linux this behaves like
    /// [LaunchMode.externalApplication].
    /// - On web, this uses webOnlyWindowName for web URLs, and behaves like
    /// [LaunchMode.externalApplication] for any other content.

ひとまずiOSとAndroidに注目すると、

  • Web URLの場合: アプリ内ブラウザ(inAppWebView
  • それ以外の場合(ディープリンク等): 外部ブラウザ or アプリ(externalApplication

といった挙動によしなに切り替えてくれるので特別要件がなければplatformDefault(未指定)で良さそうですね。
アプリ内ブラウザではなく問答無用で外部ブラウザ起動させたい場合は、externalApplicationを明示すると期待通りの動きになります。
(ちなみにexternalNonBrowserApplicationをした状態で通常のWeb URLを指定すると、iOSでは何も起きず、Androidでは外部ブラウザが起動する挙動を確認しましたが、普通にexternalApplication指定で良い気がします)。

launchUrl(
    uri,
    mode: LaunchMode.externalApplication,

    // iOS: Universal Links以外のURLを指定すると何も起こらない
    // Android: App Links以外のURLを指定しても外部ブラウザ起動し表示される
    // mode: LaunchMode.externalNonBrowserApplication,
);
6.1.0以前の場合

※執筆途中でタイムリーに更新が入ったために、途中まで記載していた内容です。


iOSではforceSafariVC、AndroidではforceWebViewとそれぞれ異なるプロパティを指定することで、アプリ内で表示するか外部ブラウザ起動にするか選択できます。デフォルト値が逆転している点が注意です。

画面 プロパティ デフォルト値
iOS forceSafariVC true(アプリ内ブラウザ起動)
Android forceWebView false(外部ブラウザ起動)

※iOS用にuniversalLinksOnlyというプロパティがありますが、url_launcuer側で「ユニバーサルリンクの場合はアプリを起動し、それ以外はブラウザで開く」挙動を実現できているため、利用機会はほぼ無いと認識しています(ちなみに、universalLinksOnly: trueとした場合にユニバーサルリンクではないページにアクセスしようとするとPlatformExceptionとなります)。

launch(
  url,
  forceSafariVC: false, // default: false
  forceWebView: true, // default: true
);

アプリ内ブラウザのネイテイブの挙動を見る

iOSでは内部でSafari View Controllerが使われています。iOS 9,10ではSafariアプリとCookie等のデータが共有されていたそうですが、以降はアプリごとに独立しており、特段Webサイトをハックすることもできなさそうで、セキュリティ面でのケアは不要そうです。

https://github.com/flutter/plugins/blob/10c8f9e3fba3ff98dd9344a09b1b6bb98efe3959/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m#L26

The view controller includes Safari features such as Reader, AutoFill, Fraudulent Website Detection, and content blocking. In iOS 9 and 10, it shares cookies and other website data with Safari. The user's activity and interaction with SFSafariViewController are not visible to your app, which cannot access AutoFill data, browsing history, or website data. You do not need to secure data between your app and Safari.
https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller

Androidのアプリ内ブラウザはWebViewのフルスクリーン表示

一方で、Androidの場合はアプリ内ブラウザ(=WebViewのフルスクリーン表示)の形となっており、表示するだけであれば良いですが、何かしらのセキュリティ面でのケアが必要な場合は対応できません。WebView表示をするのであれば前述のwebview_flutterの方がカスタマイズ性能が高く、細かいハンドリングができるので、その辺りは要件に併せて臨機応変に選定できると良いと思います。

https://github.com/flutter/plugins/blob/10c8f9e3fba3ff98dd9344a09b1b6bb98efe3959/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java#L50

参考

Discussion

ログインするとコメントできます